diff options
346 files changed, 34619 insertions, 19508 deletions
diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 410ad12e..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,46 +0,0 @@ -image: Visual Studio 2015 - -environment: - #DEPLOY_DIR: libqmatrixclient-%APPVEYOR_BUILD_VERSION% - matrix: - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - QTDIR: C:\Qt\5.9\msvc2017_64 - VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" - PLATFORM: - MAKETOOL: cmake - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - QTDIR: C:\Qt\5.9\msvc2017_64 - VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" - PLATFORM: - MAKETOOL: qmake - - QTDIR: C:\Qt\5.9\msvc2015 - VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\vcvarsall.bat" - PLATFORM: x86 - MAKETOOL: cmake - -init: -- call "%QTDIR%\bin\qtenv2.bat" -- set PATH=C:\Qt\Tools\QtCreator\bin;%PATH% -- call "%VCVARS%" %platform% -- cd /D "%APPVEYOR_BUILD_FOLDER%" - -before_build: -- git submodule update --init --recursive -- if %MAKETOOL% == cmake cmake -G "NMake Makefiles JOM" -H. -Bbuild -DCMAKE_CXX_FLAGS="/EHsc /W3" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="%DEPLOY_DIR%" - -build_script: -- if %MAKETOOL% == cmake cmake --build build -- if %MAKETOOL% == qmake qmake && jom - -#after_build: -#- cmake --build build --target install -#- 7z a libqmatrixclient.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: libqmatrixclient.zip diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..d4a3d2cc --- /dev/null +++ b/.clang-format @@ -0,0 +1,182 @@ +# SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +# SPDX-FileCopyrightText: 2019 Marc Deop <marc@marcdeop.com> + +# SPDX-License-Identifier: LGPL-2.1-or-later + +# This is the clang-format configuration style to be used by libQuotient. +# Inspired by: +# https://code.qt.io/cgit/qt/qt5.git/plain/_clang-format +# https://wiki.qt.io/Qt_Coding_Style +# https://wiki.qt.io/Coding_Conventions +# Further information: https://clang.llvm.org/docs/ClangFormatStyleOptions.html + +# For convenience, the file includes commented out settings that we assume +# to borrow from the WebKit style. The values for such settings try to but +# are not guaranteed to coincide with the latest version of the WebKit style. + +# This file assumes ClangFormat 12 or newer + +--- +Language: Cpp +BasedOnStyle: WebKit +#AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +#AlignArrayOfStructures: None # ClangFormat 13 +#AlignConsecutiveMacros: None +#AlignConsecutiveAssignments: None +#AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Left +AlignOperands: Align +#AlignTrailingComments: false +#AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +#AllowAllParametersOfDeclarationOnNextLine: true +#AllowShortEnumsOnASingleLine: true +#AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: true +#AllowShortFunctionsOnASingleLine: All +#AllowShortLambdasOnASingleLine: All +#AllowShortIfStatementsOnASingleLine: Never +#AllowShortLoopsOnASingleLine: false +#AlwaysBreakAfterDefinitionReturnType: None # deprecated +#AlwaysBreakAfterReturnType: None +#AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +#AttributeMacros: +# - __capability +#BinPackArguments: true +#BinPackParameters: true +BraceWrapping: +# AfterCaseLabel: false +# AfterClass: false + AfterControlStatement: Never # Switch to MultiLine with ClangFormat 14 (https://bugs.llvm.org/show_bug.cgi?id=47936) +# AfterEnum: false + AfterFunction: true +# AfterNamespace: false +# AfterStruct: false +# AfterUnion: false +# AfterExternBlock: false +# BeforeCatch: false +# BeforeElse: false +# BeforeLambdaBody: false # Blows up lambdas vertically, even if they become _very_ readable +# BeforeWhile: false +# IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: NonAssignment +#BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Custom +#BreakBeforeInheritanceComma: false # deprecated? +#BreakInheritanceList: BeforeColon +#BreakBeforeTernaryOperators: true +#BreakConstructorInitializersBeforeComma: false # deprecated? +#BreakConstructorInitializers: BeforeComma +#BreakStringLiterals: true +ColumnLimit: 80 +#QualifierAlignment: Leave # ClangFormat 14? +#CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +#ConstructorInitializerIndentWidth: 4 +#ContinuationIndentWidth: 4 +#Cpp11BracedListStyle: true +#DeriveLineEnding: true +#DerivePointerAlignment: false +#EmptyLineAfterAccessModifier: Never # ClangFormat 14 +EmptyLineBeforeAccessModifier: LogicalBlock +#FixNamespaceComments: false # See ShortNamespaces below +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<Qt.+/' + Priority: 24 + - Regex: '^<' + Priority: 32 + - Regex: '"csapi/' + Priority: 2 + - Regex: '"e2ee/' + Priority: 3 + - Regex: '"(events|jobs)/' + Priority: 4 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(_test)?$' +#IncludeIsMainSourceRegex: '' +#IndentAccessModifiers: false # ClangFormat 13 +#IndentCaseLabels: false +#IndentCaseBlocks: false +IndentGotoLabels: false +IndentPPDirectives: AfterHash +#IndentExternBlock: AfterExternBlock +IndentRequires: true +#IndentWidth: 4 +#IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +#LambdaBodyIndentation: Signature # ClangFormat 13 +#MacroBlockBegin: '' +#MacroBlockEnd: '' +#MaxEmptyLinesToKeep: 1 +#NamespaceIndentation: Inner +PenaltyBreakAssignment: 10 +PenaltyBreakBeforeFirstCallParameter: 70 +PenaltyBreakComment: 45 +#PenaltyBreakFirstLessLess: 120 +#PenaltyBreakOpenParenthesis: 0 # ClangFormat 14 +PenaltyBreakString: 200 +#PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 40 +PenaltyReturnTypeOnItsOwnLine: 200 +#PenaltyIndentedWhitespace: 0 +#PointerAlignment: Left +#PPIndentWidth: -1 +#ReferenceAlignment: Pointer # ClangFormat 13 +#ReflowComments: true +#ShortNamespaceLines: 1 # ClangFormat 13 - to use with FixNamespaceComments +#SortIncludes: true +#SortUsingDeclarations: true +#SpaceAfterCStyleCast: false +#SpaceAfterLogicalNot: false +#SpaceAfterTemplateKeyword: true +#SpaceBeforeAssignmentOperators: true +#SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +#SpaceBeforeCtorInitializerColon: true +#SpaceBeforeInheritanceColon: true +#SpaceBeforeParens: ControlStatements +#SpaceBeforeParensOptions: # ClangFormat 14 +# AfterControlStatements: true +# AfterForeachMacros: true +# AfterFunctionDefinitionName: false +# AfterFunctionDeclarationName: false +# AfterIfMacros: true +# AfterOverloadedOperator: false +# BeforeNonEmptyParentheses: false +SpaceAroundPointerQualifiers: After +#SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +#SpaceInEmptyParentheses: false +#SpacesBeforeTrailingComments: 1 +#SpacesInAngles: false # 'Never' since ClangFormat 13 +#SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +#SpacesInCStyleCastParentheses: false +#SpacesInLineCommentPrefix: # ClangFormat 13 +# Minimum: 1 +# Maximum: -1 +#SpacesInParentheses: false +#SpacesInSquareBrackets: false +#SpaceBeforeSquareBrackets: false +#BitFieldColonSpacing: Both +Standard: c++20 +StatementAttributeLikeMacros: + - Q_EMIT + - emit +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION + - DEFINE_EVENT_TYPEID +TabWidth: 4 +#UseCRLF: false +#UseTab: Never +#WhitespaceSensitiveMacros: [] # Whatever's the default, not using it +... + diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..db460c99 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,175 @@ +--- +Checks: '-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-parent-virtual-call,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-string-*,bugprone-stringview-nullptr,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl50-cpp,cert-dcl58-cpp,cert-dcl59-cpp,cert-env33-c,cert-err33-c,cert-err34-c,cert-err60-cpp,cert-fio38-c,cert-flp30-c,cert-mem57-cpp,cert-msc30-c,cert-msc32-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop57-cpp,cert-oop58-cpp,clang-analyzer-core.CallAndMessage,clang-analyzer-core.DivideZero,clang-analyzer-core.NullDereference,clang-analyzer-core.StackAddrEscapeBase,clang-analyzer-core.StackAddressEscape,clang-analyzer-core.UndefinedBinaryOperatorResult,clang-analyzer-core.uninitialized.*,clang-analyzer-cplusplus.*,clang-analyzer-deadcode.DeadStores,clang-analyzer-optin.cplusplus.*,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-init-variables,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-prefer-member-initializer,cppcoreguidelines-pro-bounds-array-to-pointer-decay,cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-slicing,cppcoreguidelines-special-member-functions,cppcoreguidelines-virtual-class-destructor,google-explicit-constructor,google-readability-namespace-comments,google-runtime-int,misc-*,-misc-definitions-in-headers,modernize-avoid-*,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,performance-*,-performance-no-automatic-move,readability-avoid-const-params-in-decls,readability-container-*,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-duplicate-include,readability-else-after-return,readability-function-*,readability-implicit-bool-conversion,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-member-init,readability-redundant-preprocessor,readability-redundant-smartptr-get,readability-redundant-string-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-suspicious-call-argument,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof' +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: file +CheckOptions: + - key: bugprone-argument-comment.IgnoreSingleArgument + value: '1' + - key: bugprone-argument-comment.StrictMode + value: '1' + - key: bugprone-assert-side-effect.AssertMacros + value: assert,NSAssert,NSCAssert,Q_ASSERT,Q_ASSERT_X +# - key: bugprone-assert-side-effect.IgnoredFunctions +# value: '' + - key: bugprone-assert-side-effect.CheckFunctionCalls + value: 'true' +# - key: bugprone-dangling-handle.HandleClasses +# value: 'std::basic_string_view;std::experimental::basic_string_view' +# - key: bugprone-signed-char-misuse.CharTypdefsToIgnore +# value: '' +# - key: bugprone-signed-char-misuse.DiagnoseSignedUnsignedCharComparisons +# value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression + value: 'true' + - key: bugprone-string-constructor.LargeLengthThreshold + value: '8388608' + - key: bugprone-string-constructor.StringNames + value: '::std::basic_string;::std::basic_string_view' + - key: bugprone-string-constructor.WarnOnLargeLength + value: 'true' +# - key: bugprone-suspicious-enum-usage.StrictMode +# value: 'false' +# - key: bugprone-suspicious-include.HeaderFileExtensions +# value: ';h;hh;hpp;hxx' +# - key: bugprone-suspicious-include.ImplementationFileExtensions +# value: 'c;cc;cpp;cxx' +# - key: bugprone-suspicious-missing-comma.SizeThreshold +# value: '5' +# - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison +# value: 'false' +# - key: bugprone-suspicious-string-compare.StringCompareLikeFunctions +# value: '' +# - key: bugprone-too-small-loop-variable.MagnitudeBitsUpperLimit +# value: '16' +# - key: bugprone-unhandled-self-assignment.WarnOnlyIfThisHasSuspiciousField +# value: 'true' +# - key: cert-dcl59-cpp.HeaderFileExtensions +# value: ';h;hh;hpp;hxx' +# - key: cert-msc32-c.DisallowedSeedTypes +# value: 'time_t,std::time_t' +# - key: cert-msc51-cpp.DisallowedSeedTypes +# value: 'time_t,std::time_t' + - key: cppcoreguidelines-narrowing-conversions.IgnoreConversionFromTypes + value: 'size_t;ptrdiff_t;size_type;difference_type' +# - key: cppcoreguidelines-narrowing-conversions.PedanticMode +# value: 'false' +# - key: cppcoreguidelines-narrowing-conversions.WarnOnEquivalentBitWidth +# value: 'true' +# - key: cppcoreguidelines-narrowing-conversions.WarnOnIntegerToFloatingPointNarrowingConversion +# value: 'true' + - key: cppcoreguidelines-narrowing-conversions.WarnWithinTemplateInstantiation + value: 'true' +# - key: cppcoreguidelines-pro-type-member-init.UseAssignment +# value: 'false' +# - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted +# value: 'false' +# - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor +# value: 'false' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '1' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '25' + - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' +# - key: misc-non-private-member-variables-in-classes.IgnorePublicMemberVariables +# value: 'false' +# - key: modernize-loop-convert.MakeReverseRangeFunction +# value: '' +# - key: modernize-loop-convert.MakeReverseRangeHeader +# value: '' +# - key: modernize-loop-convert.MaxCopySize +# value: '16' +# - key: modernize-loop-convert.NamingStyle +# value: CamelCase +# - key: modernize-loop-convert.UseCxx20ReverseRanges +# value: 'true' +# - key: modernize-make-shared.IgnoreMacros +# value: 'true' +# - key: modernize-make-shared.IgnoreDefaultInitialization +# value: 'true' +# - key: modernize-make-unique.IgnoreMacros +# value: 'true' +# - key: modernize-make-unique.IgnoreDefaultInitialization +# value: 'true' + - key: modernize-use-auto.MinTypeNameLength + value: '0' +# - key: modernize-use-auto.RemoveStars +# value: 'false' +# - key: modernize-use-bool-literals.IgnoreMacros +# value: 'true' +# - key: modernize-use-default-member-init.IgnoreMacros +# value: 'true' + - key: modernize-use-default-member-init.UseAssignment + value: 'true' +# - key: modernize-use-emplace.SmartPointers +# value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: modernize-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' +# - key: modernize-use-emplace.TupleTypes +# value: '::std::pair;::std::tuple' +# - key: modernize-use-equals-default.IgnoreMacros +# value: 'true' +# - key: modernize-use-equals-delete.IgnoreMacros +# value: 'true' +# - key: modernize-use-noexcept.UseNoexceptFalse +# value: 'true' +# - key: modernize-use-using.IgnoreMacros +# value: 'true' + - key: modernize-raw-string-literal.DelimiterStem + value: '' +# - key: modernize-raw-string-literal.ReplaceShorterLiterals +# value: 'false' +# - key: performance-faster-string-find.StringLikeClasses +# value: '::std::basic_string;::std::basic_string_view' +# - key: performance-for-range-copy.AllowedTypes +# value: '' +# - key: performance-for-range-copy.WarnOnAllAutoCopies +# value: 'false' +# - key: performance-inefficient-string-concatenation.StrictMode +# value: 'false' + - key: performance-inefficient-vector-operation.VectorLikeClasses + value: '::std::vector,QVector,::std::deque' +# - key: performance-unnecessary-copy-initialization.AllowedTypes +# value: '' + - key: readability-else-after-return.WarnOnConditionVariables + value: 'true' +# - key: readability-else-after-return.WarnOnUnfixable +# value: 'true' +# - key: readability-function-size.StatementThreshold +# value: '800' +# - key: readability-function-cognitive-complexity.DescribeBasicIncrements +# value: 'true' +# - key: readability-function-cognitive-complexity.IgnoreMacros +# value: 'true' +# - key: readability-function-cognitive-complexity.Threshold +# value: '25' +# - key: readability-implicit-bool-conversion.AllowIntegerConditions +# value: 'false' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: 'true' +# - key: readability-inconsistent-declaration-parameter-name.IgnoreMacros +# value: 'true' + - key: readability-inconsistent-declaration-parameter-name.Strict + value: 'true' +# - key: readability-qualified-auto.AddConstToQualified +# value: 'true' +# - key: readability-redundant-declaration.IgnoreMacros +# value: 'true' +# - key: readability-redundant-member-init.IgnoreBaseInCopyConstructors +# value: 'false' +# - key: readability-redundant-smartptr-get.IgnoreMacros +# value: 'true' + - key: readability-simplify-boolean-expr.ChainedConditionalAssignment + value: 'true' + - key: readability-simplify-boolean-expr.ChainedConditionalReturn + value: 'true' +# - key: readability-uniqueptr-delete-release.PreferResetCall +# value: 'false' +# - key: readability-uppercase-literal-suffix.IgnoreMacros +# value: 'true' + - key: readability-uppercase-literal-suffix.NewSuffixes + value: 'f;F' +... + 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 new file mode 100644 index 00000000..40ed85d1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,263 @@ +name: CI + +on: + push: + pull_request: + types: [opened, reopened] + +defaults: + run: + shell: bash + +concurrency: ci-${{ github.ref }} + +jobs: + CI: + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.qt-version != '5.15.2' }} # Qt 6 will fail for now + strategy: + fail-fast: false + max-parallel: 1 + matrix: + os: [ ubuntu-22.04, macos-11 ] + qt-version: [ '6.3.1', '5.15.2' ] + compiler: [ LLVM ] + # Not using binary values here, to make the job captions more readable + e2ee: [ '', e2ee ] + update-api: [ '', update-api ] + static-analysis: [ '' ] + platform: [ '' ] + qt-arch: [ '' ] + exclude: + - qt-version: '6.3.1' + update-api: update-api # Generated code is not specific to Qt version + - os: ubuntu-22.04 + e2ee: e2ee # Will be re-added with static analysis below + # TODO: Enable E2EE on Windows and macOS + - os: macos-11 + e2ee: e2ee + include: + - os: windows-2019 + qt-version: '5.15.2' + compiler: MSVC + platform: x64 + qt-arch: win64_msvc2019_64 + - os: ubuntu-22.04 + qt-version: '5.15.2' + compiler: LLVM + e2ee: e2ee + static-analysis: codeql + - os: ubuntu-22.04 + qt-version: '5.15.2' + compiler: GCC + e2ee: e2ee + static-analysis: sonar + - os: ubuntu-22.04 + qt-version: '5.15.2' + compiler: GCC + e2ee: e2ee + update-api: update-api + - os: ubuntu-22.04 + qt-version: '5.15.2' + compiler: LLVM + update-api: update-api + - os: windows-2019 + qt-version: '6.3.1' + compiler: MSVC + # e2ee: e2ee # TODO + platform: x64 + qt-arch: win64_msvc2019_64 + - os: windows-2019 + qt-version: '5.15.2' + compiler: MSVC + update-api: update-api + platform: x64 + qt-arch: win64_msvc2019_64 + + env: + SONAR_SERVER_URL: 'https://sonarcloud.io' + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Cache Qt + id: cache-qt + uses: actions/cache@v2 + with: + path: ${{ runner.workspace }}/Qt + key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache + + - name: Install Qt + uses: jurplel/install-qt-action@v2.14.0 + with: + version: ${{ matrix.qt-version }} + arch: ${{ matrix.qt-arch }} + cached: ${{ steps.cache-qt.outputs.cache-hit }} + + - name: Setup build environment + run: | + if [ '${{ matrix.compiler }}' == 'GCC' ]; then + echo "CC=gcc" >>$GITHUB_ENV + echo "CXX=g++" >>$GITHUB_ENV + if [ '${{ startsWith(matrix.qt-version, '5') }}' == 'true' ]; then + # Patch Qt to avoid GCC tumbling over QTBUG-90568/QTBUG-91909 + sed -i 's/ThreadEngineStarter<void>(ThreadEngine<void> \*_threadEngine)/ThreadEngineStarter(ThreadEngine<void> \*_threadEngine)/' \ + $Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h + fi + echo "VALGRIND=valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=$GITHUB_WORKSPACE/quotest/.valgrind.supp" >>$GITHUB_ENV + elif [[ '${{ runner.os }}' != 'Windows' ]]; then + echo "CC=clang" >>$GITHUB_ENV + echo "CXX=clang++" >>$GITHUB_ENV + fi + if grep -q 'refs/tags' <<<'${{ github.ref }}'; then + VERSION="$(git describe --tags)" + elif [ '${{ github.ref }}' == 'refs/heads/master' ]; then + VERSION="ci${{ github.run_number }}-$(git rev-parse --short HEAD)" + else + VERSION="$(git describe --all --contains)-ci${{ github.run_number }}-$(git rev-parse --short HEAD)" + fi + + echo "QUOTEST_ORIGIN=$VERSION @ ${{ runner.os }}/Qt-${{ matrix.qt-version }}/${{ matrix.compiler }}" >>$GITHUB_ENV + + CMAKE_ARGS="-G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_SHARED_LIBS=${{ runner.os == 'Linux' }} \ + -DCMAKE_INSTALL_PREFIX=~/.local \ + -DCMAKE_PREFIX_PATH=~/.local \ + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON \ + -DBUILD_WITH_QT6=${{ startsWith(matrix.qt-version, '6') }}" + + if [ '${{ matrix.static-analysis }}' == 'sonar' ]; then + mkdir -p $HOME/.sonar + CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CXX_FLAGS=--coverage" + fi + echo "CMAKE_ARGS=$CMAKE_ARGS" >>$GITHUB_ENV + + if [[ '${{ runner.os }}' != 'Windows' ]]; then + BIN_DIR=/bin + echo "LIB_PATH=$HOME/.local/lib" >>$GITHUB_ENV + fi + echo "BIN_DIR=$BIN_DIR" >>$GITHUB_ENV + echo "~/.local$BIN_DIR" >>$GITHUB_PATH + + cmake -E make_directory ${{ runner.workspace }}/build + echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV + + - name: Install Ninja (macOS/Windows) + if: ${{ !startsWith(matrix.os, 'ubuntu') }} + uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Install dependencies (Linux) + if: startsWith(matrix.os, 'ubuntu') + run: | + if [ -n "${{ matrix.e2ee }}" ]; then + EXTRA_DEPS="libssl-dev libolm-dev" + echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with E2EE" >>$GITHUB_ENV + fi + sudo apt-get -qq install ninja-build valgrind $EXTRA_DEPS + + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 + if: matrix.compiler == 'MSVC' + with: + arch: ${{ matrix.platform }} + + - name: Download and set up Sonar Cloud tools + if: matrix.static-analysis == '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 QtKeychain + run: | + cd .. + git clone -b v0.13.2 https://github.com/frankosterfeld/qtkeychain.git + cmake -S qtkeychain -B qtkeychain/build $CMAKE_ARGS + cmake --build qtkeychain/build --target install + + - name: get CS API definitions; clone and build GTAD + if: matrix.update-api + run: | + git clone --depth=1 https://github.com/quotient-im/matrix-spec.git ../matrix-spec + git submodule update --init --recursive --depth=1 + cmake -S gtad/gtad -B ../build/gtad $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF + cmake --build ../build/gtad + echo "CMAKE_ARGS=$CMAKE_ARGS -DMATRIX_SPEC_PATH=${{ runner.workspace }}/matrix-spec \ + -DGTAD_PATH=${{ runner.workspace }}/build/gtad/gtad" \ + >>$GITHUB_ENV + echo "QUOTEST_ORIGIN=$QUOTEST_ORIGIN with API files regeneration" >>$GITHUB_ENV + + - name: Initialize CodeQL tools + if: matrix.static-analysis == 'codeql' + uses: github/codeql-action/init@v2 + 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_PATH $CMAKE_ARGS \ + -DQuotient_ENABLE_E2EE=${{ matrix.e2ee }} -DQuotient_INSTALL_TESTS=ON + + - name: Regenerate API code + if: matrix.update-api + run: cmake --build ../build/libQuotient --target update-api + + - name: Build and install libQuotient + run: | + $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;quotient.events.debug=true' + QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}' + run: | + CTEST_ARGS="--test-dir $BUILD_PATH --output-on-failure" + if [[ -z '${{ matrix.e2ee }}' || '${{ runner.os }}' != 'Linux' ]]; then + ctest $CTEST_ARGS -E testolmaccount + else + autotests/run-tests.sh $CTEST_ARGS + fi + [[ -z "$TEST_USER" ]] || \ + LD_LIBRARY_PATH=$LIB_PATH \ + $VALGRIND quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN" + timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually + + - name: Perform CodeQL analysis + if: matrix.static-analysis == 'codeql' + uses: github/codeql-action/analyze@v2 + + - name: Run sonar-scanner + if: matrix.static-analysis == 'sonar' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + mkdir .coverage && pushd .coverage + find $BUILD_PATH -name '*.gcda' -print0 \ + | xargs -0 gcov -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 @@ -1,12 +1,28 @@ +# usual build directory names build -.kdev4 +build_dir -# Qt Creator project file -*.user +# IDE project files/directories +*.kdev4 +.directory +*.user* +.idea # qmake derivatives Makefile* object_script.* .qmake* debug/ -release/
\ No newline at end of file +release/ + +# CMake derivatives +CMakeCache.txt +cmake_install.cmake +Makefile +Quotient_autogen/ +.cmake/ +tests/.cmake/ + +# clangd +.cache/ +compile_commands.json diff --git a/.gitmodules b/.gitmodules index e69de29b..f3aef316 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gtad/gtad"] + path = gtad/gtad + url = https://github.com/quotient-im/gtad.git diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0b2967cf..00000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -language: cpp - -addons: - apt: - sources: - - ubuntu-toolchain-r-test - - sourceline: 'ppa:beineri/opt-qt571-trusty' - packages: - - g++-5 - - qt57base - - valgrind - -matrix: - include: - - os: linux - compiler: gcc - env: [ 'ENV_EVAL="CC=gcc-5 && CXX=g++-5"' ] - - os: linux - compiler: clang - - os: osx - env: [ 'ENV_EVAL="brew update && brew install qt5 && PATH=/usr/local/opt/qt/bin:$PATH"' ] - -before_install: -- eval "${ENV_EVAL}" -- if [ "$TRAVIS_OS_NAME" = "linux" ]; then VALGRIND="valgrind $VALGRIND_OPTIONS"; . /opt/qt57/bin/qt57-env.sh; fi - -install: -- git clone https://github.com/QMatrixClient/matrix-doc.git -- git clone --recursive https://github.com/KitsuneRal/gtad.git -- pushd gtad -- cmake -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} . -- cmake --build . -- popd - -before_script: -- mkdir build && pushd build -- cmake -DMATRIX_DOC_PATH="matrix-doc" -DGTAD_PATH="gtad/gtad" -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_PREFIX=../install .. -- cmake --build . --target update-api -- popd - -script: -- cmake --build build --target all -- cmake --build build --target install -# Build qmc-example with the installed library -- mkdir build-example && pushd build-example -- cmake -DCMAKE_PREFIX_PATH=../install ../examples -- cmake --build . --target all -- popd -# Build and install with qmake -- qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" -- make all -# Run the qmake-compiled qmc-example under valgrind -- if [ "$QMC_TEST_USER" != "" ]; then $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" qmc-example-travis '#qmc-test:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi - -notifications: - webhooks: - urls: - - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGtpdHN1bmUlM0FtYXRyaXgub3JnLyUyMVBDelV0eHRPalV5U3hTZWxvZiUzQW1hdHJpeC5vcmc" - on_success: change # always|never|change - on_failure: always - on_start: never diff --git a/.valgrind.qmc-example.supp b/.valgrind.qmc-example.supp deleted file mode 100644 index 6f0bf60a..00000000 --- a/.valgrind.qmc-example.supp +++ /dev/null @@ -1,152 +0,0 @@ -{ - libc_dirty_free_on_exit - Memcheck:Free - fun:free - fun:__libc_freeres - fun:_vgnU_freeres - fun:__run_exit_handlers - fun:exit -} - -{ - sendPostedEvents1 - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - fun:_ZN15QtSharedPointer20ExternalRefCountData9getAndRefEPK7QObject - obj:/opt/qt56/lib/libQt5Network.so.* -} - -{ - sendPostedEvents3 - Memcheck:Leak - ... - obj:/opt/qt56/lib/libQt5Network.so.* - fun:_ZN7QObject5eventEP6QEvent - fun:_ZN16QCoreApplication6notifyEP7QObjectP6QEvent - fun:_ZN16QCoreApplication15notifyInternal2EP7QObjectP6QEvent - fun:_ZN23QCoreApplicationPrivate16sendPostedEventsEP7QObjectiP11QThreadData - obj:/opt/qt56/lib/libQt5Core.so.* -} - -{ - QAuthenticator - Memcheck:Leak - match-leak-kinds: possible - ... - fun:_ZN14QAuthenticator6detachEv -} - -{ - QObject_connect - Memcheck:Leak - match-leak-kinds: possible - ... - obj:/opt/qt56/lib/libQt5Core.so.* - fun:_ZN7QObject7connectEPKS_PKcS1_S3_N2Qt14ConnectionTypeE -} - -{ - QNetworkProxy - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - fun:_ZN13QNetworkProxyC1ENS_9ProxyTypeERK7QStringtS3_S3_ - obj:/opt/qt56/lib/libQt5Network.so.* -} - -{ - QTimer - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - fun:_ZN7QObjectC1EPS_ - fun:_ZN6QTimerC1EP7QObject -} - -{ - QSslConfiguration - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - ... - fun:_ZN17QSslConfigurationC1Ev -} - -{ - sendPostedEvents6 - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - ... - obj:/opt/qt56/lib/libQt5Network.so.* - fun:_ZN7QObject5eventEP6QEvent - fun:_ZN16QCoreApplication6notifyEP7QObjectP6QEvent - fun:_ZN16QCoreApplication15notifyInternal2EP7QObjectP6QEvent - fun:_ZN23QCoreApplicationPrivate16sendPostedEventsEP7QObjectiP11QThreadData -} - -{ - QMetaObject_activate_in_QtNetwork - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - ... - obj:/opt/qt56/lib/libQt5Network.so.* - fun:_ZN11QMetaObject8activateEP7QObjectiiPPv -} - -{ - QMapDatabase_from_QtNetwork - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - fun:_ZN12QMapDataBase10createDataEv - obj:/opt/qt56/lib/libQt5Network.so.* -} - -{ - QThread - Memcheck:Leak - match-leak-kinds: possible - ... - fun:_ZN7QThread5startENS_8PriorityE -} - -{ - libcrypto_ASN1 - Memcheck:Leak - match-leak-kinds: definite - fun:malloc - ... - fun:ASN1_item_ex_d2i -} - -{ - QObject_from_QtNetwork - Memcheck:Leak - match-leak-kinds: possible - fun:_Znwm - fun:_ZN7QObjectC1EPS_ - obj:/opt/qt56/lib/libQt5Network.so.* -} - -{ - array_new_from_QtNetwork - Memcheck:Leak - match-leak-kinds: possible - fun:_Znam - obj:/opt/qt56/lib/libQt5Network.so.* -} - -{ - malloc_from_libcrypto - Memcheck:Leak - match-leak-kinds: possible - fun:malloc - fun:CRYPTO_malloc - ... - obj:/lib/x86_64-linux-gnu/libcrypto.so.1.0.0 - ... - obj:/opt/qt56/lib/libQt5Network.so.* -} diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a1950b3..b021411c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,21 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.16) +if (POLICY CMP0092) +cmake_policy(SET CMP0092 NEW) +endif() -project(qmatrixclient CXX) +set(API_VERSION "0.7") +project(Quotient VERSION "${API_VERSION}.0" LANGUAGES CXX) -include(CheckCXXCompilerFlag) -if (NOT WIN32) - include(GNUInstallDirs) -endif(NOT WIN32) +message(STATUS) +message(STATUS "Configuring ${PROJECT_NAME} ${PROJECT_VERSION} ==>") -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) +include(FeatureSummary) +include(CTest) + +# https://github.com/quotient-im/libQuotient/issues/369 +option(${PROJECT_NAME}_ENABLE_E2EE "end-to-end encryption (E2EE) support" OFF) +add_feature_info(EnableE2EE ${PROJECT_NAME}_ENABLE_E2EE + "end-to-end encryption (WORK IN PROGRESS)") # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) @@ -18,187 +25,373 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() +if (CMAKE_BUILD_TYPE) + message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") +endif(CMAKE_BUILD_TYPE) -if (NOT CMAKE_INSTALL_LIBDIR) - set(CMAKE_INSTALL_LIBDIR ".") +message(STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) +include(CheckCXXCompilerFlag) +if (MSVC) + add_compile_options(/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 all pedantic extra error=return-type) # Switch these on + CHECK_CXX_COMPILER_FLAG("-W${FLAG}" W${FLAG}_SUPPORTED) + if (W${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "W(no-)?${FLAG}($| )") + add_compile_options(-W${FLAG}) + endif () + endforeach () + foreach (FLAG unused-parameter gnu-zero-variadic-macro-arguments + subobject-linkage) # Switch these off + CHECK_CXX_COMPILER_FLAG("-Wno-${FLAG}" Wno-${FLAG}_SUPPORTED) + if (Wno-${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "W(no-)?${FLAG}($| )") + add_compile_options(-Wno-${FLAG}) + endif() + endforeach () endif() -if (NOT CMAKE_INSTALL_BINDIR) - set(CMAKE_INSTALL_BINDIR ".") -endif() +if (WIN32) + if (NOT CMAKE_INSTALL_LIBDIR) + set(CMAKE_INSTALL_LIBDIR ".") + set(CMakeFilesLocation "cmake") + else() + set(CMakeFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") + endif() -if (NOT CMAKE_INSTALL_INCLUDEDIR) - set(CMAKE_INSTALL_INCLUDEDIR "include") -endif() + if (NOT CMAKE_INSTALL_BINDIR) + set(CMAKE_INSTALL_BINDIR ".") + endif() -set(CMAKE_CXX_STANDARD 14) + if (NOT CMAKE_INSTALL_INCLUDEDIR) + set(CMAKE_INSTALL_INCLUDEDIR "include") + endif() +else() + include(GNUInstallDirs) + set(INCLUDEDIR_INIT ${PROJECT_NAME}) + set(CMakeFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") +endif(WIN32) +set(${PROJECT_NAME}_INSTALL_INCLUDEDIR + "${CMAKE_INSTALL_INCLUDEDIR}/${INCLUDEDIR_INIT}" CACHE PATH + "directory to install ${PROJECT_NAME} include files to") +message(STATUS "Install Prefix: ${CMAKE_INSTALL_PREFIX}") +message(STATUS " Header files will be installed to ${CMAKE_INSTALL_PREFIX}/${${PROJECT_NAME}_INSTALL_INCLUDEDIR}") -foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) - CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) - if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${FLAG}") - endif () -endforeach () +# Instruct CMake to run moc automatically when needed. +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) -find_package(Qt5 5.4.1 REQUIRED Network Gui) -get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) +option(BUILD_WITH_QT6 "Build Quotient with Qt 6 (EXPERIMENTAL)" OFF) -if (GTAD_PATH) - get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" ABSOLUTE) -endif () -if (MATRIX_DOC_PATH) - get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" ABSOLUTE) -endif () +if (BUILD_WITH_QT6) + set(QtMinVersion "6.0") +else() + set(QtMinVersion "5.15") + 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}") + +find_package(${Qt}Keychain REQUIRED) + +if (${PROJECT_NAME}_ENABLE_E2EE) + find_package(${Qt} ${QtMinVersion} REQUIRED Sql) + find_package(Olm 3.2.5 REQUIRED) + set_package_properties(Olm PROPERTIES + DESCRIPTION "Implementation of the Olm and Megolm cryptographic ratchets" + URL "https://gitlab.matrix.org/matrix-org/olm" + TYPE REQUIRED + ) + if (Olm_FOUND) + message(STATUS "Using libOlm ${Olm_VERSION} at ${Olm_DIR}") + endif() + + find_package(OpenSSL 1.1.0 REQUIRED) + set_package_properties(OpenSSL PROPERTIES + DESCRIPTION "Open source SSL and TLS implementation and cryptographic library" + URL "https://www.openssl.org/" + TYPE REQUIRED + ) + if (OpenSSL_FOUND) + message(STATUS "Using OpenSSL ${OpenSSL_VERSION} at ${OpenSSL_DIR}") + endif() +endif() -message( STATUS ) -message( STATUS "=============================================================================" ) -message( STATUS " libqmatrixclient Build Information" ) -message( STATUS "=============================================================================" ) -if (CMAKE_BUILD_TYPE) - message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") -endif(CMAKE_BUILD_TYPE) -message( STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) -message( STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}" ) -if (MATRIX_DOC_PATH AND GTAD_PATH) - message( STATUS "Generating API stubs enabled" ) - message( STATUS " Using GTAD at ${ABS_GTAD_PATH}" ) - message( STATUS " Using API files at ${ABS_API_DEF_PATH}" ) -endif () -message( STATUS "=============================================================================" ) -message( STATUS ) # Set up source files -set(libqmatrixclient_SRCS - lib/networkaccessmanager.cpp - lib/connectiondata.cpp - lib/connection.cpp - lib/logging.cpp - lib/room.cpp - lib/user.cpp - lib/avatar.cpp - lib/settings.cpp - lib/networksettings.cpp - lib/converters.cpp - lib/util.cpp - lib/eventitem.cpp - lib/events/event.cpp - lib/events/roomevent.cpp - lib/events/stateevent.cpp - lib/events/eventcontent.cpp - lib/events/roommessageevent.cpp - lib/events/roommemberevent.cpp - lib/events/typingevent.cpp - lib/events/receiptevent.cpp - lib/events/callanswerevent.cpp - lib/events/callcandidatesevent.cpp - lib/events/callhangupevent.cpp - lib/events/callinviteevent.cpp - lib/events/directchatevent.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/quotient_export.h + lib/function_traits.h lib/function_traits.cpp + lib/omittable.h + lib/expected.h + lib/networkaccessmanager.h lib/networkaccessmanager.cpp + lib/connectiondata.h lib/connectiondata.cpp + lib/connection.h lib/connection.cpp + lib/ssosession.h lib/ssosession.cpp + lib/logging.h lib/logging.cpp + lib/room.h lib/room.cpp + lib/roomstateview.h lib/roomstateview.cpp + lib/user.h lib/user.cpp + lib/avatar.h lib/avatar.cpp + lib/uri.h lib/uri.cpp + lib/uriresolver.h lib/uriresolver.cpp + lib/eventstats.h lib/eventstats.cpp + lib/syncdata.h lib/syncdata.cpp + lib/settings.h lib/settings.cpp + lib/networksettings.h lib/networksettings.cpp + lib/converters.h lib/converters.cpp + lib/util.h lib/util.cpp + lib/eventitem.h lib/eventitem.cpp + lib/accountregistry.h lib/accountregistry.cpp + lib/mxcreply.h lib/mxcreply.cpp + lib/e2ee/e2ee.h # because it's used by generated API + 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/single_key_value.h + lib/events/simplestateevents.h + lib/events/eventcontent.h lib/events/eventcontent.cpp + lib/events/eventrelation.h lib/events/eventrelation.cpp + lib/events/roomcreateevent.h lib/events/roomcreateevent.cpp + lib/events/roomtombstoneevent.h lib/events/roomtombstoneevent.cpp + lib/events/roommessageevent.h lib/events/roommessageevent.cpp + lib/events/roommemberevent.h lib/events/roommemberevent.cpp + lib/events/roomcanonicalaliasevent.h + lib/events/roomavatarevent.h + lib/events/roompowerlevelsevent.h lib/events/roompowerlevelsevent.cpp + lib/events/typingevent.h + lib/events/accountdataevents.h + lib/events/receiptevent.h lib/events/receiptevent.cpp + lib/events/reactionevent.h + lib/events/callevents.h lib/events/callevents.cpp + 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/stickerevent.h + lib/events/filesourceinfo.h lib/events/filesourceinfo.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 + res.qrc ) +if (${PROJECT_NAME}_ENABLE_E2EE) + list(APPEND lib_SRCS + lib/database.h lib/database.cpp + lib/keyverificationsession.h lib/keyverificationsession.cpp + lib/e2ee/qolmaccount.h lib/e2ee/qolmaccount.cpp + lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp + lib/e2ee/qolminboundsession.h lib/e2ee/qolminboundsession.cpp + lib/e2ee/qolmoutboundsession.h lib/e2ee/qolmoutboundsession.cpp + lib/e2ee/qolmutils.h lib/e2ee/qolmutils.cpp + lib/e2ee/qolmutility.h lib/e2ee/qolmutility.cpp + lib/e2ee/qolmsession.h lib/e2ee/qolmsession.cpp + lib/e2ee/qolmmessage.h lib/e2ee/qolmmessage.cpp + lib/events/keyverificationevent.h + ) +endif() + +# Configure API files generation set(CSAPI_DIR csapi) set(FULL_CSAPI_DIR lib/${CSAPI_DIR}) -set(FULL_CSAPI_SRC_DIR ${ABS_API_DEF_PATH}/client-server) set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) -if (MATRIX_DOC_PATH AND GTAD_PATH) + +set(API_GENERATION_ENABLED 0) +if (NOT MATRIX_SPEC_PATH AND MATRIX_DOC_PATH) + set(MATRIX_SPEC_PATH ${MATRIX_DOC_PATH}) +endif() +if (GTAD_PATH AND MATRIX_SPEC_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_SPEC_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_SPEC_PATH}/api" REALPATH) + endif () + if (IS_DIRECTORY ${ABS_API_DEF_PATH}) + set(API_GENERATION_ENABLED 1) + else () + message( WARNING "${MATRIX_SPEC_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 (API_GENERATION_ENABLED) + message( STATUS "Using GTAD at ${ABS_GTAD_PATH}" ) + 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 PROGRAM_ARGS CLANG_FORMAT_ARGS) + if (NOT ABS_CLANG_FORMAT) + message( WARNING "${CLANG_FORMAT} is NOT FOUND; API files won't be formatted") + 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(update-api - ${ABS_GTAD_PATH} --config ${CSAPI_DIR}/gtad.yaml --out ${CSAPI_DIR} + ${ABS_GTAD_PATH} --config ../gtad/gtad.yaml --out ${CSAPI_DIR} ${FULL_CSAPI_SRC_DIR} - cas_login_redirect.yaml- cas_login_ticket.yaml- old_sync.yaml- room_initial_sync.yaml- # deprecated + 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 ${FULL_CSAPI_DIR}/gtad.yaml - ${FULL_CSAPI_DIR}/{{base}}.h.mustache - ${FULL_CSAPI_DIR}/{{base}}.cpp.mustache + SOURCES gtad/gtad.yaml + gtad/data.h.mustache + gtad/operation.h.mustache + gtad/operation.cpp.mustache ${API_DEFS} VERBATIM ) endif() +add_feature_info(EnableApiCodeGeneration "${API_GENERATION_ENABLED}" + "build target update-api") + +# Produce the list of all Matrix API files for building the library. When this +# list changes (normally after calling GTAD), CONFIGURE_DEPENDS will force +# the build system to call CMake again. Checking for the glob change slows down +# each build (even if the target does not involve API generation). It would be +# ideal if GTAD could compare the initial (saved somewhere) and the generated +# file list itself and write down to some .cmake file if those are different, +# which would trigger the reconfiguration specifically before the next build. +# For now CONFIGURE_DEPENDS is the best approximation of that. +file(GLOB_RECURSE api_ALL_SRCS CONFIGURE_DEPENDS + ${FULL_CSAPI_DIR}/*.* lib/${ASAPI_DEF_DIR}/*.* lib/${ISAPI_DEF_DIR}/*.*) + +add_library(${PROJECT_NAME} ${lib_SRCS} ${api_ALL_SRCS}) +# Set BUILDING_SHARED_QUOTIENT if building as a shared library +target_compile_definitions(${PROJECT_NAME} PRIVATE + $<$<STREQUAL:$<TARGET_PROPERTY:${PROJECT_NAME},TYPE>,SHARED_LIBRARY>:BUILDING_SHARED_QUOTIENT>) +# Set QUOTIENT_STATIC in a static library setting +target_compile_definitions(${PROJECT_NAME} PUBLIC + $<$<STREQUAL:$<TARGET_PROPERTY:${PROJECT_NAME},TYPE>,STATIC_LIBRARY>:QUOTIENT_STATIC>) +target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_TO_ASCII) + +target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + ${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 + VISIBILITY_INLINES_HIDDEN ON + CXX_VISIBILITY_PRESET hidden + VERSION "${PROJECT_VERSION}" + SOVERSION ${API_VERSION} + INTERFACE_${PROJECT_NAME}_MAJOR_VERSION ${API_VERSION} +) +set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY + COMPATIBLE_INTERFACE_STRING ${PROJECT_NAME}_MAJOR_VERSION) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) +if (MSVC) + target_compile_options(${PROJECT_NAME} PUBLIC /Zc:preprocessor) +endif() + +# Don't use PCH w/GCC (https://bugzilla.redhat.com/show_bug.cgi?id=1721553#c34) +if (NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) + target_precompile_headers(${PROJECT_NAME} PRIVATE lib/converters.h) +endif () -aux_source_directory(${FULL_CSAPI_DIR} libqmatrixclient_job_SRCS) -aux_source_directory(${FULL_CSAPI_DIR}/definitions libqmatrixclient_csdef_SRCS) -aux_source_directory(${FULL_CSAPI_DIR}/definitions/wellknown libqmatrixclient_cswellknown_SRCS) -aux_source_directory(lib/${ASAPI_DEF_DIR} libqmatrixclient_asdef_SRCS) -aux_source_directory(lib/${ISAPI_DEF_DIR} libqmatrixclient_isdef_SRCS) - -set(example_SRCS examples/qmc-example.cpp) - -add_library(QMatrixClient ${libqmatrixclient_SRCS} - ${libqmatrixclient_job_SRCS} ${libqmatrixclient_csdef_SRCS} - ${libqmatrixclient_cswellknown_SRCS} - ${libqmatrixclient_asdef_SRCS} ${libqmatrixclient_isdef_SRCS}) -set(API_VERSION "0.4") -set_property(TARGET QMatrixClient PROPERTY VERSION "${API_VERSION}.0") -set_property(TARGET QMatrixClient PROPERTY SOVERSION ${API_VERSION} ) -set_property(TARGET QMatrixClient PROPERTY - INTERFACE_QMatrixClient_MAJOR_VERSION ${API_VERSION}) -set_property(TARGET QMatrixClient APPEND PROPERTY - COMPATIBLE_INTERFACE_STRING QMatrixClient_MAJOR_VERSION) - -target_include_directories(QMatrixClient PUBLIC +target_include_directories(${PROJECT_NAME} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib> - $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> + $<INSTALL_INTERFACE:${${PROJECT_NAME}_INSTALL_INCLUDEDIR}> ) -target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui) +if (${PROJECT_NAME}_ENABLE_E2EE) + target_link_libraries(${PROJECT_NAME} Olm::Olm + OpenSSL::Crypto + OpenSSL::SSL + ${Qt}::Sql) + set(FIND_DEPS "find_dependency(Olm) + find_dependency(OpenSSL) + find_dependency(${Qt}Sql)") # For QuotientConfig.cmake.in +endif() + +target_include_directories(${PROJECT_NAME} PRIVATE ${QTKEYCHAIN_INCLUDE_DIRS}) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${QTKEYCHAIN_LIBRARIES}) + +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) -add_executable(qmc-example ${example_SRCS}) -target_link_libraries(qmc-example Qt5::Core QMatrixClient) -configure_file(QMatrixClient.pc.in ${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient.pc @ONLY NEWLINE_STYLE UNIX) +# Configure testing + +if (BUILD_TESTING) + enable_testing() + add_subdirectory(quotest) + add_subdirectory(autotests) +endif() -# Installation +# Configure installation -install(TARGETS QMatrixClient EXPORT QMatrixClientTargets +install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets + LIBRARY RUNTIME ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + INCLUDES DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} ) -install(DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +install(DIRECTORY lib/ DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h") include(CMakePackageConfigHelpers) +# NB: SameMajorVersion doesn't really work yet, as we're within 0.x trail. +# Maybe consider jumping the gun and releasing 1.0, as semver advises? write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" - VERSION ${API_VERSION} - COMPATIBILITY AnyNewerVersion + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}ConfigVersion.cmake" + COMPATIBILITY SameMajorVersion ) -export(PACKAGE QMatrixClient) -export(EXPORT QMatrixClientTargets - FILE "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientTargets.cmake") -configure_file(cmake/QMatrixClientConfig.cmake - "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfig.cmake" - COPYONLY +export(PACKAGE ${PROJECT_NAME}) +export(EXPORT ${PROJECT_NAME}Targets + FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}Targets.cmake") +configure_file(cmake/${PROJECT_NAME}Config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}Config.cmake" + @ONLY ) -set(ConfigFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/QMatrixClient") -install(EXPORT QMatrixClientTargets - FILE QMatrixClientTargets.cmake DESTINATION ${ConfigFilesLocation}) +install(EXPORT ${PROJECT_NAME}Targets + FILE ${PROJECT_NAME}Targets.cmake DESTINATION ${CMakeFilesLocation}) -install(FILES cmake/QMatrixClientConfig.cmake - "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" - DESTINATION ${ConfigFilesLocation} +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}ConfigVersion.cmake" + DESTINATION ${CMakeFilesLocation} ) -# Only available from CMake 3.7; reserved for future use -#install(EXPORT_ANDROID_MK QMatrixClientTargets DESTINATION share/ndk-modules) +install(EXPORT_ANDROID_MK ${PROJECT_NAME}Targets DESTINATION ${CMAKE_INSTALL_DATADIR}/ndk-modules) if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) endif (WIN32) -install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) - if (UNIX AND NOT APPLE) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) endif() + +message(STATUS) +feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES + FATAL_ON_MISSING_REQUIRED_PACKAGES) + +message(STATUS "<== End of libQuotient configuration") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ee39eec..fb10c7da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,15 +17,15 @@ The long-read part: ## General information For specific proposals, please provide them as -[pull requests](https://github.com/QMatrixClient/libQMatrixClient/pulls) +[pull requests](https://github.com/quotient-im/libQuotient/pulls) or -[issues](https://github.com/QMatrixClient/libqmatrxclient/issues) +[issues](https://github.com/quotient-im/libQuotient/issues) For general discussion, feel free to use our Matrix room: -[#quaternion:matrix.org](https://matrix.to/#/#quaternion:matrix.org). +[#quotient:matrix.org](https://matrix.to/#/#quotient:matrix.org). If you're new to the project (or FLOSS in general), -[issues tagged as simple](https://github.com/QMatrixClient/libQMatrixClient/labels/simple) -are smaller tasks that may typically take 1-3 days. +[issues tagged as easy](https://github.com/quotient-im/libQuotient/labels/easy) +are smaller tasks that don't require much knowledge about the project. You are welcome aboard! ### Pull requests and different branches recommended @@ -35,8 +35,8 @@ See the GitHub Help [articles about pull requests](https://help.github.com/artic to learn how to deal with them. We recommend creating different branches for different (logical) -changes, and creating a pull request when you're done into the master branch. -See the GitHub documentation on +changes, and creating a pull request when you're done; the development +integration branch is `dev`. See the GitHub documentation on [creating branches](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/) and [using pull requests](https://help.github.com/articles/using-pull-requests/). @@ -44,8 +44,8 @@ and ### How we handle proposals We use GitHub to track all changes via its -[issue tracker](https://github.com/QMatrixClient/libQMatrixClient/issues) and -[pull requests](https://github.com/QMatrixClient/libQMatrixClient/pulls). +[issue tracker](https://github.com/quotient-im/libQuotient/issues) and +[pull requests](https://github.com/quotient-im/libQuotient/pulls). Specific changes are proposed using those mechanisms. Issues are assigned to an individual who works on it and then marks it complete. If there are questions or objections, the conversation area of that @@ -86,33 +86,30 @@ a commit without a DCO is an accident and the DCO still applies. --> ### License -Unless a contributor explicitly specifies otherwise, we assume that all -contributed code is released under [the same license as libQMatrixClient itself](./COPYING), -which is LGPL v2.1 as of the time of this writing. +Unless a contributor explicitly specifies otherwise, we assume contributors +to agree that all contributed code is released either under *LGPL v2.1 or later*. +This is more than just [LGPL v2.1 libQuotient now uses](./COPYING) +because the project plans to switch to LGPL v3 for library code in the near future. <!-- The below is invalid yet! All new contributed material that is not executable, including all text when not executed, is also released under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/) or later. --> Any components proposed for reuse should have a license that permits releasing -a derivative work under LGPL v2.1. Moreover, the license of a proposed component -should be approved by OSI, no exceptions. +a derivative work under *LGPL v3 or later* (that includes licenses permitting +*LGPL v2.1 or later* but not *LGPL v2.1 only*). In any case, the component +should be redistributable under a license from +[the list approved by OSI](https://opensource.org/licenses), no exceptions. -## Vulnerability reporting (security issues) +We use [SPDX](https://spdx.dev) conventions for copyright statements. Please +follow them when making a sizable contribution: add your name and year to +the top of the file. New files should begin with the following preamble: +```cpp +// SPDX-FileCopyrightText: 2021 Your Name <your@email.address> +// SPDX-License-Identifier: LGPL-2.1-or-later +``` -If you find a significant vulnerability, or evidence of one, -use either of the following contacts: -* send an email to Kitsune Ral [Kitsune-Ral@users.sf.net](mailto:Kitsune-Ral@users.sf.net) -* reach out in Matrix to #kitsune:matrix.org (if you can, switch encryption **on**) - -In any of these two options, _indicate that you have such information_ -(do not share the information yet), and we'll tell you the next steps. - -By default, we will give credit to anyone who reports a vulnerability so that -we can fix it. If you want to remain anonymous or pseudonymous instead, please -let us know that; we will gladly respect your wishes. If you provide a fix as -a PR, you have no way to remain anonymous but we still accept contributors with -pseudonyms. +## Vulnerability reporting (security issues) - see [SECURITY.md](./SECURITY.md) ## Documentation changes @@ -121,7 +118,7 @@ filename extension. Any help on fixing/extending these is more than welcome. Where reasonable, limit yourself to Markdown that will be accepted by different markdown processors (e.g., what is specified by CommonMark or the original -Markdown). In practice, as long as libQMatrixClient is hosted at GitHub, +Markdown). In practice, as long as libQuotient is hosted at GitHub, [GFM (GitHub-flavoured Markdown)](https://help.github.com/articles/github-flavored-markdown/) is used to show those files in a browser, so it's fine to use its extensions. In particular, you can mark code snippets with the programming language used; @@ -136,10 +133,9 @@ unfortunately this other algorithm is *also* called GitHub-flavoured markdown. In your markdown, please don't use tab characters and avoid "bare" URLs. In a hyperlink, the link text and URL should be on the same line. While historically we didn't care about the line length in markdown texts -(and more often than not put the whole paragraph into one line), this is subject -to change anytime soon, with 80-character limit _recommendation_ -(which is softer than the limit for C/C++ code) imposed on everything -_except hyperlinks_ (because wrapping hyperlinks breaks the rendering). +(and more often than not put the whole paragraph into one line), this is no more +recommended; instead, try to use 80-character limit (similar to the limit for +C/C++ code) _except hyperlinks_ - wrapping breaks them. Do not use trailing two spaces for line breaks, since these cannot be seen and may be silently removed by some tools. If, for whatever reason, a blank line @@ -147,103 +143,293 @@ is not an option, use <tt><br /></tt> (an HTML break). ## End of TL;DR -If you don't plan/have substantial contributions, you can end reading here. Further sections are for those who's going to actively hack on the library code. +If you don't plan/have substantial contributions, you can stop reading here. +Further sections are for those who's going to actively hack on the library code. ## Code changes -The code should strive to be DRY (don't repeat yourself), clear, and obviously correct. Some technical debt is inevitable, just don't bankrupt us with it. Refactoring is welcome. - -### Generated C++ code for CS API -The code in lib/csapi, although it resides in Git, is actually generated from the official Matrix Swagger/OpenAPI definition files. If you're unhappy with something in that directory and want to improve the code, you have to understand the way these files are produced and setup some additional tooling. The shortest possible procedure resembling the below text can be found in .travis.yml (our Travis CI configuration actually regenerates those files upon every build). The generating sequence only works with CMake atm; patches to enable it with qmake are (you guessed it) very welcome. - -#### Why generate the code at all? -Because before both original authors of libQMatrixClient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). - -#### Prerequisites for CS API code generation -1. Get the source code of GTAD and its dependencies, e.g. using the command: `git clone --recursive https://github.com/KitsuneRal/gtad.git` -2. Build GTAD: in the source code directory, do `cmake . && cmake --build .` (you might need to pass `-DCMAKE_PREFIX_PATH=<path to Qt>`, similar to libQMatrixClient itself). -3. Get the Matrix CS API definitions that are included in the matrix-doc repo: `git clone https://github.com/QMatrixClient/matrix-doc.git` (QMatrixClient/matrix-doc is a fork that's known to produce working code; you may want to use your own fork if you wish to alter something in the API). - -#### Generating lib/csapi contents -1. Pass additional configuration to CMake when configuring libQMatrixClient, namely: `-DMATRIX_DOC_PATH=<path you your matrix-doc repo> -DGTAD_PATH=<path to gtad binary (not the repo!)>`. If everything's right, these two CMake variables will be mentioned in CMake output and will trigger configuration of an additional build target, see the next step. -2. Generate the code: `cmake --build <your build dir> --target update-api`; if you use CMake with GNU Make, you can just do `make update-api` instead. Building this target will create (overwriting without warning) .h and .cpp files in lib/csapi directory for all YAML files it can find in `matrix-doc/api/client-server`. -3. Once you've done that, you can build the library as usual; rerunning CMake is recommended if the list of generated files has changed. - -#### Changing things in lib/csapi -See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQMatrixClient are described here. - -GTAD uses the following three kinds of sources: -1. OpenAPI files. Each file is treated as a separate source (because this is how GTAD works now). -2. A configuration file, in our case it's lib/csapi/gtad.yaml - this one is common for the whole API. -3. Source code template files: lib/csapi/{{base}}.*.mustache - also common. - -The mustache files have a templated (not in C++ sense) definition of a network job, deriving from BaseJob; each job class is prepended, if necessary, with data structure definitions used by this job. The look of those files is hideous for a newcomer; the fact that there's no highlighter for the combination of Mustache (originally a web templating language) and C++ doesn't help things, either. To slightly simplify things some more or less generic constructs are defined in gtad.yaml (see its "mustache:" section). Adventurous souls that would like to figure what's going on in these files should speak up in the libQMatrixClient room - I (Kitsune) will be very glad to help you out. - -The types map in gtad.yaml is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": -* `avoidCopy` - this attribute defines whether a const ref should be used instead of a value. For basic types like int this is obviously unnecessary; but compound types like `QVector` should rather be taken by reference when possible. -* `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). The template will use `T&&` instead of `T` or `const T&` to pass such types around. -* `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h` - a substitute for `std::optional` from C++17 (we're still at C++14 yet). -* `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable). -* `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`. - -Instead of relying on the event structure definition in the OpenAPI files, gtad.yaml uses pointers to libQMatrixClient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. - -### Library API and doc-comments - -Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen (with backslashes) style is preferred. You can find that some parts of the code still use JavaDoc (with @'s) style; feel free to replace it with Doxygen backslashes if that bothers you. Some parts are not even documented; add doc-comments to them is highly encouraged. - -Calls, data structures and other symbols not intended for use by clients should _not_ be exposed in (public) .h files, unless they are necessary to declare other public symbols. In particular, this involves private members (functions, typedefs, or variables) in public classes; use pimpl idiom to hide implementation details as much as possible. - -Note: As of now, all header files of libQMatrixClient are considered public; this may change eventually. - -### Qt-flavoured C++ - -This is our primary language. We don't have a particular code style _as of yet_ but some rules-of-thumb are below: -* 4-space indents, no tabs, no trailing spaces, no last empty lines. If you spot the code abusing these - we'll thank you for fixing it. -* Prefer keeping lines within 80 characters. -* Braces after if's, while's, do's, function signatures etc. take a separate line. Keeping the opening brace on the same line is still ok. -* A historical deviation from the usual Qt code format conventions is an extra indent inside _classes_ (access specifiers go at +4 spaces to the base, members at +8 spaces) but not _structs_ (members at +4 spaces). This may change in the future for something more conventional. -* Please don't make "hypocritical structs" with protected or private members. Just make them classes instead. -* For newly created classes, keep to [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three) - make sure to read about the rule of zero if you haven't before, it's not what you might think it is. -* Qt containers are generally preferred to STL containers; however, there are notable exceptions, and libQMatrixClient already uses them: +The code should strive to be DRY (don't repeat yourself), clear, and obviously +correct (i.e. buildable). Some technical debt is inevitable, +just don't bankrupt us with it. Refactoring is welcome. + +### Code style and formatting + +As of Quotient 0.7, the C++ standard for newly written code is C++20. + +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 +accepted in PRs; however, unless explicitly marked with `// clang-format off` +and `// clang-format on`, these deviations will be rectified any commit soon +after. + +Notable things from .clang-format: +* 4-space indents, no tabs, no trailing spaces, no last empty lines. If you + spot the code abusing these - thank you for fixing it. +* Prefer keeping lines within 80 characters. Slight overflows are ok only + if that helps readability. + +Additionally: +* Please don't make "hypocritical structs" with protected or private members. + In general, `struct` is used to denote a plain-old-data structure, rather + than data+behaviour. If you need access control or are adding yet another + non-trivial (construction, assignment) member function to a `struct`, + just make it a `class` instead. +* For newly created classes, keep to + [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three) - + make sure to read about the rule of zero if you haven't before, it's not + what you might think it is. +* Qt containers are generally preferred to STL containers; however, there are + notable exceptions, and libQuotient already uses them: * `std::array` and `std::deque` have no direct counterparts in Qt. - * Because of COW semantics, Qt containers cannot hold uncopyable classes. Classes without a default constructor are a problem too. Examples of that are `SyncRoomData` and `EventsArray<>`. Use STL containers for those but see the next point and also consider if you can supply a reasonable copy/default constructor. - * STL containers can be freely used in code internal to a translation unit (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up code where you don't need COW, or when dealing with uncopyable data structures (see the previous point). However, exposing STL containers in the API is not encouraged (except where absolutely necessary, e.g. we use `std::deque` for a timeline). Exposing STL containers or iterators in API intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work and therefore unlikely to be accepted into `master`. -* Use `QVector` instead of `QList` where possible - see a [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) for details. + * Because of COW semantics, Qt containers cannot hold uncopyable classes. + Classes without a default constructor are a problem too. Examples of that + are `SyncRoomData` and `EventsArray<>`. Use STL containers for structures + having those but see the next point and also consider if you can supply + a reasonable copy/default constructor. + * STL containers can be freely used in code internal to a translation unit + (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. + It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up + code where you don't need COW, or when dealing with uncopyable + data structures (see the previous point). However, exposing STL containers + in the API is not encouraged (except where absolutely necessary, e.g. we use + `std::deque` for a timeline). Especially when it comes to API intended + for usage from QML (e.g. `Q_PROPERTY`), STL containers or iterators are + unlikely to work and therefore unlikely to be accepted into `dev`. + * Notwithstanding the above (you're not going to use them with QML anyway), + prefer `std::unique_ptr<>` over `QScopedPointer<>` as it gives stronger + guarantees. +* Always use `QVector` instead of `QList` unless Qt's own API uses it - see the + [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) + for details. With Qt 6, these two become the same type matching what used + to be `QVector` in Qt 5. + +### API conventions + +Calls, data structures and other symbols not intended for use by clients +should _not_ be exposed in (public) .h files, unless they are necessary +to declare other public symbols. In particular, this involves private members +(functions, typedefs, or variables) in public classes; use pimpl idiom to hide +implementation details as much as possible. `_impl` namespace is reserved for +definitions that should not be used by clients and are not covered by +API guarantees. + +Note: As of now, all header files of libQuotient are considered public; +this may change eventually. + +### Comments + +Whenever you add a new call to the library API that you expect to be used +from client code, make sure to supply a proper doc-comment along with the call. +Quotient uses the Doxygen style; some legacy code may use Javadoc style but it +is not encouraged any more. Some parts are not documented at all; +adding doc-comments to them is highly encouraged and is a great first-time +contribution. + +Use `\brief` for the summary, and follow with details after +an empty doc-comment line, using `\param`, `\return` etc. as necessary. + +For in-code comments, the advice is as follows: +* Don't restate what's happening in the code unless it's not really obvious. + We assume the readers to have some command of C++ and Qt. If your code is + not obvious, consider making it clearer itself before commenting. +* That said, both C++ and Qt have their arcane features and dark corners, + and we don't want to limit anybody who feels they have a case for + variadic templates, raw literals, and so on. Use your experience to figure + what might be less well-known to readers and comment such cases: leave + references to web pages, Quotient wiki etc. +* Make sure to document not so much "what" but more "why" certain code is done + the way it is. In the worst case, the logic of the code can be + reverse-engineered; but you can almost never reverse-engineer the line of + reasoning and the pitfalls avoided. ### Automated tests -There's no testing framework as of now; either Catch or Qt Test or both will be used eventually. However, as a stopgap measure, qmc-example is used for automated end-to-end testing. - -Any significant addition to the library API should be accompanied by a respective test in qmc-example. To add a test you should: -- Add a new private slot to the `QMCTest` class. -- Add to the beginning of the slot the line `running.push_back("Test name");`. -- Add test logic to the slot, using `QMC_CHECK` macro to assert the test outcome. ALL (even failing) branches should conclude with a QMC_CHECK invocation, unless you intend to have a "DID NOT FINISH" message in the logs under certain conditions. -- Call the slot from `QMCTest::startTests()`. - -`QMCTest` sets up some basic test fixture to help you with testing; notably by the moment `startTests()` is invoked you can rely on having a working connection in `c` member variable and a test room in `targetRoom` member variable. PRs to introduce a proper testing framework are very welcome (make sure to migrate tests from qmc-example though); shifting qmc-example to use Qt Test seems to be a particularly low-hanging fruit. +We gradually introduce autotests based on a combination of CTest and Qt Test +frameworks - see `autotests/` directory. There are very few of those, as we +have just started adding those to the new code (you guessed it; adding more +tests to the old code is very welcome). + +Aside from that, libQuotient comes with a command-line end-to-end test suite +called Quotest. Any significant addition to the library API should be +accompanied by a respective test in `autotests/` and/or in Quotest. + +To add a test to autotests: +- In a new .cpp file in `autotests/`, define a test class derived from + QObject with `private Q_SLOTS:` section having the member functions called + for testing. If you feel more comfortable using a header file to define + the class, feel free to do so. If you're new to Qt Test framework, use + existing tests as a guidance. +- Add a `quotient_add_test` macro call with your test to + `autotests/CMakeLists.txt` + +To add a test to Quotest: +- In `quotest.cpp`, add a new test to the `TestSuite` class. Similar to Qt Test, + each test in Quotest is a private slot; unlike Qt Test, you should use + special macros, `TEST_DECL()` and `TEST_IMPL()`, to declare and define + the test (those macros conceal passing the testing handle in `thisTest` + variable to the test method). +- In the test function definition, add test logic using `FINISH_TEST` macro + to check for the test outcome and complete the test (be mindful that + `FINISH_TEST` always `return`s, not only in case of error). ALL (even failing) + branches should conclude with a `FINISH_TEST` (or `FAIL_TEST` that is + a shortcut for a failing `FINISH_TEST`) invocation, unless you intend to have + a "DID NOT FINISH" message in the logs in certain conditions. + +The `TestManager` class sets up some basic test fixture to help you with testing; +notably, the tests can rely on having an initialised `Room` object with loaded +state for the test room in `targetRoom` member variable. Note that it's normal +for tests to go async, which is not something Qt Test is easy with (and this +is why Quotest doesn't directly use Qt Test but rather fetches a few ideas +from it). ### Security, privacy, and performance -Pay attention to security, and work *with* (not against) the usual security hardening mechanisms (however few in C++). +Pay attention to security, and work *with*, not against, the usual security hardening practices (however few in C++). -`char *` and similar unchecked C-style read/write arrays are forbidden - use Qt containers or at the very least `std::array<>` instead. Where you see fit (usually with data structures), try to use smart pointers, especially `std::unique_ptr<>` or `QScopedPointer` instead of bare pointers. When dealing with `QObject`s, use the parent-child ownership semantics exercised by Qt (this is preferred to using smart pointers). Shared pointers are not used in the code so far; but if you find a particular use case where the strict semantic of unique pointers doesn't help and a shared pointer is necessary, feel free to step up with the working code and it will be considered for inclusion. +`char *` and similar unchecked C-style read/write arrays are forbidden - use +Qt containers or at the very least `std::array<>` instead. Where you see fit +(usually with data structures), try to use smart pointers, especially +`std::unique_ptr<>` or `QScopedPointer` instead of bare pointers. When dealing +with `QObject`s, use the parent-child ownership semantics exercised by Qt +(this is preferred to using smart pointers). If you find a particular use case +where the strict semantic of unique pointers doesn't help and a shared pointer +is necessary, feel free to step up with the working code and it will be +considered for inclusion. Exercise the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) where reasonable and appropriate. Prefer less-coupled cohesive code. -Protect private information, in particular passwords and email addresses. Absolutely _don't_ spill around this information in logs - use `access_token` and similar opaque ids instead, and only display those in UI where needed. Do not forget about local access to data (in particular, be very careful when storing something in temporary files, let alone permanent configuration or state). Avoid mechanisms that could be used for tracking where possible (we do need to verify people are logged in but that's pretty much it), and ensure that third parties can't use interactions for tracking. Matrix protocols evolve towards decoupling the personally identifiable information from user activity entirely - follow this trend. +Protect private information, in particular passwords and email addresses. +Absolutely _don't_ spill around this information in logs, and only display +those in UI where really needed. Do not forget about local access to data +(in particular, be very careful when storing something in temporary files, +let alone permanent configuration or state). Avoid mechanisms that could be +used for tracking where possible (we do need to verify people are logged in +but that's pretty much it), and ensure that third parties can't use interactions +for tracking. Matrix protocols evolve towards decoupling +the personally identifiable information from user activity entirely - follow +this trend. + +We want the software to have decent performance for users even on weaker +machines. At the same time we keep libQuotient single-threaded as much +as possible, to keep the code simple. That means being cautious about operation +complexity (read about big-O notation if you need a kickstart on the subject). +This especially refers to operations on the whole timeline and the list of +users - each of these can have tens of thousands of elements so even operations +with linear complexity, if heavy enough (with I/O or complex processing), +can produce noticeable GUI freezing or stuttering. When you don't see a way +to reduce algorithmic complexity, either split processing into isolated +pieces that can be individually scheduled as queued events (see the end of +`Connection::consumeRoomData()` to get the idea) or uncouple the logic from +GUI and execute it outside of the main thread with `QtConcurrent` facilities. + +Having said that, there's always a trade-off between various attributes; +in particular, readability and maintainability of the code is more important +than squeezing every bit out of that clumsy algorithm. Beware of premature +optimization and profile the code before before diving into hardcore tweaking +that might not give the benefits you think it would. + +Speaking of profiling logs (see README.md on how to turn them on) - if you +expect some code to take considerable (more than 10k "simple operations") time +you might want to setup a `QElapsedTimer` and drop the elapsed time into logs +under `PROFILER` logging category. See the existing code for examples - +`room.cpp` has quite a few. In order to reduce small timespan logging spam, +`PROFILER` log lines are usually guarded by a check that the timer counted big +enough time (200 microseconds by default, 20 microseconds for tighter parts); +this threshold can be altered at compile-time by defining `PROFILER_LOG_USECS` +preprocessor symbol (i.e. passing `-DPROFILE_LOG_USECS=<usecs>` to the compiler +if you're on Linux/macOS). + +### Generated C++ code for CS API +The code in `lib/csapi`, `lib/identity` and `lib/application-service`, although +it resides in Git, is actually generated from the official Swagger/OpenAPI +definition files. If you're unhappy with something in there and want to improve +the code, you have to understand the way these files are produced and setup +some additional tooling. The shortest possible procedure resembling +the below text can be found in .github/workflows/ci.yml (our CI configuration +tests regeneration of those files). As described below, there is also a handy +build target for CMake. -We want the software to have decent performance for typical users. At the same time we keep libQMatrixClient single-threaded as much as possible, to keep the code simple. That means being cautious about operation complexity (read about big-O notation if you need a kickstart on the topic). This especially refers to operations on the whole timeline and the list of users - each of these can have tens of thousands of elements so even operations with linear complexity, if heavy enough, can produce noticeable GUI freezing. When you don't see a way to reduce algorithmic complexity, embed occasional `processEvents()` invocations in heavy loops (see `Connection::saveState()` to get the idea). +#### Why generate the code at all? +Because otherwise we have to do monkey business of writing boilerplate code, +with the same patterns, types etc., literally, for every single API endpoint, +and one of libQuotient authors got fed up with it at some point in time. +By then about 15 job classes were written; the entire API is about 100 endpoints +and counting. Besides, the existing jobs had to be updated according to changes +in CS API that have been, and will keep, coming. Other considerations can be +found in [this talk about API description languages](https://youtu.be/W5TmRozH-rg) +that also briefly touches on GTAD. -Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important than squeezing every bit out of that clumsy algorithm. Beware of premature optimization and have profiling data around before going into some hardcore optimization. +#### Prerequisites for CS API code generation +1. Get the source code of GTAD and its dependencies. Recent libQuotient + includes GTAD as a submodule so you can get everything you need by updating + gtad/gtad submodule in libQuotient sources: + `git submodule update --init --recursive gtad/gtad`. + + You can also just clone GTAD sources to keep them separate from libQuotient: + `git clone --recursive https://github.com/quotient-im/gtad.git` +2. Configure and build GTAD: same as libQuotient, it uses CMake so this should + be quite straightforward (if not - you're probably not quite ready for this + stuff anyway). +3. Get Matrix CS API definitions from a matrix-spec repo. Although the official + repo is at https://github.com/matrix-org/matrix-spec.git` (formerly + https://github.com/matrix-org/matrix-doc.git), you may or may not be able + to generate working code from it because the way it evolves is not + necessarily in line with libQuotient needs. For that reason, a soft fork + of the official definitions is kept at + https://github.com/quotient-im/matrix-spec.git that guarantees buildability + of the generated code. This repo closely follows the official one (but maybe + not its freshest commit), applying a few adjustments on top. And of course + you can use your own repository if you need to change the API definition. +4. If you plan to submit a PR with the generated code to libQuotient or just + would like it to be properly formatted, you should either ensure you have + clang-format (version 10 at least) in your PATH or pass + `-DCLANG_FORMAT=<path>` to CMake, as mentioned in the next section. + +#### Generating CS API contents +1. Pass additional configuration to CMake when configuring libQuotient: + `-DMATRIX_SPEC_PATH=/path/to/matrix-spec/ -DGTAD_PATH=/path/to/gtad`. + Note that `MATRIX_SPEC_PATH` should lead to the repo while `GTAD_PATH` should + have the path to GTAD binary. If you need to specify where your clang-format + is (see the previous section) add `-DCLANG_FORMAT=/path/to/clang-format` to + the line above. If everything's right, the detected locations will be + mentioned in CMake output and will trigger configuration of an additional + build target called `update-api`. +2. Generate the code: `cmake --build <your build dir> --target update-api`. + Building this target will create (overwriting without warning) source files + in `lib/csapi`, `lib/identity`, `lib/application-service` for all YAML files + it can find in `/path/to/matrix-spec/data/api/client-server` and their + dependencies. + +#### Changing generated code +See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQuotient are described here. -Speaking of profiling logs (see README.md on how to turn them on) - in order -to reduce small timespan logging spam, there's a default limit of at least -200 microseconds to log most operations with the PROFILER -(aka libqmatrixclient.profile.debug) logging category. You can override this -limit by passing the new value (in microseconds) in PROFILER_LOG_USECS to -the compiler. In the future, this parameter will be made changeable at runtime -_if_ needed. +GTAD uses the following three kinds of sources: +1. OpenAPI files. Each file is treated as a separate source (unlike + swagger-codegen, you do _not_ need to have a single file for the whole API). +2. A configuration file, in Quotient case it's `gtad/gtad.yaml` - common for + all OpenAPI files GTAD is invoked on. +3. Source code template files: `gtad/*.mustache` - are also common. + +The Mustache files have a templated (not in C++ sense) definition of a network +job class derived from BaseJob; if necessary, data structure definitions used +by this job are put before the job class. Bigger Mustache files look a bit +hideous for a newcomer; and the only known highlighter that can handle +the combination of Mustache (originally a web templating language) and C++ can +be found in CLion IDE. Fortunately, all our Mustache files are reasonably +concise and well-formatted these days. +To simplify things some reusable Mustache blocks are defined in `gtad.yaml` - +see its `mustache:` section. Adventurous souls that would like to figure +what's going on in these files should speak up in the Quotient room - +I (Kitsune) will be very glad to help you out. + +The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": +* `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<>`). +* `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`. + +Instead of relying on the event structure definition in the OpenAPI files, `gtad.yaml` uses pointers to libQuotient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. ## How to check proposed changes before submitting them @@ -253,15 +439,66 @@ your commit into it (with an explanation what it is about and why). ### Standard checks -The following warnings configuration is applied with GCC and Clang when using CMake: `-W -Wall -Wextra -pedantic -Werror=return-type -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments` (the last one is to mute a warning triggered by Qt code for debug logging). We don't turn most of the warnings to errors but please treat them as such. In Qt Creator, the following line can be used with the Clang code model (before Qt Creator 4.7 you should explicitly enable the Clang code model plugin): `-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables` +The warnings configuration applied when using CMake can be found in +`CMakeLists.txt`. Most warnings triggered by that configuration are not formally +considered errors (the compiler will keep going) but please treat them as such. +If you want to be cautious, you can use the following line for your IDE's Clang +analyzer code model to enable as many compiler warnings as reasonable (that +does not include `clang-tidy`/`clazy` warnings - see below on those): +`-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables -Wno-unknown-attributes -Wno-comma -Wno-string-conversion -Wno-return-std-move-in-c++11`. ### Continuous Integration -We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) and MacOS (Clang), and AppVeyor CI to build on Windows (MSVC). Every PR will go through these, and you'll see the traffic lights from them on the PR page. Failure on any platform will most likely entail a request to you for a fix before merging a PR. +We use CI to check buildability and smoke-testing on Linux (GCC, Clang), +MacOS (Clang), and Windows (MSVC). Every PR will go through these, and you'll +see the traffic lights from them on the PR page. If your PR fails +on any platform double-check that it's not your code causing it - and fix +(or ask how to fix if you don't know) if it is. ### Other tools -If you know how to use clang-tidy, here's a list of checks we do and do not use (a leading hyphen means a disabled check, an asterisk is a wildcard): `*,cert-env33-c,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-cppcoreguidelines-pro-type-const-cast,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-special-member-functions,-google-build-using-namespace,-google-readability-braces-around-statements,-hicpp-*,-llvm-*,-misc-unused-parameters,-misc-noexcept-moveconstructor,-modernize-use-using,-readability-braces-around-statements,readability-identifier-naming,-readability-implicit-bool-cast,-clang-diagnostic-*,-clang-analyzer-*`. If you're on CLion, you can simply copy-paste the above list into the Clang-Tidy inspection configuration. In Qt Creator 4.6 and newer, one can enable clang-tidy and clazy (clazy level 1 eats away CPU but produces some very relevant and unobvious notices, such as possible unintended copying of a Qt container, or unguarded null pointers). +Recent versions of Qt Creator and CLion can automatically run your code through +clang-tidy. The source code contains `.clang-tidy` file with the recommended +set of checks that doesn't give too many false positives. + +Qt Creator in addition knows about clazy, a Qt-aware static analysis tool that +hunts for Qt-specific issues that are easy to overlook otherwise, such as +possible unintended copying of a Qt container. Most of clazy checks are relevant +to our code, except: +`fully-qualified-moc-types,overloaded-signal,qstring-comparison-to-implicit-char,foreach,non-pod-global-static,qstring-allocations,jni-signatures,qt4-qstring-from-array`. + +### Submitting API changes + +If you changed the API definitions, the path to upstream becomes somewhat +intricate, as you have to coordinate with two projects, making up to 4 PRs along +the way. The recommended sequence depends on whether or not you have to +[write a Matrix Spec Change aka MSC](https://matrix.org/docs/spec/proposals). +Usually you have to, unless your API changes keep API semantics intact. +In that case: +1. Submit an MSC before submitting changes to the API definition files and + libQuotient. +2. The MSC gets reviewed by the Spec Core Team. This can be a lengthy process + but it's necessary for the Matrix ecosystem integrity. +3. When your MSC has at least some approvals (not necessarily a complete + acceptance but at least some approvals should be there) submit a PR to + libQuotient, referring to your `matrix-spec` repo. Make sure that generated + files are committed separately from non-generated ones (no need to make two + PRs; just separate them in different commits). +4. If/when your libQuotient PR is approved and MSC is not there yet you'll + be asked to submit a PR with API definition files at + `https://github.com/quotient-im/matrix-spec`. Note that this is _not_ + an official repo; but you can refer to your libQuotient PR as + an _implementation_ of the MSC - a necessary step before making a so-called + "spec PR". +5. Once MSC is accepted, submit your `matrix-spec` changes as a PR to + `https://github.com/matrix-org/matrix-spec` (the "spec PR" mentioned above). + This will require that your submission meets the standards set by this + project (they are quite reasonable and not too hard to meet). + +If your changes don't need an MSC, it becomes a more straightforward combination +of 2 PRs: one to `https://github.com/matrix-org/matrix-spec` ("spec PR") and one +to libQuotient (with the same guidance about putting generated and non-generated +files in different commits). ## Git commit messages @@ -279,15 +516,26 @@ When writing git commit messages, try to follow the guidelines in ## Reuse (libraries, frameworks, etc.) -C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very conservative about adding dependencies to libQMatrixClient. That relates to additional Qt components and even more to other libraries. Fortunately, even the Qt components now in use (Qt Core and Network) are very feature-rich and provide plenty of ready-made stuff. - -Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for automated testing, so PRs onboarding a test framework will be considered with much gratitude. +C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very conservative about adding dependencies to libQuotient. That relates to additional Qt components and even more to other libraries. Fortunately, even the Qt components now in use (Qt Core and Network) are very feature-rich and provide plenty of ready-made stuff. Some cases need additional explanation: -* Before rolling out your own super-optimised container or algorithm written from scratch, take a good long look through documentation on Qt and C++ standard library. Please try to reuse the existing facilities as much as possible. -* You should have a good reason (or better several ones) to add a component from KDE Frameworks. We don't rule this out and there's no prejudice against KDE; it just so happened that KDE Frameworks is one of most obvious reuse candidates but so far none of these components survived as libQMatrixClient deps. So we are cautious. -* Never forget that libQMatrixClient is aimed to be a non-visual library; QtGui in dependencies is only driven by (entirely offscreen) dealing with QPixmaps. While there's a bunch of visual code (in C++ and QML) shared between libQMatrixClient-enabled _applications_, this is likely to end up in a separate (libQMatrixClient-enabled) library, rather than libQMatrixClient itself. +* Before rolling out your own super-optimised container or algorithm written + from scratch, take a good long look through documentation on Qt and + C++ standard library. Please try to reuse the existing facilities + as much as possible. +* libQuotient is a library to build Qt applications; for that reason, + components from KDE Frameworks should be really lightweight and useful + to be accepted as a dependency. If the intention is to better integrate + libQuotient into KDE environment there's nothing wrong in building another + library on top of libQuotient. Consider people who run LXDE and normally + don't have KDE frameworks installed (some even oppose installing those) - + libQuotient caters to them too. +* Never forget that libQuotient is aimed to be a non-visual library; + QtGui in dependencies is only driven by (entirely offscreen) dealing with + QImages. While there's a bunch of visual code (in C++ and QML) shared + between Quotient-enabled _applications_, this is likely to end up + in a separate (Quotient-backed) library, rather than libQuotient itself. ## Attribution -This text is largely based on CONTRIBUTING.md from CII Best Practices Badge project, which is a collective work of its contributors (many thanks!). The text itself is licensed under CC-BY-4.0. +This text is based on CONTRIBUTING.md from CII Best Practices Badge project, which is a collective work of its contributors (many thanks!). The text itself is licensed under CC-BY-4.0. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 64a80350..00000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,39 +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. - -libqmatrixclient-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 --> -- **libqmatrixclient 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/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 00000000..aaaba168 --- /dev/null +++ b/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,462 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as +the successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/QMatrixClient.pc.in b/Quotient.pc.in index efb41498..6eb1672e 100644 --- a/QMatrixClient.pc.in +++ b/Quotient.pc.in @@ -3,8 +3,8 @@ exec_prefix=${prefix} includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ -Name: QMatrixClient +Name: Quotient Description: A Qt5 library to write cross-platfrom clients for Matrix Version: @API_VERSION@ Cflags: -I${includedir} -Libs: -L${libdir} -lQMatrixClient +Libs: -L${libdir} -lQuotient @@ -1,104 +1,269 @@ -# libQMatrixClient +# libQuotient (former libQMatrixClient) -[![license](https://img.shields.io/github/license/QMatrixClient/libqmatrixclient.svg)](https://github.com/QMatrixClient/libqmatrixclient/blob/master/COPYING) +<a href='https://matrix.org'><img src='https://matrix.org/docs/projects/images/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a> + +[![license](https://img.shields.io/github/license/quotient-im/libQuotient.svg)](https://github.com/quotient-im/libQuotient/blob/master/COPYING) ![status](https://img.shields.io/badge/status-beta-yellow.svg) -[![release](https://img.shields.io/github/release/QMatrixClient/libqmatrixclient/all.svg)](https://github.com/QMatrixClient/libqmatrixclient/releases/latest) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1023/badge)](https://bestpractices.coreinfrastructure.org/projects/1023) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) +[![release](https://img.shields.io/github/release/quotient-im/libQuotient/all.svg)](https://github.com/quotient-im/libQuotient/releases/latest) +[![](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) +![CI Status](https://img.shields.io/github/workflow/status/quotient-im/libQuotient/CI) +![Sonar Tech Debt](https://img.shields.io/sonar/tech_debt/quotient-im_libQuotient?server=https%3A%2F%2Fsonarcloud.io) +![Sonar Coverage](https://img.shields.io/sonar/coverage/quotient-im_libQuotient?server=https%3A%2F%2Fsonarcloud.io) +![Matrix](https://img.shields.io/matrix/quotient:matrix.org?logo=matrix) -libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Tensor](https://matrix.org/docs/projects/client/tensor.html) and some other projects. +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), +[NeoChat](https://matrix.org/docs/projects/client/neo-chat) and other projects. +Versions 0.5.x and older use the previous name - libQMatrixClient. ## Contacts -You can find authors of libQMatrixClient in the Matrix room: [#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org). - -You can also file issues at [the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues). If you have what looks like a security issue, please see respective instructions in CONTRIBUTING.md. - -## Building and usage -So far the library is typically used as a git submodule of another project (such as Quaternion); however it can be built separately (either as a static or as a dynamic library). As of version 0.2, the library can be installed and CMake package config files are provided; projects can use `find_package(QMatrixClient)` to setup their code with the installed library files. PRs to enable the same for qmake are most welcome. +You can find Quotient developers in the Matrix room: +[#quotient:matrix.org](https://matrix.to/#/#quotient:matrix.org). -The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for one-off building. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), you're advised to make a recursive check out of that project (in this case, Quaternion) and update the library submodule to its master branch. +You can file issues at +[the project issue tracker](https://github.com/quotient-im/libQuotient/issues). +If you find what looks like a security issue, please use instructions +in SECURITY.md. -Tags starting with `v` represent released versions; `rc` mark release candidates. +## Getting and using libQuotient +Depending on your platform, the library can be obtained from a package +management system. Recent releases of Fedora, Debian and openSUSE already have +it. Alternatively, you can build the library from the source and bundle it with +your application, as described below. ### Pre-requisites -- a Linux, OSX or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) - - For Ubuntu flavours - zesty or later (or a derivative) is good enough out of the box; older ones will need PPAs at least for a newer Qt; in particular, if you have xenial you're advised to add Kubuntu Backports PPA for it -- a Git client to check out this repo -- Qt 5 (either Open Source or Commercial), version 5.6 or higher -- a build configuration tool: - - CMake (from your package management system or [the official website](https://cmake.org/download/)) - - or qmake (comes with Qt) -- a C++ toolchain supported by your version of Qt (see a link for your platform at [the Qt's platform requirements page](http://doc.qt.io/qt-5/gettingstarted.html#platform-requirements)) - - GCC 5 (Windows, Linux, OSX), Clang 5 (Linux), Apple Clang 8.1 (OSX) and Visual C++ 2015 (Windows) are the oldest officially supported; Clang 3.8 and GCC 4.9.2 are known to still work, maintenance patches for them are accepted - - 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. +- 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 Bullseye; Fedora 35; + openSUSE Leap 15.4; Ubuntu 22.04 LTS. +- Qt 5.15 or 6 (experimental, as of libQuotient 0.7) - either Open Source or + Commercial +- CMake 3.16 or newer +- A C++ toolchain that supports at least some subset of C++20 (concepts, + in particular): + - GCC 11 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) + and Visual Studio 2019 (Windows) are the oldest officially supported. +- If using E2EE (beta, as of libQuotient 0.7): + - libolm 3.2.5 or newer (the latest 3.x strongly recommended) + - OpenSSL (1.1.x is known to work; 3.x should likely work too). +- 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. -#### OS X -`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you may need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=<where-Qt-installed>` +#### macOS +`brew install qt5` should get you a recent Qt5. You may need to add the output +of `brew --prefix qt5` to `CMAKE_PREFIX_PATH` (see below) to make CMake aware +of the Qt location. + +If using E2EE, you need to perform the same dance for libolm and openssl. #### Windows -1. Install Qt5, using their official installer. -1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\<Qt version>\<toolchain>\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for OS X (see above), also helps. +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. -There are no official MinGW-based 64-bit packages for Qt. If you're determined to build a 64-bit library, either use a Visual Studio toolchain or build Qt5 yourself as described in Qt documentation. +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. -### Building -#### CMake-based -In the root directory of the project sources: -``` +If you're trying out E2EE, you will also need libolm and OpenSSL. Unfortunately, +neither project provides official binary libraries for Windows. libolm can +be compiled from the sources (available at ) using the same toolchain +(CMake+MSVC). It's not recommended to compile OpenSSL yourself; instead, use +one of the "OpenSSL for Windows" links in +[unofficial list on the project Wiki](https://wiki.openssl.org/index.php/Binaries). + + +### Using the library +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. + +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). +Checking out a certain commit or tag (rather than downloading the archive) +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) 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. + +The following commands issued in the root directory of the project sources: +```shell script mkdir build_dir cd build_dir -cmake .. # Pass -DCMAKE_PREFIX_PATH and -DCMAKE_INSTALL_PREFIX here if needed +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. Dynamic builds of libqmatrixclient are only tested on Linux at the moment; experiments with dynamic builds on Windows/OSX are welcome. Taking a look at [qmc-example](https://github.com/QMatrixClient/libqmatrixclient/tree/master/examples) (used to test the library) should give you a basic idea of using libQMatrixClient; for more extensive usage check out the source code of [Quaternion](https://github.com/QMatrixClient/Quaternion) (the reference client built on QMatrixClient). +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. + +Before proceeding, double-check that you have installed development libraries +for all prerequisites above. CMake will stop and tell you if something's missing. + +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: +- `Quotient_INSTALL_TESTS=<ON/OFF>`, `ON` by default - install `quotest` along + with the library files when `install` target is invoked. `quotest` is a small + command-line program that (assuming correct parameters, see `quotest --help`) + that tries to connect to a given room as a given user and perform some basic + Matrix operations, such as sending messages and small files, redaction, + setting room tags etc. This is useful to check the sanity of your library + installation. As of now, `quotest` expects the used homeserver to be able + to get the contents of `#quotient:matrix.org`; this is being fixed in + [#401](https://github.com/quotient-im/libQuotient/issues/401). +- `Quotient_ENABLE_E2EE=<ON/OFF>`, `OFF` by default - enable work-in-progress + E2EE code in the library. As of 0.6, this code is very incomplete and buggy; + you should NEVER use it. In 0.7, the enabled code is beta-quality and is + generally good for trying the technology and API but really not for + mission-critical applications. + + Switching this on will define `Quotient_E2EE_ENABLED` macro (note + the difference from the CMake switch) for compiler invocations on all + Quotient and Quotient-dependent (if it uses `find_package(Quotient)`) + code; so you can use `#ifdef Quotient_E2EE_ENABLED` to guard the code using + E2EE parts of Quotient. +- `MATRIX_SPEC_PATH` and `GTAD_PATH` - these two variables are used to point + CMake to the directory with the matrix-doc repository containing API files + and to a GTAD binary. These two are used to generate C++ files from Matrix + Client-Server API description made in OpenAPI notation. This is not needed + if you just need to build the library; if you're really into hacking on it, + CONTRIBUTING.md elaborates on what these two variables are for. You can install the library with CMake: -``` +```shell script cmake --build . --target install ``` -This will also install cmake package config files; once this is done, you can use `examples/CMakeLists.txt` to compile the example with the _installed_ library. This file is a good starting point for your own CMake-based project using libQMatrixClient. - -#### qmake-based -The library provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `qmc-example.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: -``` -qmake qmc-example.pro -make all -``` -This will get you `debug/qmc-example` and `release/qmc-example` 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. - -Installing the library with qmake is not possible; similarly, a .prl file is not provided. A PR to fix this is welcome. +This will also install cmake package config files; once this is done, you +should be able to use [`quotest/CMakeLists.txt`](quotest/CMakeLists.txt) to compile quotest +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`. ## Troubleshooting #### Building fails -If `cmake` fails with... -``` -CMake Warning at CMakeLists.txt:11 (find_package): - By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project - has asked CMake to find a package configuration file provided by - "Qt5Widgets", but CMake did not find one. -``` -...then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. +- If `cmake` fails with + ``` + CMake Warning at CMakeLists.txt:11 (find_package): + By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project + has asked CMake to find a package configuration file provided by + "Qt5Widgets", but CMake did not find one. + ``` + then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. + +- If `cmake` fails with a message similar to: + ``` + CMake Error at /usr/lib64/cmake/Qt6Core/Qt6CoreVersionlessTargets.cmake:37 (message): + Some (but not all) targets in this export set were already defined. + + Targets Defined: Qt::Core + + Targets not yet defined: Qt::CorePrivate + ``` + then you likely have both Qt 5 and Qt 6 on your system, and your client code + uses a different major version than Quotient. Make sure you use the client + version that matches libQuotient (e.g. you can't configure Quaternion 0.0.95 + with libQuotient 0.7 in Qt 6 mode). + +- If you use GCC and get an "unknown declarator" compilation error in the file +`qtconcurrentthreadengine.h` - unfortunately, it is an actual error in Qt 5.15 +sources, see https://bugreports.qt.io/browse/QTBUG-90568 (or +https://bugreports.qt.io/browse/QTBUG-91909). The Qt company did not make +an open source release with the fix, therefore: + + - if you're on Linux - try to use Qt from your package management system, as + most likely this bug is already fixed in the packages + - if you're on Windows, or if you have to use Qt (5.15) from download.qt.io + for any other reason, you should apply the fix to Qt sources: locate + the file (the GCC error message tells exactly where it is), find the line + with the (strange-looking) `ThreadEngineStarter` constructor definition: + ```cplusplus + ThreadEngineStarter<void>(ThreadEngine<void> \*_threadEngine) + ``` + and remove the template specialisation from the constructor name so that it + looks like + ```cplusplus + ThreadEngineStarter(ThreadEngine<void> \*_threadEngine) + ``` + This will fix your build (and any other build involving QtConcurrent from + this installation of Qt - the fix is not specific to Quotient in any way). #### Logging configuration -libqmatrixclient uses Qt's logging categories to make switching certain types of logging easier. In case of troubles at runtime (bugs, crashes) you can increase logging if you add the following to the `QT_LOGGING_RULES` environment variable: +libQuotient uses Qt's logging categories to make switching certain types of logging easier. In case of troubles at runtime (bugs, crashes) you can increase logging if you add the following to the `QT_LOGGING_RULES` environment variable: ``` -libqmatrixclient.<category>.<level>=<flag> +quotient.<category>.<level>=<flag> ``` where -- `<category>` is one of: `main`, `jobs`, `jobs.sync`, `events`, `events.ephemeral`, and `profiler` (you can always find the full list in the file `logging.cpp`) -- `<level>` is one of `debug` 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 comma 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 +`*` 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 +```shell script +QT_LOGGING_RULES="quotient.*.debug=true;quotient.jobs.debug=false" ``` -QT_LOGGING_RULES="libqmatrixclient.*.debug=true,libqmatrixclient.jobs.debug=false" +Note that `quotient` is a prefix that only works since version 0.6 of +the library; 0.5.x and older used `libqmatrixclient` instead. If you happen +to deal with both libQMatrixClient-era and Quotient-era versions, +it's reasonable to use both prefixes, to make sure you're covered with no +regard to the library version. For example, the above setting could look like +```shell script +QT_LOGGING_RULES="libqmatrixclient.*.debug=true;libqmatrixclient.jobs.debug=false;quotient.*.debug=true;quotient.jobs.debug=false" ``` #### Cache format -In case of troubles with room state and caching it may be useful to switch cache format from binary to JSON. To do that, set the following value in your client's configuration file/registry key (you might need to create the libqmatrixclient key for that): `libqmatrixclient/cache_type` to `json`. This will make cache saving and loading work slightly slower but the cache will be in a text JSON file (very long and unindented so prepare a good JSON viewer or text editor with JSON formatting capabilities). +In case of troubles with room state and caching it may be useful to switch +cache format from binary to JSON. To do that, set the following value in +your client's configuration file/registry key (you might need to create +the libQuotient key for that): `libQuotient/cache_type` to `json`. +This will make cache saving and loading work slightly slower but the cache +will be in text JSON files (possibly very long and unindented so prepare a good +JSON viewer or text editor with JSON formatting capabilities). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..e821aed1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| master | :white_check_mark: | +| 0.6.x | :white_check_mark: | +| 0.5.x | :white_check_mark: | +| older | :x: | + +## Reporting a Vulnerability + +If you find a vulnerability, or evidence of one, use either of the following contacts: +- via email: [Kitsune Ral](mailto:Kitsune-Ral@users.sf.net); or +- via Matrix: [direct chat with @kitsune:matrix.org](https://matrix.to/#/@kitsune:matrix.org?action=chat). + +In any of these two options, indicate that you have such information (do not share it yet), and we'll tell you the next steps. + +By default, we will give credit to anyone who reports a vulnerability in a responsible way so that we can fix it before public disclosure. +If you want to remain anonymous or pseudonymous instead, please let us know; we will gladly respect your wishes. +If you provide a fix as a PR, you have no way to remain anonymous; you also thereby lay out the vulnerability itself +so this is NOT the right way for undisclosed vulnerabilities, whether or not you want to stay incognito. + +## Timeline and commitments + +Initial reaction to the message about a vulnerability (see above) will be +no more than 5 days. From the moment of the private report or public disclosure +(if it hasn't been reported earlier in private) of each vulnerability, we take +effort to fix it on priority before any other issues. In case of vulnerabilities +with [CVSS v2](https://nvd.nist.gov/cvss.cfm) score of 4.0 and higher +the commitment is to provide a workaround within 30 days and a full fix +within 60 days after the project has been made aware about the vulnerability +(in private or in public). For vulnerabilities with lower score there is +no commitment on the timeline, only prioritisation. The full fix doesn't imply +that all software functionality remains accessible (in the worst case +the vulnerable functionality may be disabled or removed to prevent the attack). diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 00000000..48edb168 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +# +# SPDX-License-Identifier: BSD-3-Clause + +include(CMakeParseArguments) + +function(QUOTIENT_ADD_TEST) + cmake_parse_arguments(ARG "" "NAME" "" ${ARGN}) + add_executable(${ARG_NAME} ${ARG_NAME}.cpp) + target_link_libraries(${ARG_NAME} ${Qt}::Core ${Qt}::Test Quotient) + add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME}) +endfunction() + +quotient_add_test(NAME callcandidateseventtest) +quotient_add_test(NAME utiltests) +if(${PROJECT_NAME}_ENABLE_E2EE) + quotient_add_test(NAME testolmaccount) + quotient_add_test(NAME testgroupsession) + quotient_add_test(NAME testolmsession) + quotient_add_test(NAME testolmutility) + quotient_add_test(NAME testfilecrypto) + quotient_add_test(NAME testkeyverification) +endif() diff --git a/autotests/adjust-config.sh b/autotests/adjust-config.sh new file mode 100644 index 00000000..68ea58ab --- /dev/null +++ b/autotests/adjust-config.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +CMD="" + +$CMD perl -pi -w -e \ + 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g;' homeserver.yaml +$CMD perl -pi -w -e \ + 's/rc_message_burst_count.*/rc_message_burst_count: 10000/g;' homeserver.yaml + +( +cat <<HEREDOC +rc_message: + per_second: 10000 + burst_count: 100000 +rc_registration: + per_second: 10000 + burst_count: 30000 +rc_login: + address: + per_second: 10000 + burst_count: 30000 + account: + per_second: 10000 + burst_count: 30000 + failed_attempts: + per_second: 10000 + burst_count: 30000 +rc_admin_redaction: + per_second: 1000 + burst_count: 5000 +rc_joins: + local: + per_second: 10000 + burst_count: 100000 + remote: + per_second: 10000 + burst_count: 100000 +HEREDOC +) | $CMD tee -a homeserver.yaml + +$CMD perl -pi -w -e \ + 's/^#enable_registration: false/enable_registration: true/g;' homeserver.yaml +$CMD perl -pi -w -e \ + 's/^#enable_registration_without_verification: .+/enable_registration_without_verification: true/g;' homeserver.yaml +$CMD perl -pi -w -e \ + 's/tls: false/tls: true/g;' homeserver.yaml +$CMD perl -pi -w -e \ + 's/#tls_certificate_path:/tls_certificate_path:/g;' homeserver.yaml +$CMD perl -pi -w -e \ + 's/#tls_private_key_path:/tls_private_key_path:/g;' homeserver.yaml + +$CMD openssl req -x509 -newkey rsa:4096 -keyout localhost.tls.key -out localhost.tls.crt -days 365 -subj '/CN=localhost' -nodes + +$CMD chmod 0777 localhost.tls.crt +$CMD chmod 0777 localhost.tls.key diff --git a/autotests/callcandidateseventtest.cpp b/autotests/callcandidateseventtest.cpp new file mode 100644 index 00000000..257e0ef2 --- /dev/null +++ b/autotests/callcandidateseventtest.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "events/callevents.h" + +#include <QtTest/QtTest> + +class TestCallCandidatesEvent : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void fromJson(); +}; + +void TestCallCandidatesEvent::fromJson() +{ + auto document = QJsonDocument::fromJson(R"({ + "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" + })"); + + QVERIFY(document.isObject()); + + auto object = document.object(); + + using namespace Quotient; + const auto& callCandidatesEvent = loadEvent<CallCandidatesEvent>(object); + QVERIFY(callCandidatesEvent); + QVERIFY(callCandidatesEvent->is<CallCandidatesEvent>()); + + QCOMPARE(callCandidatesEvent->version(), 0); + QCOMPARE(callCandidatesEvent->callId(), QStringLiteral("12345")); + QCOMPARE(callCandidatesEvent->candidates().count(), 1); + + const auto& candidate = callCandidatesEvent->candidates().at(0).toObject(); + QCOMPARE(candidate.value("sdpMid").toString(), QStringLiteral("audio")); + QCOMPARE(candidate.value("sdpMLineIndex").toInt(), 0); + QCOMPARE(candidate.value("candidate").toString(), + QStringLiteral("candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0")); +} + +QTEST_APPLESS_MAIN(TestCallCandidatesEvent) +#include "callcandidateseventtest.moc" diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh new file mode 100755 index 00000000..e7a228ef --- /dev/null +++ b/autotests/run-tests.sh @@ -0,0 +1,32 @@ +mkdir -p data +chmod 0777 data + +SYNAPSE_IMAGE='matrixdotorg/synapse:v1.61.1' + +rm ~/.local/share/testolmaccount -rf +docker run -v `pwd`/data:/data --rm \ + -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no $SYNAPSE_IMAGE generate +(cd data && . ../autotests/adjust-config.sh) +docker run -d \ + --name synapse \ + -p 1234:8008 \ + -p 8448:8008 \ + -p 8008:8008 \ + -v `pwd`/data:/data $SYNAPSE_IMAGE +trap "rm -rf ./data/*; docker rm -f synapse 2>&1 >/dev/null; trap - EXIT" EXIT + +echo Waiting for synapse to start... +until curl -s -f -k https://localhost:1234/_matrix/client/versions; do echo "Checking ..."; sleep 2; done +echo Register alice +for i in 1 2 3 4 5 6 7 8 9; do + docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u alice$i -p secret -c /data/homeserver.yaml https://localhost:8008" +done +echo Register bob +for i in 1 2 3; do + docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u bob$i -p secret -c /data/homeserver.yaml https://localhost:8008" +done +echo Register carl +docker exec synapse /bin/sh -c "register_new_matrix_user --admin -u carl -p secret -c /data/homeserver.yaml https://localhost:8008" + +GTEST_COLOR=1 ctest --verbose "$@" + diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp new file mode 100644 index 00000000..29521060 --- /dev/null +++ b/autotests/testfilecrypto.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testfilecrypto.h" + +#include "events/filesourceinfo.h" + +#include <qtest.h> + +using namespace Quotient; +void TestFileCrypto::encryptDecryptData() +{ + QByteArray data = "ABCDEF"; + auto [file, cipherText] = encryptFile(data); + auto decrypted = decryptFile(cipherText, file); + // AES CTR produces ciphertext of the same size as the original + QCOMPARE(cipherText.size(), data.size()); + QCOMPARE(decrypted.size(), data.size()); + QCOMPARE(decrypted, data); +} +QTEST_APPLESS_MAIN(TestFileCrypto) diff --git a/autotests/testfilecrypto.h b/autotests/testfilecrypto.h new file mode 100644 index 00000000..9096a8c7 --- /dev/null +++ b/autotests/testfilecrypto.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include <QtTest/QtTest> + +class TestFileCrypto : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void encryptDecryptData(); +}; diff --git a/autotests/testgroupsession.cpp b/autotests/testgroupsession.cpp new file mode 100644 index 00000000..1054a160 --- /dev/null +++ b/autotests/testgroupsession.cpp @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testgroupsession.h" +#include "e2ee/qolminboundsession.h" +#include "e2ee/qolmoutboundsession.h" +#include "e2ee/qolmutils.h" + +using namespace Quotient; + +void TestGroupSession::groupSessionPicklingValid() +{ + auto ogs = QOlmOutboundGroupSession::create(); + const auto ogsId = ogs->sessionId(); + QVERIFY(QByteArray::fromBase64(ogsId).size() > 0); + QCOMPARE(0, ogs->sessionMessageIndex()); + + auto&& ogsPickled = ogs->pickle(Unencrypted {}); + auto ogs2 = + QOlmOutboundGroupSession::unpickle(std::move(ogsPickled), Unencrypted{}) + .value(); + QCOMPARE(ogsId, ogs2->sessionId()); + + auto igs = QOlmInboundGroupSession::create(ogs->sessionKey()).value(); + const auto igsId = igs->sessionId(); + // ID is valid base64? + QVERIFY(QByteArray::fromBase64(igsId).size() > 0); + + //// no messages have been sent yet + QCOMPARE(0, igs->firstKnownIndex()); + + auto igsPickled = igs->pickle(Unencrypted {}); + igs = QOlmInboundGroupSession::unpickle(std::move(igsPickled), Unencrypted{}) + .value(); + QCOMPARE(igsId, igs->sessionId()); +} + +void TestGroupSession::groupSessionCryptoValid() +{ + auto ogs = QOlmOutboundGroupSession::create(); + auto igs = QOlmInboundGroupSession::create(ogs->sessionKey()).value(); + QCOMPARE(ogs->sessionId(), igs->sessionId()); + + const auto plainText = "Hello world!"; + const auto ciphertext = ogs->encrypt(plainText); + // ciphertext valid base64? + QVERIFY(QByteArray::fromBase64(ciphertext).size() > 0); + + const auto decryptionResult = igs->decrypt(ciphertext).value(); + + //// correct plaintext? + QCOMPARE(plainText, decryptionResult.first); + + QCOMPARE(0, decryptionResult.second); +} +QTEST_GUILESS_MAIN(TestGroupSession) diff --git a/autotests/testgroupsession.h b/autotests/testgroupsession.h new file mode 100644 index 00000000..6edf0d16 --- /dev/null +++ b/autotests/testgroupsession.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include <QTest> + +class TestGroupSession : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void groupSessionPicklingValid(); + void groupSessionCryptoValid(); +}; diff --git a/autotests/testkeyverification.cpp b/autotests/testkeyverification.cpp new file mode 100644 index 00000000..1fa6d8c6 --- /dev/null +++ b/autotests/testkeyverification.cpp @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#include <QTest> +#include "testutils.h" +#include <qt_connection_util.h> + +class TestKeyVerificationSession : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testVerification() + { + CREATE_CONNECTION(a, "alice1", "secret", "AliceDesktop") + CREATE_CONNECTION(b, "alice1", "secret", "AlicePhone") + + QPointer<KeyVerificationSession> aSession{}; + connect(a.get(), &Connection::newKeyVerificationSession, this, [&](KeyVerificationSession* session) { + aSession = session; + QVERIFY(session->remoteDeviceId() == b->deviceId()); + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORREADY); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::ACCEPTED || session->state() == KeyVerificationSession::READY); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); + session->sendMac(); + }); + }); + }); + a->startKeyVerificationSession(b->deviceId()); + connect(b.get(), &Connection::newKeyVerificationSession, this, [=](KeyVerificationSession* session) { + QVERIFY(session->remoteDeviceId() == a->deviceId()); + QVERIFY(session->state() == KeyVerificationSession::INCOMING); + session->sendReady(); + // KeyVerificationSession::READY is skipped because we have only one method + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORACCEPT); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=](){ + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORKEY || session->state() == KeyVerificationSession::ACCEPTED); + connectSingleShot(session, &KeyVerificationSession::stateChanged, this, [=]() { + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORVERIFICATION); + QVERIFY(aSession); + QVERIFY(aSession->sasEmojis() == session->sasEmojis()); + session->sendMac(); + QVERIFY(session->state() == KeyVerificationSession::WAITINGFORMAC); + }); + }); + + }); + b->syncLoop(); + a->syncLoop(); + QSignalSpy spy(aSession, &KeyVerificationSession::finished); + spy.wait(10000); + } +}; +QTEST_GUILESS_MAIN(TestKeyVerificationSession) +#include "testkeyverification.moc" diff --git a/autotests/testolmaccount.cpp b/autotests/testolmaccount.cpp new file mode 100644 index 00000000..53a0c955 --- /dev/null +++ b/autotests/testolmaccount.cpp @@ -0,0 +1,479 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-FileCopyrightText: 2020 mtxclient developers +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testolmaccount.h" + +#include <connection.h> +#include <csapi/joining.h> +#include <e2ee/qolmaccount.h> +#include <e2ee/qolmutility.h> +#include <events/encryptionevent.h> +#include <events/filesourceinfo.h> +#include <networkaccessmanager.h> +#include <room.h> + +#include "testutils.h" + +using namespace Quotient; + +void TestOlmAccount::pickleUnpickledTest() +{ + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); + auto identityKeys = olmAccount.identityKeys(); + auto pickled = olmAccount.pickle(Unencrypted{}); + QOlmAccount olmAccount2(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + auto unpickleResult = olmAccount2.unpickle(std::move(pickled), + Unencrypted{}); + QCOMPARE(unpickleResult, 0); + auto identityKeys2 = olmAccount2.identityKeys(); + QCOMPARE(identityKeys.curve25519, identityKeys2.curve25519); + QCOMPARE(identityKeys.ed25519, identityKeys2.ed25519); +} + +void TestOlmAccount::identityKeysValid() +{ + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); + const auto identityKeys = olmAccount.identityKeys(); + const auto curve25519 = identityKeys.curve25519; + const auto ed25519 = identityKeys.ed25519; + // verify encoded keys length + QCOMPARE(curve25519.size(), 43); + QCOMPARE(ed25519.size(), 43); + + // encoded as valid base64? + QVERIFY(QByteArray::fromBase64(curve25519).size() > 0); + QVERIFY(QByteArray::fromBase64(ed25519).size() > 0); +} + +void TestOlmAccount::signatureValid() +{ + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); + const auto message = "Hello world!"; + const auto signature = olmAccount.sign(message); + QVERIFY(QByteArray::fromBase64(signature).size() > 0); + + QOlmUtility utility; + const auto identityKeys = olmAccount.identityKeys(); + const auto ed25519Key = identityKeys.ed25519; + QVERIFY(utility.ed25519Verify(ed25519Key, message, signature)); +} + +void TestOlmAccount::oneTimeKeysValid() +{ + QOlmAccount olmAccount(QStringLiteral("@foo:bar.com"), QStringLiteral("QuotientTestDevice")); + olmAccount.createNewAccount(); + const auto maxNumberOfOneTimeKeys = olmAccount.maxNumberOfOneTimeKeys(); + QCOMPARE(100, maxNumberOfOneTimeKeys); + + const auto oneTimeKeysEmpty = olmAccount.oneTimeKeys(); + QVERIFY(oneTimeKeysEmpty.curve25519().isEmpty()); + + olmAccount.generateOneTimeKeys(20); + const auto oneTimeKeysFilled = olmAccount.oneTimeKeys(); + QCOMPARE(20, oneTimeKeysFilled.curve25519().count()); +} + +void TestOlmAccount::deviceKeys() +{ + // copied from mtxclient + DeviceKeys device1; + device1.userId = "@alice:example.com"; + device1.deviceId = "JLAFKJWSCS"; + device1.keys = {{"curve25519:JLAFKJWSCS", "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI"}, + {"ed25519:JLAFKJWSCS", "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"}}; + + // TODO that should be the default value + device1.algorithms = + QStringList { OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey }; + + device1.signatures = { + {"@alice:example.com", + {{"ed25519:JLAFKJWSCS", + "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/" + "a+myXS367WT6NAIcBA"}}}}; + + QJsonObject j; + JsonObjectConverter<DeviceKeys>::dumpTo(j, device1); + QJsonDocument doc(j); + QCOMPARE(doc.toJson(QJsonDocument::Compact), "{\"algorithms\":[\"m.olm.v1.curve25519-aes-sha2\",\"m.megolm.v1.aes-sha2\"]," + "\"device_id\":\"JLAFKJWSCS\",\"keys\":{\"curve25519:JLAFKJWSCS\":" + "\"3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI\",\"ed25519:JLAFKJWSCS\":" + "\"lEuiRJBit0IG6nUf5pUzWTUEsRVVe/" + "HJkoKuEww9ULI\"},\"signatures\":{\"@alice:example.com\":{\"ed25519:JLAFKJWSCS\":" + "\"dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/" + "a+myXS367WT6NAIcBA\"}},\"user_id\":\"@alice:example.com\"}"); + + auto doc2 = QJsonDocument::fromJson(R"({ + "user_id": "@alice:example.com", + "device_id": "JLAFKJWSCS", + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "keys": { + "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI", + "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI" + }, + "signatures": { + "@alice:example.com": { + "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA" + } + }, + "unsigned": { + "device_display_name": "Alice's mobile phone" + } + })"); + + DeviceKeys device2; + JsonObjectConverter<DeviceKeys>::fillFrom(doc2.object(), device2); + + QCOMPARE(device2.userId, device1.userId); + QCOMPARE(device2.deviceId, device1.deviceId); + QCOMPARE(device2.keys, device1.keys); + QCOMPARE(device2.algorithms, device1.algorithms); + QCOMPARE(device2.signatures, device1.signatures); + + // UnsignedDeviceInfo is missing from the generated DeviceKeys object :( + // QCOMPARE(device2.unsignedInfo.deviceDisplayName, "Alice's mobile phone"); +} + +void TestOlmAccount::encryptedFile() +{ + auto doc = QJsonDocument::fromJson(R"({ + "url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe", + "v": "v2", + "key": { + "alg": "A256CTR", + "ext": true, + "k": "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0", + "key_ops": ["encrypt","decrypt"], + "kty": "oct" + }, + "iv": "w+sE15fzSc0AAAAAAAAAAA", + "hashes": { + "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA" + }})"); + + const auto file = fromJson<EncryptedFileMetadata>(doc); + + QCOMPARE(file.v, "v2"); + QCOMPARE(file.iv, "w+sE15fzSc0AAAAAAAAAAA"); + QCOMPARE(file.hashes["sha256"], "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA"); + QCOMPARE(file.key.alg, "A256CTR"); + QCOMPARE(file.key.ext, true); + QCOMPARE(file.key.k, "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0"); + QCOMPARE(file.key.keyOps.count(), 2); + QCOMPARE(file.key.kty, "oct"); +} + +void TestOlmAccount::uploadIdentityKey() +{ + CREATE_CONNECTION(conn, "alice1", "secret", "AlicePhone") + + auto olmAccount = conn->olmAccount(); + auto idKeys = olmAccount->identityKeys(); + + QVERIFY(idKeys.curve25519.size() > 10); + + UnsignedOneTimeKeys unused; + auto request = olmAccount->createUploadKeyRequest(unused); + connect(request, &BaseJob::result, this, [request, conn] { + if (!request->status().good()) + QFAIL("upload failed"); + const auto& oneTimeKeyCounts = request->oneTimeKeyCounts(); + // Allow the response to have entries with zero counts + QCOMPARE(std::accumulate(oneTimeKeyCounts.begin(), + oneTimeKeyCounts.end(), 0), + 0); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); +} + +void TestOlmAccount::uploadOneTimeKeys() +{ + CREATE_CONNECTION(conn, "alice2", "secret", "AlicePhone") + auto olmAccount = conn->olmAccount(); + + auto nKeys = olmAccount->generateOneTimeKeys(5); + QCOMPARE(nKeys, 5); + + auto oneTimeKeys = olmAccount->oneTimeKeys(); + + OneTimeKeys oneTimeKeysHash; + const auto curve = oneTimeKeys.curve25519(); + for (const auto &[keyId, key] : asKeyValueRange(curve)) { + oneTimeKeysHash["curve25519:"+keyId] = key; + } + auto request = new UploadKeysJob(none, oneTimeKeysHash); + connect(request, &BaseJob::result, this, [request, conn] { + if (!request->status().good()) + QFAIL("upload failed"); + QCOMPARE(request->oneTimeKeyCounts().value(Curve25519Key), 5); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); +} + +void TestOlmAccount::uploadSignedOneTimeKeys() +{ + CREATE_CONNECTION(conn, "alice3", "secret", "AlicePhone") + auto olmAccount = conn->olmAccount(); + auto nKeys = olmAccount->generateOneTimeKeys(5); + QCOMPARE(nKeys, 5); + + auto oneTimeKeys = olmAccount->oneTimeKeys(); + OneTimeKeys oneTimeKeysHash; + const auto signedKey = olmAccount->signOneTimeKeys(oneTimeKeys); + for (const auto &[keyId, key] : asKeyValueRange(signedKey)) { + oneTimeKeysHash[keyId] = key; + } + auto request = new UploadKeysJob(none, oneTimeKeysHash); + connect(request, &BaseJob::result, this, [request, nKeys, conn] { + if (!request->status().good()) + QFAIL("upload failed"); + QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), nKeys); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); +} + +void TestOlmAccount::uploadKeys() +{ + CREATE_CONNECTION(conn, "alice4", "secret", "AlicePhone") + auto olmAccount = conn->olmAccount(); + auto idks = olmAccount->identityKeys(); + olmAccount->generateOneTimeKeys(1); + auto otks = olmAccount->oneTimeKeys(); + auto request = olmAccount->createUploadKeyRequest(otks); + connect(request, &BaseJob::result, this, [request, conn] { + if (!request->status().good()) + QFAIL("upload failed"); + QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1); + }); + conn->run(request); + QSignalSpy spy3(request, &BaseJob::result); + QVERIFY(spy3.wait(10000)); +} + +void TestOlmAccount::queryTest() +{ + CREATE_CONNECTION(alice, "alice5", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob1", "secret", "BobPhone") + + // Create and upload keys for both users. + auto aliceOlm = alice->olmAccount(); + aliceOlm->generateOneTimeKeys(1); + auto aliceRes = aliceOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); + connect(aliceRes, &BaseJob::result, this, [aliceRes] { + QCOMPARE(aliceRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1); + }); + QSignalSpy spy(aliceRes, &BaseJob::result); + alice->run(aliceRes); + QVERIFY(spy.wait(10000)); + + auto bobOlm = bob->olmAccount(); + bobOlm->generateOneTimeKeys(1); + auto bobRes = bobOlm->createUploadKeyRequest(aliceOlm->oneTimeKeys()); + connect(bobRes, &BaseJob::result, this, [bobRes] { + QCOMPARE(bobRes->oneTimeKeyCounts().value(SignedCurve25519Key), 1); + }); + QSignalSpy spy1(bobRes, &BaseJob::result); + bob->run(bobRes); + QVERIFY(spy1.wait(10000)); + + { + // Each user is requests each other's keys. + QHash<QString, QStringList> deviceKeys; + deviceKeys[bob->userId()] = QStringList(); + auto job = alice->callApi<QueryKeysJob>(deviceKeys); + QSignalSpy spy(job, &BaseJob::result); + connect(job, &BaseJob::result, this, [job, bob, bobOlm] { + QCOMPARE(job->failures().size(), 0); + + const auto& aliceDevices = job->deviceKeys().value(bob->userId()); + QVERIFY(!aliceDevices.empty()); + + const auto& devKeys = aliceDevices.value(bob->deviceId()); + QCOMPARE(devKeys.userId, bob->userId()); + QCOMPARE(devKeys.deviceId, bob->deviceId()); + QCOMPARE(devKeys.keys, bobOlm->deviceKeys().keys); + QCOMPARE(devKeys.signatures, bobOlm->deviceKeys().signatures); + }); + QVERIFY(spy.wait(10000)); + } + + { + QHash<QString, QStringList> deviceKeys; + deviceKeys[alice->userId()] = QStringList(); + auto job = bob->callApi<QueryKeysJob>(deviceKeys); + QSignalSpy spy(job, &BaseJob::result); + connect(job, &BaseJob::result, this, [job, alice, aliceOlm] { + QCOMPARE(job->failures().size(), 0); + + const auto& bobDevices = job->deviceKeys().value(alice->userId()); + QVERIFY(!bobDevices.empty()); + + auto devKeys = bobDevices[alice->deviceId()]; + QCOMPARE(devKeys.userId, alice->userId()); + QCOMPARE(devKeys.deviceId, alice->deviceId()); + QCOMPARE(devKeys.keys, aliceOlm->deviceKeys().keys); + QCOMPARE(devKeys.signatures, aliceOlm->deviceKeys().signatures); + }); + QVERIFY(spy.wait(10000)); + } +} + +void TestOlmAccount::claimKeys() +{ + CREATE_CONNECTION(alice, "alice6", "secret", "AlicePhone") + CREATE_CONNECTION(bob, "bob2", "secret", "BobPhone") + + // Bob uploads his keys. + auto *bobOlm = bob->olmAccount(); + bobOlm->generateOneTimeKeys(1); + auto request = bobOlm->createUploadKeyRequest(bobOlm->oneTimeKeys()); + + connect(request, &BaseJob::result, this, [request, bob] { + QCOMPARE(request->oneTimeKeyCounts().value(SignedCurve25519Key), 1); + }); + bob->run(request); + + QSignalSpy requestSpy(request, &BaseJob::result); + QVERIFY(requestSpy.wait(10000)); + + // Alice retrieves bob's keys & claims one signed one-time key. + QHash<QString, QStringList> deviceKeys; + deviceKeys[bob->userId()] = QStringList(); + auto queryKeysJob = alice->callApi<QueryKeysJob>(deviceKeys); + QSignalSpy requestSpy2(queryKeysJob, &BaseJob::result); + QVERIFY(requestSpy2.wait(10000)); + + const auto& bobDevices = queryKeysJob->deviceKeys().value(bob->userId()); + QVERIFY(!bobDevices.empty()); + + const auto currentDevice = bobDevices[bob->deviceId()]; + + // Verify signature. + QVERIFY(verifyIdentitySignature(currentDevice, bob->deviceId(), + bob->userId())); + // Retrieve the identity key for the current device. + const auto& bobEd25519 = + bobDevices.value(bob->deviceId()).keys["ed25519:" + bob->deviceId()]; + + QHash<QString, QHash<QString, QString>> oneTimeKeys; + oneTimeKeys[bob->userId()] = QHash<QString, QString>(); + oneTimeKeys[bob->userId()][bob->deviceId()] = SignedCurve25519Key; + + auto claimKeysJob = alice->callApi<ClaimKeysJob>(oneTimeKeys); + connect(claimKeysJob, &BaseJob::result, this, [bob, bobEd25519, claimKeysJob] { + const auto userId = bob->userId(); + const auto deviceId = bob->deviceId(); + + // The device exists. + QCOMPARE(claimKeysJob->oneTimeKeys().size(), 1); + QCOMPARE(claimKeysJob->oneTimeKeys().value(userId).size(), 1); + + // The key is the one bob sent. + const auto& oneTimeKeys = + claimKeysJob->oneTimeKeys().value(userId).value(deviceId); + for (auto it = oneTimeKeys.begin(); it != oneTimeKeys.end(); ++it) { + if (it.key().startsWith(SignedCurve25519Key) + && std::holds_alternative<SignedOneTimeKey>(it.value())) + return; + } + QFAIL("The claimed one time key is not in /claim response"); + }); + QSignalSpy completionSpy(claimKeysJob, &BaseJob::result); + QVERIFY(completionSpy.wait(10000)); +} + +void TestOlmAccount::claimMultipleKeys() +{ + // Login with alice multiple times + CREATE_CONNECTION(alice, "alice7", "secret", "AlicePhone") + CREATE_CONNECTION(alice1, "alice7", "secret", "AlicePhone") + CREATE_CONNECTION(alice2, "alice7", "secret", "AlicePhone") + + auto olm = alice->olmAccount(); + olm->generateOneTimeKeys(10); + auto res = olm->createUploadKeyRequest(olm->oneTimeKeys()); + QSignalSpy spy(res, &BaseJob::result); + connect(res, &BaseJob::result, this, [res] { + QCOMPARE(res->oneTimeKeyCounts().value(SignedCurve25519Key), 10); + }); + alice->run(res); + QVERIFY(spy.wait(10000)); + + auto olm1 = alice1->olmAccount(); + olm1->generateOneTimeKeys(10); + auto res1 = olm1->createUploadKeyRequest(olm1->oneTimeKeys()); + QSignalSpy spy1(res1, &BaseJob::result); + connect(res1, &BaseJob::result, this, [res1] { + QCOMPARE(res1->oneTimeKeyCounts().value(SignedCurve25519Key), 10); + }); + alice1->run(res1); + QVERIFY(spy1.wait(10000)); + + auto olm2 = alice2->olmAccount(); + olm2->generateOneTimeKeys(10); + auto res2 = olm2->createUploadKeyRequest(olm2->oneTimeKeys()); + QSignalSpy spy2(res2, &BaseJob::result); + connect(res2, &BaseJob::result, this, [res2] { + QCOMPARE(res2->oneTimeKeyCounts().value(SignedCurve25519Key), 10); + }); + alice2->run(res2); + QVERIFY(spy2.wait(10000)); + + // Bob will claim all keys from alice + CREATE_CONNECTION(bob, "bob3", "secret", "BobPhone") + + QStringList devices_; + devices_ << alice->deviceId() + << alice1->deviceId() + << alice2->deviceId(); + + QHash<QString, QHash<QString, QString>> oneTimeKeys; + oneTimeKeys[alice->userId()] = QHash<QString, QString>(); + for (const auto &d : devices_) { + oneTimeKeys[alice->userId()][d] = SignedCurve25519Key; + } + auto job = bob->callApi<ClaimKeysJob>(oneTimeKeys); + QSignalSpy jobSpy(job, &BaseJob::finished); + QVERIFY(jobSpy.wait(10000)); + const auto userId = alice->userId(); + + QCOMPARE(job->oneTimeKeys().value(userId).size(), 3); +} + +void TestOlmAccount::enableEncryption() +{ + CREATE_CONNECTION(alice, "alice9", "secret", "AlicePhone") + + auto job = alice->createRoom(Connection::PublishRoom, {}, {}, {}, {}); + QSignalSpy createRoomSpy(job, &BaseJob::success); + QVERIFY(createRoomSpy.wait(10000)); + alice->sync(); + connect(alice.get(), &Connection::syncDone, this, [alice](){ + qDebug() << "foo"; + alice->sync(); + }); + while(alice->roomsCount(JoinState::Join) == 0) { + QThread::sleep(100); + } + auto room = alice->rooms(JoinState::Join)[0]; + room->activateEncryption(); + QSignalSpy encryptionSpy(room, &Room::encryption); + QVERIFY(encryptionSpy.wait(10000)); + QVERIFY(room->usesEncryption()); +} + +QTEST_GUILESS_MAIN(TestOlmAccount) diff --git a/autotests/testolmaccount.h b/autotests/testolmaccount.h new file mode 100644 index 00000000..367092f6 --- /dev/null +++ b/autotests/testolmaccount.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include <QtTest/QtTest> +#include <QString> + +namespace Quotient { + class Connection; +} + +class TestOlmAccount : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void pickleUnpickledTest(); + void identityKeysValid(); + void signatureValid(); + void oneTimeKeysValid(); + //void removeOneTimeKeys(); + void deviceKeys(); + void encryptedFile(); + void uploadIdentityKey(); + void uploadOneTimeKeys(); + void uploadSignedOneTimeKeys(); + void uploadKeys(); + void queryTest(); + void claimKeys(); + void claimMultipleKeys(); + void enableEncryption(); +}; diff --git a/autotests/testolmsession.cpp b/autotests/testolmsession.cpp new file mode 100644 index 00000000..18b0d5f2 --- /dev/null +++ b/autotests/testolmsession.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmsession.h" +#include "testolmsession.h" + +using namespace Quotient; + +std::pair<QOlmSessionPtr, QOlmSessionPtr> createSessionPair() +{ + QByteArray pickledAccountA("eOBXIKivUT6YYowRH031BNv7zNmzqM5B7CpXdyeaPvala5mt7/OeqrG1qVA7vA1SYloFyvJPIy0QNkD3j1HiPl5vtZHN53rtfZ9exXDok03zjmssqn4IJsqcA7Fbo1FZeKafG0NFcWwCPTdmcV7REqxjqGm3I4K8MQFa45AdTGSUu2C12cWeOcbSMlcINiMral+Uyah1sgPmLJ18h1qcnskXUXQvpffZ5DiUw1Iz5zxnwOQF1GVyowPJD7Zdugvj75RQnDxAn6CzyvrY2k2CuedwqDC3fIXM2xdUNWttW4nC2g4InpBhCVvNwhZYxlUb5BUEjmPI2AB3dAL5ry6o9MFncmbN6x5x"); + QByteArray pickledAccountB("eModTvoFi9oOIkax4j4nuxw9Tcl/J8mOmUctUWI68Q89HSaaPTqR+tdlKQ85v2GOs5NlZCp7EuycypN9GQ4fFbHUCrS7nspa3GFBWsR8PnM8+wez5PWmfFZLg3drOvT0jbMjpDx0MjGYClHBqcrEpKx9oFaIRGBaX6HXzT4lRaWSJkXxuX92q8iGNrLn96PuAWFNcD+2JXpPcNFntslwLUNgqzpZ04aIFYwL80GmzyOgq3Bz1GO6u3TgCQEAmTIYN2QkO0MQeuSfe7UoMumhlAJ6R8GPcdSSPtmXNk4tdyzzlgpVq1hm7ZLKto+g8/5Aq3PvnvA8wCqno2+Pi1duK1pZFTIlActr"); + auto accountA = QOlmAccount(u"accountA:foo.com", u"Device1UserA"); + if (accountA.unpickle(std::move(pickledAccountA), Unencrypted{}) + != OLM_SUCCESS) + qFatal("Failed to unpickle account A: %s", accountA.lastError()); + + auto accountB = QOlmAccount(u"accountB:foo.com", u"Device1UserB"); + if (accountB.unpickle(std::move(pickledAccountB), Unencrypted{}) + != OLM_SUCCESS) + qFatal("Failed to unpickle account B: %s", accountB.lastError()); + + + const QByteArray identityKeyA("qIEr3TWcJQt4CP8QoKKJcCaukByIOpgh6erBkhLEa2o"); + const QByteArray oneTimeKeyA("WzsbsjD85iB1R32iWxfJdwkgmdz29ClMbJSJziECYwk"); + const QByteArray identityKeyB("q/YhJtog/5VHCAS9rM9uUf6AaFk1yPe4GYuyUOXyQCg"); + const QByteArray oneTimeKeyB("oWvzryma+B2onYjo3hM6A3Mgo/Yepm8HvgSvwZMTnjQ"); + auto outbound = + accountA.createOutboundSession(identityKeyB, oneTimeKeyB).value(); + + const auto preKey = outbound->encrypt(""); // Payload does not matter for PreKey + + if (preKey.type() != QOlmMessage::PreKey) { + // We can't call QFail here because it's an helper function returning a value + throw "Wrong first message type received, can't create session"; + } + auto inbound = accountB.createInboundSession(preKey).value(); + return { std::move(inbound), std::move(outbound) }; +} + +void TestOlmSession::olmOutboundSessionCreation() +{ + const auto [_, outboundSession] = createSessionPair(); + QCOMPARE(0, outboundSession->hasReceivedMessage()); +} + +void TestOlmSession::olmEncryptDecrypt() +{ + const auto [inboundSession, outboundSession] = createSessionPair(); + const auto encrypted = outboundSession->encrypt("Hello world!"); + if (encrypted.type() == QOlmMessage::PreKey) { + QOlmMessage m(encrypted); // clone + QVERIFY(inboundSession->matchesInboundSession(m)); + } + + const auto decrypted = inboundSession->decrypt(encrypted).value(); + + QCOMPARE(decrypted, "Hello world!"); +} + +void TestOlmSession::correctSessionOrdering() +{ + // n0W5IJ2ZmaI9FxKRj/wohUQ6WEU0SfoKsgKKHsr4VbM + auto session1 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGvlaV6t/0ihD2/0QGckDIvbmE1aV+PxB0zUtHXh99bI/60N+PWkCLA84jEY4sz3d45ui/TVoFGLDHlymKxvlj7XngXrbtlxSkVntsPzDiNpKEXCa26N2ubKpQ0fbjrV5gbBTYWfU04DXHPXFDTksxpNALYt/h0eVMVhf6hB0ZzpLBsOG0mpwkLufwub0CuDEDGGmRddz3TcNCLq5NnI8R9udDWvHAkTS1UTbHuIf/y6cZg875nJyXpAvd8/XhL8TOo8ot2sE1fElBa4vrH/m9rBQMC1GPkhLBIizmY44C+Sq9PQRnF+uCZ", Unencrypted{}).value(); + // +9pHJhP3K4E5/2m8PYBPLh8pS9CJodwUOh8yz3mnmw0 + auto session2 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UFD+q37/WlfTAzQsSjCdD07FcErZ4siEy5vpiB+pyO8i53ptZvb2qRvqNKFzPaXuu33PS2PBTmmnR+kJt+DgDNqWadyaj/WqEAejc7ALqSs5GuhbZtpoLe+lRSRK0rwVX3gzz4qrl8pm0pD5pSZAUWRXDRlieGWMclz68VUvnSaQH7ElTo4S634CJk+xQfFFCD26v0yONPSN6rwouS1cWPuG5jTlnV8vCFVTU2+lduKh54Ko6FUJ/ei4xR8Nk2duBGSc/TdllX9e2lDYHSUkWoD4ti5xsFioB8Blus7JK9BZfcmRmdlxIOD", Unencrypted {}).value(); + // MC7n8hX1l7WlC2/WJGHZinMocgiBZa4vwGAOredb/ME + auto session3 = QOlmSession::unpickle("7g5cfQRsDk2ROXf9S01n2leZiFRon+EbvXcMOADU0UGNk2TmVDJ95K0Nywf24FNklNVtXtFDiFPHFwNSmCbHNCp3hsGtZlt0AHUkMmL48XklLqzwtVk5/v2RRmSKR5LqYdIakrtuK/fY0ENhBZIbI1sRetaJ2KMbY9l6rCJNfFg8VhpZ4KTVvEZVuP9g/eZkCnP5NxzXiBRF6nfY3O/zhcKxa3acIqs6BMhyLsfuJ80t+hQ1HvVyuhBerGujdSDzV9tJ9SPidOwfYATk81LVF9hTmnI0KaZa7qCtFzhG0dU/Z3hIWH9HOaw1aSB/IPmughbwdJOwERyhuo3YHoznlQnJ7X252BlI", Unencrypted{}).value(); + + const auto session1Id = session1->sessionId(); + const auto session2Id = session2->sessionId(); + const auto session3Id = session3->sessionId(); + + std::vector<QOlmSessionPtr> sessionList; + sessionList.push_back(std::move(session1)); + sessionList.push_back(std::move(session2)); + sessionList.push_back(std::move(session3)); + + std::sort(sessionList.begin(), sessionList.end()); + QCOMPARE(sessionList[0]->sessionId(), session2Id); + QCOMPARE(sessionList[1]->sessionId(), session3Id); + QCOMPARE(sessionList[2]->sessionId(), session1Id); +} + +QTEST_GUILESS_MAIN(TestOlmSession) diff --git a/autotests/testolmsession.h b/autotests/testolmsession.h new file mode 100644 index 00000000..9a5798fa --- /dev/null +++ b/autotests/testolmsession.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include <QtTest/QtTest> + +class TestOlmSession : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void olmOutboundSessionCreation(); + void olmEncryptDecrypt(); + void correctSessionOrdering(); +}; diff --git a/autotests/testolmutility.cpp b/autotests/testolmutility.cpp new file mode 100644 index 00000000..4de5afdf --- /dev/null +++ b/autotests/testolmutility.cpp @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testolmutility.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutility.h" + +#include <olm/olm.h> + +using namespace Quotient; + +void TestOlmUtility::canonicalJSON() +{ + // Examples taken from + // https://matrix.org/docs/spec/appendices.html#canonical-json + auto data = QJsonDocument::fromJson(QByteArrayLiteral(R"({ + "auth": { + "success": true, + "mxid": "@john.doe:example.com", + "profile": { + "display_name": "John Doe", + "three_pids": [{ + "medium": "email", + "address": "john.doe@example.org" + }, { + "medium": "msisdn", + "address": "123456789" + }] + }}})")); + + QCOMPARE(data.toJson(QJsonDocument::Compact), + "{\"auth\":{\"mxid\":\"@john.doe:example.com\",\"profile\":{\"display_name\":\"John " + "Doe\",\"three_pids\":[{\"address\":\"john.doe@example.org\",\"medium\":\"email\"},{" + "\"address\":\"123456789\",\"medium\":\"msisdn\"}]},\"success\":true}}"); + + auto data0 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"b":"2","a":"1"})")); + QCOMPARE(data0.toJson(QJsonDocument::Compact), "{\"a\":\"1\",\"b\":\"2\"}"); + + auto data1 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "本": 2, "日": 1 })")); + QCOMPARE(data1.toJson(QJsonDocument::Compact), "{\"日\":1,\"本\":2}"); + + auto data2 = QJsonDocument::fromJson(QByteArrayLiteral(R"({"a": "\u65E5"})")); + QCOMPARE(data2.toJson(QJsonDocument::Compact), "{\"a\":\"日\"}"); + + auto data3 = QJsonDocument::fromJson(QByteArrayLiteral(R"({ "a": null })")); + QCOMPARE(data3.toJson(QJsonDocument::Compact), "{\"a\":null}"); +} + +void TestOlmUtility::verifySignedOneTimeKey() +{ + QOlmAccount aliceOlm { u"@alice:matrix.org", u"aliceDevice" }; + aliceOlm.createNewAccount(); + aliceOlm.generateOneTimeKeys(1); + auto keys = aliceOlm.oneTimeKeys(); + + auto firstKey = *keys.curve25519().begin(); + auto msgObj = QJsonObject({{"key", firstKey}}); + auto sig = aliceOlm.sign(msgObj); + + auto msg = QJsonDocument(msgObj).toJson(QJsonDocument::Compact); + + auto utilityBuf = new uint8_t[olm_utility_size()]; + auto utility = olm_utility(utilityBuf); + + + QByteArray signatureBuf1(sig.length(), '\0'); + std::copy(sig.begin(), sig.end(), signatureBuf1.begin()); + + auto res = + olm_ed25519_verify(utility, aliceOlm.identityKeys().ed25519.data(), + aliceOlm.identityKeys().ed25519.size(), msg.data(), + msg.size(), sig.data(), sig.size()); + + QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); + QCOMPARE(res, 0); + + delete[](reinterpret_cast<uint8_t *>(utility)); + + QOlmUtility utility2; + auto res2 = utility2.ed25519Verify(aliceOlm.identityKeys().ed25519, msg, + signatureBuf1); + + //QCOMPARE(std::string(olm_utility_last_error(utility)), "SUCCESS"); + QVERIFY(res2); +} + +void TestOlmUtility::validUploadKeysRequest() +{ + const auto userId = QStringLiteral("@alice:matrix.org"); + const auto deviceId = QStringLiteral("FKALSOCCC"); + + QOlmAccount alice { userId, deviceId }; + alice.createNewAccount(); + alice.generateOneTimeKeys(1); + + auto idSig = alice.signIdentityKeys(); + + QJsonObject body + { + {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}}, + {"user_id", userId}, + {"device_id", deviceId}, + {"keys", + QJsonObject{ + {QStringLiteral("curve25519:") + deviceId, QString::fromUtf8(alice.identityKeys().curve25519)}, + {QStringLiteral("ed25519:") + deviceId, QString::fromUtf8(alice.identityKeys().ed25519)} + } + }, + {"signatures", + QJsonObject{ + {userId, + QJsonObject{ + {"ed25519:" + deviceId, QString::fromUtf8(idSig)} + } + } + } + } + }; + + DeviceKeys deviceKeys = alice.deviceKeys(); + QCOMPARE(QJsonDocument(toJson(deviceKeys)).toJson(QJsonDocument::Compact), + QJsonDocument(body).toJson(QJsonDocument::Compact)); + + QVERIFY(verifyIdentitySignature(fromJson<DeviceKeys>(body), deviceId, userId)); + QVERIFY(verifyIdentitySignature(deviceKeys, deviceId, userId)); +} +QTEST_GUILESS_MAIN(TestOlmUtility) diff --git a/autotests/testolmutility.h b/autotests/testolmutility.h new file mode 100644 index 00000000..f2a3ca45 --- /dev/null +++ b/autotests/testolmutility.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include <QTest> + +class TestOlmUtility : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void canonicalJSON(); + void verifySignedOneTimeKey(); + void validUploadKeysRequest(); +}; diff --git a/autotests/testutils.h b/autotests/testutils.h new file mode 100644 index 00000000..7d016a34 --- /dev/null +++ b/autotests/testutils.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <connection.h> +#include <networkaccessmanager.h> + +#include <QtTest/QSignalSpy> + +using namespace Quotient; + +#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \ + NetworkAccessManager::instance()->ignoreSslErrors(true); \ + auto VAR = std::make_shared<Connection>(); \ + (VAR)->resolveServer("@" USERNAME ":localhost:1234"); \ + connect((VAR).get(), &Connection::loginFlowsChanged, this, [=] { \ + (VAR)->loginWithPassword((USERNAME), SECRET, DEVICE_NAME, ""); \ + }); \ + connect((VAR).get(), &Connection::networkError, [](const QString& error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Network error: make sure synapse is running"); \ + }); \ + connect((VAR).get(), &Connection::loginError, [](const QString& error) { \ + QWARN(qUtf8Printable(error)); \ + QFAIL("Login failed"); \ + }); \ + QSignalSpy spy##VAR((VAR).get(), &Connection::loginFlowsChanged); \ + QSignalSpy spy2##VAR((VAR).get(), &Connection::connected); \ + QVERIFY(spy##VAR.wait(10000)); \ + QVERIFY(spy2##VAR.wait(10000)); diff --git a/autotests/utiltests.cpp b/autotests/utiltests.cpp new file mode 100644 index 00000000..e3ec63d0 --- /dev/null +++ b/autotests/utiltests.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "omittable.h" + +#include <QtTest/QtTest> + +// compile-time Omittable<> tests +using namespace Quotient; + +Omittable<int> testFn(bool) { return 0; } +bool testFn2(int) { return false; } +static_assert( + std::is_same_v<decltype(std::declval<Omittable<bool>>().then(testFn)), + Omittable<int>>); +static_assert( + std::is_same_v< + decltype(std::declval<Omittable<bool>>().then_or(testFn, 0)), int>); +static_assert( + std::is_same_v<decltype(std::declval<Omittable<bool>>().then(testFn)), + Omittable<int>>); +static_assert(std::is_same_v<decltype(std::declval<Omittable<int>>() + .then(testFn2) + .then(testFn)), + Omittable<int>>); +static_assert(std::is_same_v<decltype(std::declval<Omittable<bool>>() + .then(testFn) + .then_or(testFn2, false)), + bool>); + +constexpr auto visitTestFn(int, bool) { return false; } +static_assert( + std::is_same_v<Omittable<bool>, decltype(lift(testFn2, Omittable<int>()))>); +static_assert(std::is_same_v<Omittable<bool>, + decltype(lift(visitTestFn, Omittable<int>(), + Omittable<bool>()))>); + +class TestUtils : public QObject { + Q_OBJECT +private Q_SLOTS: + // TODO +}; + +QTEST_APPLESS_MAIN(TestUtils) +#include "utiltests.moc" diff --git a/cmake/QMatrixClientConfig.cmake b/cmake/QMatrixClientConfig.cmake deleted file mode 100644 index 900038a5..00000000 --- a/cmake/QMatrixClientConfig.cmake +++ /dev/null @@ -1 +0,0 @@ -include("${CMAKE_CURRENT_LIST_DIR}/QMatrixClientTargets.cmake") diff --git a/cmake/QuotientConfig.cmake.in b/cmake/QuotientConfig.cmake.in new file mode 100644 index 00000000..798fa87a --- /dev/null +++ b/cmake/QuotientConfig.cmake.in @@ -0,0 +1,5 @@ +include(CMakeFindDependencyMacro) + +@FIND_DEPS@ + +include("${CMAKE_CURRENT_LIST_DIR}/QuotientTargets.cmake") diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt deleted file mode 100644 index 49e0089a..00000000 --- a/examples/CMakeLists.txt +++ /dev/null @@ -1,69 +0,0 @@ -cmake_minimum_required(VERSION 3.1) - -# This CMakeLists file assumes that the library is installed to CMAKE_INSTALL_PREFIX -# and ignores the in-tree library code. You can use this to start work on your own client. - -project(qmc-example CXX) - -include(CheckCXXCompilerFlag) -if (NOT WIN32) - include(GNUInstallDirs) -endif(NOT WIN32) - -# Find includes in corresponding build directories -set(CMAKE_INCLUDE_CURRENT_DIR ON) -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) - -# Set a default build type if none was specified -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message(STATUS "Setting build type to 'Debug' as none was specified") - set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) - # Set the possible values of build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" - "MinSizeRel" "RelWithDebInfo") -endif() - -if (NOT CMAKE_INSTALL_LIBDIR) - set(CMAKE_INSTALL_LIBDIR ".") -endif() - -if (NOT CMAKE_INSTALL_BINDIR) - set(CMAKE_INSTALL_BINDIR ".") -endif() - -if (NOT CMAKE_INSTALL_INCLUDEDIR) - set(CMAKE_INSTALL_INCLUDEDIR "include") -endif() - -set(CMAKE_CXX_STANDARD 14) - -foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) - CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) - if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${FLAG}") - endif () -endforeach () - -find_package(Qt5 5.6 REQUIRED Network Gui) -get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) - -find_package(QMatrixClient REQUIRED) -get_filename_component(QMC_Prefix "${QMatrixClient_DIR}/../.." ABSOLUTE) - -message( STATUS "qmc-example configuration:" ) -if (CMAKE_BUILD_TYPE) - message( STATUS " Build type: ${CMAKE_BUILD_TYPE}") -endif(CMAKE_BUILD_TYPE) -message( STATUS " Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) -message( STATUS " Qt: ${Qt5_VERSION} at ${Qt5_Prefix}" ) -message( STATUS " QMatrixClient: ${QMatrixClient_VERSION} at ${QMC_Prefix}" ) - -set(example_SRCS qmc-example.cpp) - -add_executable(qmc-example ${example_SRCS}) -target_link_libraries(qmc-example Qt5::Core QMatrixClient) - -# Installation - -install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp deleted file mode 100644 index 9c86d4a9..00000000 --- a/examples/qmc-example.cpp +++ /dev/null @@ -1,347 +0,0 @@ - -#include "connection.h" -#include "room.h" -#include "user.h" -#include "csapi/room_send.h" -#include "csapi/joining.h" -#include "csapi/leaving.h" - -#include <QtCore/QCoreApplication> -#include <QtCore/QStringBuilder> -#include <QtCore/QTimer> -#include <iostream> -#include <functional> - -using namespace QMatrixClient; -using std::cout; -using std::endl; -using namespace std::placeholders; - -class QMCTest : public QObject -{ - public: - QMCTest(Connection* conn, const QString& testRoomName, QString source); - - private slots: - void setup(const QString& testRoomName); - void onNewRoom(Room* r); - void startTests(); - void sendMessage(); - void addAndRemoveTag(); - void sendAndRedact(); - void checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events); - void markDirectChat(); - void checkDirectChatOutcome( - const Connection::DirectChatsMap& added); - void leave(); - void finalize(); - - private: - QScopedPointer<Connection, QScopedPointerDeleteLater> c; - QStringList running; - QStringList succeeded; - QStringList failed; - QString origin; - Room* targetRoom = nullptr; -}; - -#define QMC_CHECK(description, condition) \ -{ \ - const bool result = !!(condition); \ - Q_ASSERT(running.removeOne(description)); \ - (result ? succeeded : failed).push_back(description); \ - cout << (description) << (result ? " successul" : " FAILED") << endl; \ - if (targetRoom) \ - targetRoom->postMessage(origin % ": " % QStringLiteral(description) % \ - (result ? QStringLiteral(" successful") : QStringLiteral(" FAILED")), \ - result ? MessageEventType::Notice : MessageEventType::Text); \ -} - -QMCTest::QMCTest(Connection* conn, const QString& testRoomName, QString source) - : c(conn), origin(std::move(source)) -{ - if (!origin.isEmpty()) - cout << "Origin for the test message: " << origin.toStdString() << endl; - if (!testRoomName.isEmpty()) - cout << "Test room name: " << testRoomName.toStdString() << endl; - - connect(c.data(), &Connection::connected, - this, std::bind(&QMCTest::setup, this, testRoomName)); - connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom); - // Big countdown watchdog - QTimer::singleShot(180000, this, &QMCTest::leave); -} - -void QMCTest::setup(const QString& testRoomName) -{ - cout << "Connected, server: " - << c->homeserver().toDisplayString().toStdString() << endl; - cout << "Access token: " << c->accessToken().toStdString() << endl; - - // Setting up sync loop - c->sync(); - connect(c.data(), &Connection::syncDone, c.data(), [this,testRoomName] { - cout << "Sync complete, " - << running.size() << " tests in the air" << endl; - if (!running.isEmpty()) - c->sync(10000); - else if (targetRoom) - { - targetRoom->postPlainText(origin % ": All tests finished"); - connect(targetRoom, &Room::pendingEventMerged, this, &QMCTest::leave); - } - else - finalize(); - }); - - // Join a testroom, if provided - if (!targetRoom && !testRoomName.isEmpty()) - { - cout << "Joining " << testRoomName.toStdString() << endl; - running.push_back("Join room"); - auto joinJob = c->joinRoom(testRoomName); - connect(joinJob, &BaseJob::failure, this, - [this] { QMC_CHECK("Join room", false); finalize(); }); - // As of BaseJob::success, a Room object is not guaranteed to even - // exist; it's a mere confirmation that the server processed - // the request. - connect(c.data(), &Connection::loadedRoomState, this, - [this,testRoomName] (Room* room) { - Q_ASSERT(room); // It's a grave failure if room is nullptr here - if (room->canonicalAlias() != testRoomName) - return; // Not our room - - targetRoom = room; - QMC_CHECK("Join room", true); - startTests(); - }); - } -} - -void QMCTest::onNewRoom(Room* r) -{ - cout << "New room: " << r->id().toStdString() << endl - << " Name: " << r->name().toStdString() << endl - << " Canonical alias: " << r->canonicalAlias().toStdString() << endl - << endl; - connect(r, &Room::aboutToAddNewMessages, r, [r] (RoomEventsRange timeline) { - cout << timeline.size() << " new event(s) in room " - << r->canonicalAlias().toStdString() << endl; -// for (const auto& item: timeline) -// { -// cout << "From: " -// << r->roomMembername(item->senderId()).toStdString() -// << endl << "Timestamp:" -// << item->timestamp().toString().toStdString() << endl -// << "JSON:" << endl << item->originalJson().toStdString() << endl; -// } - }); -} - -void QMCTest::startTests() -{ - cout << "Starting tests" << endl; - sendMessage(); - addAndRemoveTag(); - sendAndRedact(); - markDirectChat(); -} - -void QMCTest::sendMessage() -{ - running.push_back("Message sending"); - cout << "Sending a message" << endl; - auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); - auto& pending = targetRoom->pendingEvents(); - if (pending.empty()) - { - QMC_CHECK("Message sending", false); - return; - } - auto it = std::find_if(pending.begin(), pending.end(), - [&txnId] (const auto& e) { - return e->transactionId() == txnId; - }); - QMC_CHECK("Message sending", it != pending.end()); - // TODO: Wait when it actually gets sent; check that it obtained an id - // Independently, check when it shows up in the timeline. -} - -void QMCTest::addAndRemoveTag() -{ - running.push_back("Tagging test"); - static const auto TestTag = QStringLiteral("org.qmatrixclient.test"); - // Pre-requisite - if (targetRoom->tags().contains(TestTag)) - targetRoom->removeTag(TestTag); - - // Connect first because the signal is emitted synchronously. - connect(targetRoom, &Room::tagsChanged, targetRoom, [=] { - cout << "Room " << targetRoom->id().toStdString() - << ", tag(s) changed:" << endl - << " " << targetRoom->tagNames().join(", ").toStdString() << endl; - if (targetRoom->tags().contains(TestTag)) - { - cout << "Test tag set, removing it now" << endl; - targetRoom->removeTag(TestTag); - QMC_CHECK("Tagging test", !targetRoom->tags().contains(TestTag)); - QObject::disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); - } - }); - cout << "Adding a tag" << endl; - targetRoom->addTag(TestTag); -} - -void QMCTest::sendAndRedact() -{ - running.push_back("Redaction"); - cout << "Sending a message to redact" << endl; - if (auto* job = targetRoom->connection()->sendMessage(targetRoom->id(), - RoomMessageEvent(origin % ": message to redact"))) - { - connect(job, &BaseJob::success, targetRoom, [job,this] { - cout << "Redacting the message" << endl; - targetRoom->redactEvent(job->eventId(), origin); - // Make sure to save the event id because the job is about to end. - connect(targetRoom, &Room::aboutToAddNewMessages, this, - std::bind(&QMCTest::checkRedactionOutcome, - this, job->eventId(), _1)); - }); - } else - QMC_CHECK("Redaction", false); -} - -void QMCTest::checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events) -{ - static bool checkSucceeded = false; - // There are two possible (correct) outcomes: either the event comes already - // redacted at the next sync, or the nearest sync completes with - // the unredacted event but the next one brings redaction. - auto it = std::find_if(events.begin(), events.end(), - [=] (const RoomEventPtr& e) { - return e->id() == evtIdToRedact; - }); - if (it == events.end()) - return; // Waiting for the next sync - - if ((*it)->isRedacted()) - { - if (checkSucceeded) - { - const auto msg = - "The redacted event came in with the sync again, ignoring"; - cout << msg << endl; - targetRoom->postPlainText(msg); - return; - } - cout << "The sync brought already redacted message" << endl; - QMC_CHECK("Redaction", true); - // Not disconnecting because there are other connections from this class - // to aboutToAddNewMessages - checkSucceeded = true; - return; - } - // The event is not redacted - if (checkSucceeded) - { - const auto msg = - "Warning: the redacted event came non-redacted with the sync!"; - cout << msg << endl; - targetRoom->postPlainText(msg); - } - cout << "Message came non-redacted with the sync, waiting for redaction" << endl; - connect(targetRoom, &Room::replacedEvent, targetRoom, - [=] (const RoomEvent* newEvent, const RoomEvent* oldEvent) { - QMC_CHECK("Redaction", oldEvent->id() == evtIdToRedact && - newEvent->isRedacted() && - newEvent->redactionReason() == origin); - checkSucceeded = true; - disconnect(targetRoom, &Room::replacedEvent, nullptr, nullptr); - }); - -} - -void QMCTest::markDirectChat() -{ - if (targetRoom->directChatUsers().contains(c->user())) - { - cout << "Warning: the room is already a direct chat," - " only unmarking will be tested" << endl; - checkDirectChatOutcome({{ c->user(), targetRoom->id() }}); - return; - } - // Connect first because the signal is emitted synchronously. - connect(c.data(), &Connection::directChatsListChanged, - this, &QMCTest::checkDirectChatOutcome); - cout << "Marking the room as a direct chat" << endl; - c->addToDirectChats(targetRoom, c->user()); -} - -void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added) -{ - running.push_back("Direct chat test"); - disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr); - if (!targetRoom->isDirectChat()) - { - cout << "The room has not been marked as a direct chat" << endl; - QMC_CHECK("Direct chat test", false); - return; - } - if (!added.contains(c->user(), targetRoom->id())) - { - cout << "The room has not been listed in new direct chats" << endl; - QMC_CHECK("Direct chat test", false); - return; - } - - cout << "Unmarking the direct chat" << endl; - c->removeFromDirectChats(targetRoom->id(), c->user()); - QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom->id())); -} - -void QMCTest::leave() -{ - if (targetRoom) - { - cout << "Leaving the room" << endl; - connect(targetRoom->leaveRoom(), &BaseJob::finished, - this, &QMCTest::finalize); - } - else - finalize(); -} - -void QMCTest::finalize() -{ - cout << "Logging out" << endl; - c->logout(); - connect(c.data(), &Connection::loggedOut, qApp, - [this] { - if (!failed.isEmpty()) - cout << "FAILED: " << failed.join(", ").toStdString() << endl; - if (!running.isEmpty()) - cout << "DID NOT FINISH: " - << running.join(", ").toStdString() << endl; - QCoreApplication::processEvents(); - QCoreApplication::exit(failed.size() + running.size()); - }); -} - -int main(int argc, char* argv[]) -{ - QCoreApplication app(argc, argv); - if (argc < 4) - { - cout << "Usage: qmc-example <user> <passwd> <device_name> [<room_alias> [origin]]" << endl; - return -1; - } - - cout << "Connecting to the server as " << argv[1] << endl; - auto conn = new Connection; - conn->connectToServer(argv[1], argv[2], argv[3]); - QMCTest test { conn, argc >= 5 ? argv[4] : nullptr, - argc >= 6 ? argv[5] : nullptr }; - return app.exec(); -} diff --git a/gtad/data.h.mustache b/gtad/data.h.mustache new file mode 100644 index 00000000..1b511262 --- /dev/null +++ b/gtad/data.h.mustache @@ -0,0 +1,56 @@ +{{! +SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net> +SPDX-License-Identifier: LGPL-2.1-or-later +}}{{>preamble}} +#pragma once + +#include "converters.h" +{{#imports}} +#include {{_}}{{/imports}} + +namespace Quotient { +{{#models}} + {{#model}} +{{>docCommentShort}} +struct {{name}}{{#parents?}} : {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} +{ {{#vars}} + + {{>docCommentShort}} + {{>maybeOmittableType}} {{nameCamelCase}}; + {{/vars}}{{#propertyMap}} + + {{>docCommentShort}} + {{>maybeOmittableType}} {{nameCamelCase}}; + {{/propertyMap}} +}; + +template <> struct JsonObjectConverter<{{name}}> +{ + {{#in?}} + static void dumpTo(QJsonObject& jo, const {{name}}& pod) + { {{#propertyMap}} + fillJson(jo, pod.{{nameCamelCase}}); + {{/propertyMap}}{{#parents}} + fillJson<{{name}}>(jo, pod); + {{/parents}}{{#vars}} + addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, + QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); + {{/vars}} + } + {{/in?}} + {{#out?}} + static void fillFrom({{>maybeCrefJsonObject}} jo, {{name}}& pod) + { {{#parents}} + fillFromJson<{{qualifiedName}}>(jo, pod); + {{/parents}}{{#vars}} + fromJson(jo.{{>takeOrValue}}("{{baseName}}"_ls), pod.{{nameCamelCase}}); + {{/vars}}{{#propertyMap}} + fromJson(jo, pod.{{nameCamelCase}}); + {{/propertyMap}} + } + {{/out?}} +}; + + {{/model}} +{{/models}} +} // namespace Quotient diff --git a/gtad/gtad.yaml b/gtad/gtad.yaml new file mode 100644 index 00000000..4b05d2d4 --- /dev/null +++ b/gtad/gtad.yaml @@ -0,0 +1,243 @@ +analyzer: + subst: + "%CLIENT_RELEASE_LABEL%": r0 + "%CLIENT_MAJOR_VERSION%": r0 + identifiers: + signed: signedData + unsigned: unsignedData + PushRule/default: isDefault + default: defaultVersion # getCapabilities/RoomVersionsCapability + origin_server_ts: originServerTimestamp # Instead of originServerTs + start: begin # Because start() is a method in BaseJob + m.upload.size: uploadSize + m.homeserver: homeserver + m.identity_server: identityServer + m.change_password: changePassword + m.room_versions: roomVersions + AuthenticationData/additionalProperties: authInfo + /^/(Location|Protocol|User)$/: 'ThirdParty$1' + # Change some response names + /requestTokenTo.*</data/: response + requestOpenIdToken</data: tokenData + getDevice</data: device + getFilter</data: filter + getProtocols</data: protocols + getOneRoomEvent</data: event + getRoomState</data: events + getPushRule</data: pushRule + # These parameters are deprecated and unused in Quotient; so drop them + login>/user: "" + login>/medium: "" + login>/address: "" + login</home_server: "" + register</home_server: "" + + # Structure inside `types`: + # - swaggerType: <targetTypeSpec> + # OR + # - swaggerType: + # - swaggerFormat: <targetTypeSpec> + # - /swaggerFormatRegEx/: <targetTypeSpec> + # - //: <targetTypeSpec> # default, if the format doesn't mach anything above + # WHERE + # targetTypeSpec = targetType OR + # { type: targetType, imports: <filename OR [ filenames... ]>, <other attributes...> } + # swaggerType can be +set/+on pair; attributes from the map under +set + # are added to each type from the sequence under +on. + types: + - +set: &UseOmittable + useOmittable: + omittedValue: 'none' # Quotient::none in lib/omittable.h + +on: + - integer: + - int64: qint64 + - int32: qint32 + - //: int + - number: + - float: float + - //: double + - boolean: bool + - string: + - byte: &ByteStream + type: QIODevice* + imports: <QtCore/QIODevice> + - binary: *ByteStream + - +set: { avoidCopy: } + +on: + - date: + type: QDate + initializer: QDate::fromString("{{defaultValue}}") + - dateTime: + type: QDateTime + initializer: QDateTime::fromString("{{defaultValue}}") + - uri: + type: QUrl + initializer: QUrl::fromEncoded("{{defaultValue}}") + - //: &QString + type: QString + initializer: QStringLiteral("{{defaultValue}}") + isString: + - file: *ByteStream + - +set: { avoidCopy: } + +on: + - object: &QJsonObject { type: QJsonObject } + - $ref: + - +set: + moveOnly: + +on: + - /state_event.yaml$/: + type: StateEventPtr + imports: '"events/stateevent.h"' + - /(room|client)_event.yaml$/: + type: RoomEventPtr + imports: '"events/roomevent.h"' + - /event(_without_room_id)?.yaml$/: + type: EventPtr + imports: '"events/event.h"' + - +set: + # This renderer applies to everything actually $ref'ed + # (not substituted) + _importRenderer: '"{{#segments}}{{_}}{{#_join}}/{{/_join}}{{/segments}}.h"' + +on: + - '/^(\./)?definitions/request_email_validation.yaml$/': + title: EmailValidationData + - '/^(\./)?definitions/request_msisdn_validation.yaml$/': + title: MsisdnValidationData + - /_filter.yaml$/: # Event/RoomEventFilters do NOT need Omittable<> + + # Despite being used in two calls, it's more practical to have those + # fields available as getters right from the respective job classes + - /public_rooms_response.yaml$/: { _inline: true } + + # list_public_rooms.yaml (via public_rooms_response.yaml) and + # space_hierarchy.yaml use public_rooms_chunk.yaml as a common base + # structure, adding (space_hiearchy) or overriding + # (public_rooms_response) fields for their purposes. The spec text + # confusingly ends up with having two different structures named + # "PublicRoomsChunk". To make sure the types are distinct in + # libQuotient, this common base is inlined into the actually used + # data structures (that have distinct names) defined + # in space_hierarchy.h and public_rooms_response.h, respectively + - /public_rooms_chunk.yaml$/: { _inline: true } + - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types + - schema: + - getTurnServer<: *QJsonObject # It's used as an opaque JSON object +# - defineFilter>: &Filter # Force folding into a structure +# type: Filter +# imports: '"csapi/definitions/sync_filter.h"' +# - getFilter<: *Filter + - StrippedChildStateEvent: void # only used in an array, see below + - RoomFilter: # A structure inside Filter, same story as with *_filter.yaml + - OneTimeKeys: + type: OneTimeKeys + imports: '"e2ee/e2ee.h"' + - //: *UseOmittable + - array: + - string: QStringList + - +set: { moveOnly: } + +on: + - /^Notification|Result|ChildRoomsChunk$/: "std::vector<{{1}}>" + - /^StrippedChildStateEvent$|state_event.yaml$/: + type: StateEvents + imports: '"events/stateevent.h"' # For StrippedChildStateEvent + - /(room|client)_event.yaml$/: RoomEvents + - /event(_without_room_id)?.yaml$/: Events + - //: "QVector<{{1}}>" + - map: # `additionalProperties` in OpenAPI + - RoomState: + type: "UnorderedMap<QString, {{1}}>" + moveOnly: + - /.+/: "QHash<QString, {{1}}>" + - //: QVariantHash # QJsonObject?.. + - variant: # A sequence `type` or a 'oneOf' group in OpenAPI + - /^string,null|null,string$/: *QString + - //: QVariant + + #operations: + +mustache: +# delimiter: '%| |%' # or something else instead of '{{ }}' + constants: + # Syntax elements used by GTAD +# _quote: '"' # Common quote for left and right +# _leftQuote: '"' +# _rightQuote: '"_ls' + _comment: '//' + copyrightName: Kitsune Ral + copyrightEmail: <kitsune-ral@users.sf.net> + + partials: + _typeRenderer: "{{#scope}}{{scopeCamelCase}}Job::{{/scope}}{{>name}}" + omittedValue: '{}' # default value to initialize omitted parameters with + initializer: '{{defaultValue}}' + cjoin: '{{#hasMore}}, {{/hasMore}}' + + openOmittable: + "{{^required?}}{{#useOmittable}}\ + {{^defaultValue}}Omittable<{{/defaultValue}}\ + {{/useOmittable}}{{/required?}}" + closeOmittable: + "{{^required?}}{{#useOmittable}}\ + {{^defaultValue}}>{{/defaultValue}}\ + {{/useOmittable}}{{/required?}}" + + maybeOmittableType: "{{>openOmittable}}{{dataType.name}}{{>closeOmittable}}" + qualifiedMaybeOmittableType: + "{{>openOmittable}}{{dataType.qualifiedName}}{{>closeOmittable}}" + + maybeCrefType: + "{{#avoidCopy}}const {{/avoidCopy}}{{>maybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}" + + maybeCrefJsonObject: + "{{^propertyMap}}const QJsonObject&{{/propertyMap}}\ + {{#propertyMap}}QJsonObject{{/propertyMap}}" + + takeOrValue: + "{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}" + takeOrLoad: "{{#moveOnly}}take{{/moveOnly}}{{^moveOnly}}load{{/moveOnly}}" + + initializeDefaultValue: + "{{#defaultValue}}{{>initializer}}{{/defaultValue}}\ + {{^defaultValue}}{{>omittedValue}}{{/defaultValue}}" + + # No inner indents in folded values! + + joinedParamDecl: >- + {{>maybeCrefType}} {{paramName}} + {{^required?}} = {{>initializeDefaultValue}}{{/required?}}{{>cjoin}} + joinedParamDef: "{{>maybeCrefType}} {{paramName}}{{>cjoin}}" + + passPathAndMaybeQuery: >- + makePath("{{basePathWithoutHost}}"{{#pathParts}}, + {{_}}{{/pathParts}}){{#queryParams?}}, + queryTo{{camelCaseOperationId}}( + {{#queryParams}}{{paramName}}{{>cjoin}}{{/queryParams}}){{/queryParams?}} + + nonInlineResponseSignature: |- + {{>docCommentShort}} + {{>maybeOmittableType}} {{paramName}}(){{^moveOnly}} const{{/moveOnly}} + + # Doc-comment blocks. Comment indent is managed by clang-format + # (without clang-format there'd have to be a separate partial definition + # for each indent...) but we take care of line breaks to maintain + # some sanity even before clang-format + + # This is for structures that don't expect a summary (e.g., JSON schema) + docCommentShort: |- + {{#description}} + /// {{_}}{{/description}} + # For structures with the summary, a common partial for summary is here; + # the main part is different in different places + docCommentSummary: |- + {{#summary}} \brief {{summary}} + *{{/summary}} + + templates: + data: + .h: "{{>data.h.mustache}}" + api: + .h: "{{>operation.h.mustache}}" + .cpp: "{{>operation.cpp.mustache}}" + + #outFilesList: apifiles.txt + diff --git a/gtad/operation.cpp.mustache b/gtad/operation.cpp.mustache new file mode 100644 index 00000000..4b75434c --- /dev/null +++ b/gtad/operation.cpp.mustache @@ -0,0 +1,59 @@ +{{! +SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net> +SPDX-License-Identifier: LGPL-2.1-or-later +}}{{>preamble}} +#include "{{filenameBase}}.h" + +using namespace Quotient; +{{#operations}}{{#operation}} + {{#queryParams?}} + +auto queryTo{{camelCaseOperationId}}( + {{#queryParams}}{{>joinedParamDef}}{{/queryParams}}) +{ + QUrlQuery _q;{{#queryParams}} + addParam<{{^required?}}IfNotEmpty{{/required?}}>(_q, + QStringLiteral("{{baseName}}"), {{paramName}});{{/queryParams}} + return _q; +} + {{/queryParams?}} + {{^hasBody?}} + +QUrl {{camelCaseOperationId}}Job::makeRequestUrl(QUrl baseUrl{{#allParams?}}, + {{#allParams}}{{>joinedParamDef}}{{/allParams}}{{/allParams?}}) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), {{>passPathAndMaybeQuery}}); +} {{/hasBody?}} + +{{camelCaseOperationId}}Job::{{camelCaseOperationId}}Job( + {{#allParams}}{{>joinedParamDef}}{{/allParams}}) + : BaseJob(HttpVerb::{{#_cap}}{{#_tolower}}{{httpMethod}}{{/_tolower}}{{/_cap}}, + {{!object name}}QStringLiteral("{{camelCaseOperationId}}Job"), + {{>passPathAndMaybeQuery}} + {{#skipAuth}}{{#queryParams?}}, {}{{/queryParams?}}, false{{/skipAuth}} ) +{ {{#headerParams}} + setRequestHeader("{{baseName}}", {{paramName}}.toLatin1()); + {{/headerParams}}{{#inlineBody}}{{^propertyMap}}{{^bodyParams?}} + setRequestData({ {{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? + }}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}} }); + {{/bodyParams?}}{{/propertyMap}}{{/inlineBody + }}{{^consumesNonJson?}}{{#bodyParams?}} + QJsonObject _dataJson; + {{#propertyMap}} + fillJson(_dataJson, {{nameCamelCase}}); + {{/propertyMap}}{{#inlineBody}} + fillJson<{{>maybeOmittableType}}>(_dataJson, {{paramName}}); + {{/inlineBody}}{{#bodyParams}} + addParam<{{^required?}}IfNotEmpty{{/required?}}>(_dataJson, + QStringLiteral("{{baseName}}"), {{paramName}}); + {{/bodyParams}} + setRequestData({ _dataJson }); + {{/bodyParams?}}{{/consumesNonJson?}}{{#producesNonJson?}} + setExpectedContentTypes({ {{#produces}}"{{_}}"{{>cjoin}}{{/produces}} }); + {{/producesNonJson?}}{{^producesNonJson? + }}{{#responses}}{{#normalResponse?}}{{#properties}}{{#required?}} + addExpectedKey("{{baseName}}"); + {{/required?}}{{/properties}}{{/normalResponse?}}{{/responses + }}{{/producesNonJson?}} +} +{{/operation}}{{/operations}} diff --git a/gtad/operation.h.mustache b/gtad/operation.h.mustache new file mode 100644 index 00000000..063f0bbd --- /dev/null +++ b/gtad/operation.h.mustache @@ -0,0 +1,128 @@ +{{! +SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net> +SPDX-License-Identifier: LGPL-2.1-or-later +}}{{>preamble}} +#pragma once + +#include "jobs/basejob.h" +{{#imports}} +#include {{_}}{{/imports}} +{{#operations.producesNonJson?}} +#include <QtNetwork/QNetworkReply>{{/operations.producesNonJson?}} + +namespace Quotient { +{{#operations.operation}} + +/*!{{>docCommentSummary}}{{#description}} + * {{_}}{{/description}} + */ +class QUOTIENT_API {{camelCaseOperationId}}Job : public BaseJob { +public: + {{#models}} + // Inner data structures + {{#model}} + + {{>docCommentShort}} + struct {{name}}{{#parents?}} : + {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} + { + {{#vars}} + {{>docCommentShort}} + {{>maybeOmittableType}} {{nameCamelCase}}; + {{/vars}} + {{#propertyMap}} + {{>docCommentShort}} + {{>maybeOmittableType}} {{nameCamelCase}}; + {{/propertyMap}} + }; + {{/model}} + + // Construction/destruction + + {{/models}} + {{#allParams?}} + /*!{{>docCommentSummary}} + {{#allParams}} + * \param {{nameCamelCase}}{{#description}} + * {{_}}{{/description}}{{#_join}} + * {{/_join}} + {{/allParams}} + */ + {{/allParams?}}{{^allParams?}} + {{#summary}} + /// {{summary}} + {{/summary}} + {{/allParams?}} + explicit {{camelCaseOperationId}}Job({{#allParams}}{{>joinedParamDecl}}{{/allParams}}); + {{^hasBody?}} + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for {{camelCaseOperationId}}Job + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl{{#allParams?}}, + {{#allParams}}{{>joinedParamDecl}}{{/allParams}}{{/allParams?}}); + {{/hasBody?}} + {{#responses}}{{#normalResponse?}}{{#allProperties?}} + + // Result properties + {{#headers}} + + {{>nonInlineResponseSignature}} + { + return reply()->rawHeader("{{baseName}}"); + } + {{/headers}}{{#inlineResponse}} + + {{>docCommentShort}} + {{dataType.name}} {{paramName}}() + {{^moveOnly}}{{^producesNonJson?}} const{{/producesNonJson?}}{{/moveOnly}} + { + return {{#producesNonJson?}}reply(){{/producesNonJson?}} + {{^producesNonJson? + }}fromJson<{{dataType.name}}>(jsonData()){{/producesNonJson? + }}; + } + {{/inlineResponse}}{{#properties}} + + {{!there's nothing in #properties if the response is inline}} + {{>nonInlineResponseSignature}} + { + return {{>takeOrLoad}}FromJson<{{>maybeOmittableType}}>("{{baseName}}"_ls); + } + {{/properties}} + {{/allProperties?}}{{/normalResponse?}}{{/responses}} +}; + {{#models.model}} + +template <> struct JsonObjectConverter<{{qualifiedName}}> { + {{#in?}} + static void dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod) + { {{#propertyMap}} + fillJson(jo, pod.{{nameCamelCase}}); + {{/propertyMap}}{{#parents}} + fillJson<{{name}}>(jo, pod); + {{/parents}}{{#vars}} + addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, + QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); + {{/vars}} + } + {{/in?}} + {{#out?}} + static void fillFrom({{>maybeCrefJsonObject}} jo, {{qualifiedName}}& result) + { {{#parents}} + fillFromJson<{{name}}{{!of the parent!}}>(jo, result); + {{/parents}}{{#vars}} + fromJson(jo.{{>takeOrValue}}("{{baseName}}"_ls), + result.{{nameCamelCase}}); + {{/vars}}{{#propertyMap}} + fromJson(jo, result.{{nameCamelCase}}); + {{/propertyMap}} + } + {{/out?}} +}; + {{/models.model}} +{{/operations.operation}} + +} // namespace Quotient diff --git a/lib/csapi/preamble.mustache b/gtad/preamble.mustache index 3ba87d61..3ba87d61 100644 --- a/lib/csapi/preamble.mustache +++ b/gtad/preamble.mustache diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp new file mode 100644 index 00000000..ad7c5f99 --- /dev/null +++ b/lib/accountregistry.cpp @@ -0,0 +1,137 @@ +// 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" +#include <QtCore/QCoreApplication> + +using namespace Quotient; + +void AccountRegistry::add(Connection* a) +{ + if (contains(a)) + return; + beginInsertRows(QModelIndex(), size(), size()); + push_back(a); + endInsertRows(); + emit accountCountChanged(); +} + +void AccountRegistry::drop(Connection* a) +{ + if (const auto idx = indexOf(a); idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + remove(idx); + endRemoveRows(); + } + Q_ASSERT(!contains(a)); +} + +bool AccountRegistry::isLoggedIn(const QString &userId) const +{ + return std::any_of(cbegin(), cend(), [&userId](const Connection* a) { + return a->userId() == userId; + }); +} + +QVariant AccountRegistry::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() >= count()) + return {}; + + if (role == AccountRole) + return QVariant::fromValue(at(index.row())); + + return {}; +} + +int AccountRegistry::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : count(); +} + +QHash<int, QByteArray> AccountRegistry::roleNames() const +{ + return { { AccountRole, "connection" } }; +} + +Connection* AccountRegistry::get(const QString& userId) +{ + for (const auto &connection : *this) { + if (connection->userId() == userId) + return connection; + } + return nullptr; +} + +QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const QString& userId) +{ + qCDebug(MAIN) << "Reading access token from keychain for" << userId; + auto job = new QKeychain::ReadPasswordJob(qAppName(), this); + job->setKey(userId); + job->start(); + + return job; +} + +void AccountRegistry::invokeLogin() +{ + const auto accounts = SettingsGroup("Accounts").childGroups(); + for (const auto& accountId : accounts) { + AccountSettings account { accountId }; + m_accountsLoading += accountId; + emit accountsLoadingChanged(); + + if (account.homeserver().isEmpty()) + continue; + + auto accessTokenLoadingJob = + loadAccessTokenFromKeychain(account.userId()); + connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, + [accountId, this, accessTokenLoadingJob]() { + if (accessTokenLoadingJob->error() + != QKeychain::Error::NoError) { + emit keychainError(accessTokenLoadingJob->error()); + return; + } + + AccountSettings account { accountId }; + auto connection = new Connection(account.homeserver()); + connect(connection, &Connection::connected, this, + [connection, this, accountId] { + connection->loadState(); + connection->setLazyLoading(true); + + connection->syncLoop(); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); + }); + connect(connection, &Connection::loginError, this, + [this, connection, accountId](const QString& error, + const QString& details) { + emit loginError(connection, error, details); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); + }); + connect(connection, &Connection::resolveError, this, + [this, connection, accountId](const QString& error) { + emit resolveError(connection, error); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); + }); + connection->assumeIdentity( + account.userId(), accessTokenLoadingJob->binaryData(), + account.deviceId()); + }); + } +} + +QStringList AccountRegistry::accountsLoading() const +{ + return m_accountsLoading; +} diff --git a/lib/accountregistry.h b/lib/accountregistry.h new file mode 100644 index 00000000..9560688e --- /dev/null +++ b/lib/accountregistry.h @@ -0,0 +1,92 @@ +// 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 "quotient_export.h" +#include "settings.h" + +#include <QtCore/QAbstractListModel> + +#if QT_VERSION_MAJOR >= 6 +# include <qt6keychain/keychain.h> +#else +# include <qt5keychain/keychain.h> +#endif + +namespace QKeychain { +class ReadPasswordJob; +} + +namespace Quotient { +class Connection; + +class QUOTIENT_API AccountRegistry : public QAbstractListModel, + private QVector<Connection*> { + Q_OBJECT + /// Number of accounts that are currently fully loaded + Q_PROPERTY(int accountCount READ rowCount NOTIFY accountCountChanged) + /// List of accounts that are currently in some stage of being loaded (Reading token from keychain, trying to contact server, etc). + /// Can be used to inform the user or to show a login screen if size() == 0 and no accounts are loaded + Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged) +public: + using vector_t = QVector<Connection*>; + using const_iterator = vector_t::const_iterator; + using const_reference = vector_t::const_reference; + + enum EventRoles { + AccountRole = Qt::UserRole + 1, + ConnectionRole = AccountRole + }; + + [[deprecated("Use Accounts variable instead")]] // + static AccountRegistry& instance(); + + // Expose most of vector_t's const-API but only provide add() and drop() + // for changing it. In theory other changing operations could be supported + // too; but then boilerplate begin/end*() calls has to be tucked into each + // and this class gives no guarantees on the order of entries, so why care. + + const vector_t& accounts() const { return *this; } + void add(Connection* a); + void drop(Connection* a); + const_iterator begin() const { return vector_t::begin(); } + const_iterator end() const { return vector_t::end(); } + const_reference front() const { return vector_t::front(); } + const_reference back() const { return vector_t::back(); } + bool isLoggedIn(const QString& userId) const; + Connection* get(const QString& userId); + + using vector_t::isEmpty, vector_t::empty; + using vector_t::size, vector_t::count, vector_t::capacity; + using vector_t::cbegin, vector_t::cend, vector_t::contains; + + // QAbstractItemModel interface implementation + + [[nodiscard]] QVariant data(const QModelIndex& index, + int role) const override; + [[nodiscard]] int rowCount( + const QModelIndex& parent = QModelIndex()) const override; + [[nodiscard]] QHash<int, QByteArray> roleNames() const override; + + QStringList accountsLoading() const; + + void invokeLogin(); +Q_SIGNALS: + void accountCountChanged(); + void accountsLoadingChanged(); + + void keychainError(QKeychain::Error error); + void loginError(Connection* connection, QString message, QString details); + void resolveError(Connection* connection, QString error); + +private: + QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId); + QStringList m_accountsLoading; +}; + +inline QUOTIENT_API AccountRegistry Accounts {}; + +inline AccountRegistry& AccountRegistry::instance() { return Accounts; } +} // namespace Quotient diff --git a/lib/application-service/definitions/location.cpp b/lib/application-service/definitions/location.cpp deleted file mode 100644 index 958a55bf..00000000 --- a/lib/application-service/definitions/location.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "location.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const ThirdPartyLocation& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("alias"), pod.alias); - addParam<>(jo, QStringLiteral("protocol"), pod.protocol); - addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; -} - -ThirdPartyLocation FromJsonObject<ThirdPartyLocation>::operator()(const QJsonObject& jo) const -{ - ThirdPartyLocation result; - result.alias = - fromJson<QString>(jo.value("alias"_ls)); - result.protocol = - fromJson<QString>(jo.value("protocol"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - - return result; -} - diff --git a/lib/application-service/definitions/location.h b/lib/application-service/definitions/location.h index 89b48a43..6801c99f 100644 --- a/lib/application-service/definitions/location.h +++ b/lib/application-service/definitions/location.h @@ -6,27 +6,33 @@ #include "converters.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Data structures +struct ThirdPartyLocation { + /// An alias for a matrix room. + QString alias; - struct ThirdPartyLocation - { - /// An alias for a matrix room. - QString alias; - /// The protocol ID that the third party location is a part of. - QString protocol; - /// Information used to identify this third party location. - QJsonObject fields; - }; + /// The protocol ID that the third party location is a part of. + QString protocol; - QJsonObject toJson(const ThirdPartyLocation& pod); + /// Information used to identify this third party location. + QJsonObject fields; +}; - template <> struct FromJsonObject<ThirdPartyLocation> +template <> +struct JsonObjectConverter<ThirdPartyLocation> { + static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod) + { + addParam<>(jo, QStringLiteral("alias"), pod.alias); + addParam<>(jo, QStringLiteral("protocol"), pod.protocol); + addParam<>(jo, QStringLiteral("fields"), pod.fields); + } + static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod) { - ThirdPartyLocation operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("alias"_ls), pod.alias); + fromJson(jo.value("protocol"_ls), pod.protocol); + fromJson(jo.value("fields"_ls), pod.fields); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/application-service/definitions/protocol.cpp b/lib/application-service/definitions/protocol.cpp deleted file mode 100644 index 04bb7dfc..00000000 --- a/lib/application-service/definitions/protocol.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "protocol.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const FieldType& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("regexp"), pod.regexp); - addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); - return jo; -} - -FieldType FromJsonObject<FieldType>::operator()(const QJsonObject& jo) const -{ - FieldType result; - result.regexp = - fromJson<QString>(jo.value("regexp"_ls)); - result.placeholder = - fromJson<QString>(jo.value("placeholder"_ls)); - - return result; -} - -QJsonObject QMatrixClient::toJson(const ProtocolInstance& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("desc"), pod.desc); - addParam<IfNotEmpty>(jo, QStringLiteral("icon"), pod.icon); - addParam<>(jo, QStringLiteral("fields"), pod.fields); - addParam<>(jo, QStringLiteral("network_id"), pod.networkId); - return jo; -} - -ProtocolInstance FromJsonObject<ProtocolInstance>::operator()(const QJsonObject& jo) const -{ - ProtocolInstance result; - result.desc = - fromJson<QString>(jo.value("desc"_ls)); - result.icon = - fromJson<QString>(jo.value("icon"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - result.networkId = - fromJson<QString>(jo.value("network_id"_ls)); - - return result; -} - -QJsonObject QMatrixClient::toJson(const ThirdPartyProtocol& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); - addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); - addParam<>(jo, QStringLiteral("icon"), pod.icon); - addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); - addParam<>(jo, QStringLiteral("instances"), pod.instances); - return jo; -} - -ThirdPartyProtocol FromJsonObject<ThirdPartyProtocol>::operator()(const QJsonObject& jo) const -{ - ThirdPartyProtocol result; - result.userFields = - fromJson<QStringList>(jo.value("user_fields"_ls)); - result.locationFields = - fromJson<QStringList>(jo.value("location_fields"_ls)); - result.icon = - fromJson<QString>(jo.value("icon"_ls)); - result.fieldTypes = - fromJson<QHash<QString, FieldType>>(jo.value("field_types"_ls)); - result.instances = - fromJson<QVector<ProtocolInstance>>(jo.value("instances"_ls)); - - return result; -} - diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h index 2aca7d66..213dbf19 100644 --- a/lib/application-service/definitions/protocol.h +++ b/lib/application-service/definitions/protocol.h @@ -6,84 +6,112 @@ #include "converters.h" -#include <QtCore/QHash> -#include <QtCore/QJsonObject> -#include "converters.h" -#include <QtCore/QVector> +namespace Quotient { +/// Definition of valid values for a field. +struct FieldType { + /// A regular expression for validation of a field's value. This may be + /// relatively coarse to verify the value as the application service + /// providing this protocol may apply additional validation or filtering. + QString regexp; -namespace QMatrixClient -{ - // Data structures + /// An placeholder serving as a valid example of the field value. + QString placeholder; +}; - /// Definition of valid values for a field. - struct FieldType +template <> +struct JsonObjectConverter<FieldType> { + static void dumpTo(QJsonObject& jo, const FieldType& pod) + { + addParam<>(jo, QStringLiteral("regexp"), pod.regexp); + addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); + } + static void fillFrom(const QJsonObject& jo, FieldType& pod) { - /// A regular expression for validation of a field's value. This may be relatively - /// coarse to verify the value as the application service providing this protocol - /// may apply additional validation or filtering. - QString regexp; - /// An placeholder serving as a valid example of the field value. - QString placeholder; - }; + fromJson(jo.value("regexp"_ls), pod.regexp); + fromJson(jo.value("placeholder"_ls), pod.placeholder); + } +}; - QJsonObject toJson(const FieldType& pod); +struct ProtocolInstance { + /// A human-readable description for the protocol, such as the name. + QString desc; - template <> struct FromJsonObject<FieldType> - { - FieldType operator()(const QJsonObject& jo) const; - }; + /// An optional content URI representing the protocol. Overrides the one + /// provided at the higher level Protocol object. + QString icon; + + /// Preset values for `fields` the client may use to search by. + QJsonObject fields; + + /// A unique identifier across all instances. + QString networkId; +}; - struct ProtocolInstance +template <> +struct JsonObjectConverter<ProtocolInstance> { + static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod) { - /// A human-readable description for the protocol, such as the name. - QString desc; - /// An optional content URI representing the protocol. Overrides the one provided - /// at the higher level Protocol object. - QString icon; - /// Preset values for ``fields`` the client may use to search by. - QJsonObject fields; - /// A unique identifier across all instances. - QString networkId; - }; - - QJsonObject toJson(const ProtocolInstance& pod); - - template <> struct FromJsonObject<ProtocolInstance> + addParam<>(jo, QStringLiteral("desc"), pod.desc); + addParam<IfNotEmpty>(jo, QStringLiteral("icon"), pod.icon); + addParam<>(jo, QStringLiteral("fields"), pod.fields); + addParam<>(jo, QStringLiteral("network_id"), pod.networkId); + } + static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod) { - ProtocolInstance operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("desc"_ls), pod.desc); + fromJson(jo.value("icon"_ls), pod.icon); + fromJson(jo.value("fields"_ls), pod.fields); + fromJson(jo.value("network_id"_ls), pod.networkId); + } +}; + +struct ThirdPartyProtocol { + /// Fields which may be used to identify a third party user. These should be + /// ordered to suggest the way that entities may be grouped, where higher + /// groupings are ordered first. For example, the name of a network should + /// be searched before the nickname of a user. + QStringList userFields; + + /// Fields which may be used to identify a third party location. These + /// should be ordered to suggest the way that entities may be grouped, where + /// higher groupings are ordered first. For example, the name of a network + /// should be searched before the name of a channel. + QStringList locationFields; + + /// 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. + /// + /// May be an empty object if no fields are defined. + QHash<QString, FieldType> fieldTypes; + + /// A list of objects representing independent instances of configuration. + /// For example, multiple networks on IRC if multiple are provided by the + /// same application service. + QVector<ProtocolInstance> instances; +}; - struct ThirdPartyProtocol +template <> +struct JsonObjectConverter<ThirdPartyProtocol> { + static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod) { - /// Fields which may be used to identify a third party user. These should be - /// ordered to suggest the way that entities may be grouped, where higher - /// groupings are ordered first. For example, the name of a network should be - /// searched before the nickname of a user. - QStringList userFields; - /// Fields which may be used to identify a third party location. These should be - /// ordered to suggest the way that entities may be grouped, where higher - /// groupings are ordered first. For example, the name of a network should be - /// searched before the name of a channel. - QStringList locationFields; - /// 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. - /// - /// May be an empty object if no fields are defined. - QHash<QString, FieldType> fieldTypes; - /// A list of objects representing independent instances of configuration. - /// For example, multiple networks on IRC if multiple are provided by the - /// same application service. - QVector<ProtocolInstance> instances; - }; - - QJsonObject toJson(const ThirdPartyProtocol& pod); - - template <> struct FromJsonObject<ThirdPartyProtocol> + addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); + addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); + addParam<>(jo, QStringLiteral("icon"), pod.icon); + addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); + addParam<>(jo, QStringLiteral("instances"), pod.instances); + } + static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod) { - ThirdPartyProtocol operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("user_fields"_ls), pod.userFields); + fromJson(jo.value("location_fields"_ls), pod.locationFields); + fromJson(jo.value("icon"_ls), pod.icon); + fromJson(jo.value("field_types"_ls), pod.fieldTypes); + fromJson(jo.value("instances"_ls), pod.instances); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/application-service/definitions/user.cpp b/lib/application-service/definitions/user.cpp deleted file mode 100644 index ca334236..00000000 --- a/lib/application-service/definitions/user.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "user.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const ThirdPartyUser& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("userid"), pod.userid); - addParam<>(jo, QStringLiteral("protocol"), pod.protocol); - addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; -} - -ThirdPartyUser FromJsonObject<ThirdPartyUser>::operator()(const QJsonObject& jo) const -{ - ThirdPartyUser result; - result.userid = - fromJson<QString>(jo.value("userid"_ls)); - result.protocol = - fromJson<QString>(jo.value("protocol"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - - return result; -} - diff --git a/lib/application-service/definitions/user.h b/lib/application-service/definitions/user.h index 79ca7789..3342ef80 100644 --- a/lib/application-service/definitions/user.h +++ b/lib/application-service/definitions/user.h @@ -6,27 +6,33 @@ #include "converters.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Data structures +struct ThirdPartyUser { + /// A Matrix User ID represting a third party user. + QString userid; - struct ThirdPartyUser - { - /// A Matrix User ID represting a third party user. - QString userid; - /// The protocol ID that the third party location is a part of. - QString protocol; - /// Information used to identify this third party location. - QJsonObject fields; - }; + /// The protocol ID that the third party location is a part of. + QString protocol; - QJsonObject toJson(const ThirdPartyUser& pod); + /// Information used to identify this third party location. + QJsonObject fields; +}; - template <> struct FromJsonObject<ThirdPartyUser> +template <> +struct JsonObjectConverter<ThirdPartyUser> { + static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod) + { + addParam<>(jo, QStringLiteral("userid"), pod.userid); + addParam<>(jo, QStringLiteral("protocol"), pod.protocol); + addParam<>(jo, QStringLiteral("fields"), pod.fields); + } + static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod) { - ThirdPartyUser operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("userid"_ls), pod.userid); + fromJson(jo.value("protocol"_ls), pod.protocol); + fromJson(jo.value("fields"_ls), pod.fields); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/avatar.cpp b/lib/avatar.cpp index b8e1096d..13de99bf 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -1,101 +1,74 @@ -#include <utility> - -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "avatar.h" -#include "jobs/mediathumbnailjob.h" -#include "events/eventcontent.h" #include "connection.h" -#include <QtGui/QPainter> -#include <QtCore/QPointer> +#include "events/eventcontent.h" +#include "jobs/mediathumbnailjob.h" + #include <QtCore/QDir> +#include <QtCore/QPointer> #include <QtCore/QStandardPaths> #include <QtCore/QStringBuilder> +#include <QtGui/QPainter> -using namespace QMatrixClient; +using namespace Quotient; using std::move; -class Avatar::Private -{ - public: - explicit Private(QUrl url = {}) - : _url(move(url)) - { } - ~Private() - { - if (isJobRunning(_thumbnailRequest)) - _thumbnailRequest->abandon(); - if (isJobRunning(_uploadRequest)) - _uploadRequest->abandon(); - } - - QImage get(Connection* connection, QSize size, - get_callback_t callback) const; - bool upload(UploadContentJob* job, upload_callback_t callback); - - bool checkUrl(const QUrl& url) const; - QString localFile() const; - - QUrl _url; - - // The below are related to image caching, hence mutable - mutable QImage _originalImage; - mutable std::vector<QPair<QSize, QImage>> _scaledImages; - mutable QSize _requestedSize; - mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown; - mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr; - mutable QPointer<BaseJob> _uploadRequest = nullptr; - mutable std::vector<get_callback_t> callbacks; -}; +class Avatar::Private { +public: + explicit Private(QUrl url = {}) : _url(move(url)) {} + ~Private() + { + if (isJobPending(_thumbnailRequest)) + _thumbnailRequest->abandon(); + if (isJobPending(_uploadRequest)) + _uploadRequest->abandon(); + } -Avatar::Avatar() - : d(std::make_unique<Private>()) -{ } + QImage get(Connection* connection, QSize size, + get_callback_t callback) const; + bool upload(UploadContentJob* job, upload_callback_t&& callback); -Avatar::Avatar(QUrl url) - : d(std::make_unique<Private>(std::move(url))) -{ } + bool checkUrl(const QUrl& url) const; + QString localFile() const; -Avatar::Avatar(Avatar&&) = default; + QUrl _url; -Avatar::~Avatar() = default; + // The below are related to image caching, hence mutable + mutable QImage _originalImage; + mutable std::vector<std::pair<QSize, QImage>> _scaledImages; + mutable QSize _requestedSize; + mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown; + mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr; + mutable QPointer<BaseJob> _uploadRequest = nullptr; + mutable std::vector<get_callback_t> callbacks; +}; -Avatar& Avatar::operator=(Avatar&&) = default; +Avatar::Avatar() + : d(makeImpl<Private>()) +{} + +Avatar::Avatar(QUrl url) : d(makeImpl<Private>(std::move(url))) {} QImage Avatar::get(Connection* connection, int dimension, get_callback_t callback) const { - return d->get(connection, {dimension, dimension}, move(callback)); + return d->get(connection, { dimension, dimension }, move(callback)); } QImage Avatar::get(Connection* connection, int width, int height, get_callback_t callback) const { - return d->get(connection, {width, height}, move(callback)); + return d->get(connection, { width, height }, move(callback)); } bool Avatar::upload(Connection* connection, const QString& fileName, upload_callback_t callback) const { - if (isJobRunning(d->_uploadRequest)) + if (isJobPending(d->_uploadRequest)) return false; return d->upload(connection->uploadFile(fileName), move(callback)); } @@ -103,27 +76,22 @@ bool Avatar::upload(Connection* connection, const QString& fileName, bool Avatar::upload(Connection* connection, QIODevice* source, upload_callback_t callback) const { - if (isJobRunning(d->_uploadRequest) || !source->isReadable()) + if (isJobPending(d->_uploadRequest) || !source->isReadable()) return false; return d->upload(connection->uploadContent(source), move(callback)); } -QString Avatar::mediaId() const -{ - return d->_url.authority() + d->_url.path(); -} +QString Avatar::mediaId() const { return d->_url.authority() + d->_url.path(); } QImage Avatar::Private::get(Connection* connection, QSize size, get_callback_t callback) const { - if (!callback) - { + if (!callback) { qCCritical(MAIN) << "Null callbacks are not allowed in Avatar::get"; Q_ASSERT(false); } - if (_imageSource == Unknown && _originalImage.load(localFile())) - { + if (_imageSource == Unknown && _originalImage.load(localFile())) { _imageSource = Cache; _requestedSize = _originalImage.size(); } @@ -132,45 +100,48 @@ QImage Avatar::Private::get(Connection* connection, QSize size, // to trick the below code into constantly getting another image from // the server because the existing one is alleged unsatisfactory. // Client authors can only blame themselves if they do so. - if (((_imageSource == Unknown && !_thumbnailRequest) || - size.width() > _requestedSize.width() || - size.height() > _requestedSize.height()) && checkUrl(_url)) { + if (((_imageSource == Unknown && !_thumbnailRequest) + || size.width() > _requestedSize.width() + || size.height() > _requestedSize.height()) + && checkUrl(_url)) { qCDebug(MAIN) << "Getting avatar from" << _url.toString(); _requestedSize = size; - if (isJobRunning(_thumbnailRequest)) + if (isJobPending(_thumbnailRequest)) _thumbnailRequest->abandon(); if (callback) callbacks.emplace_back(move(callback)); _thumbnailRequest = connection->getThumbnail(_url, size); - QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, - _thumbnailRequest, [this] { - _imageSource = Network; - _originalImage = - _thumbnailRequest->scaledThumbnail(_requestedSize); - _originalImage.save(localFile()); - _scaledImages.clear(); - for (const auto& n: callbacks) - n(); - callbacks.clear(); - }); + QObject::connect(_thumbnailRequest, &MediaThumbnailJob::success, + _thumbnailRequest, [this] { + _imageSource = Network; + _originalImage = _thumbnailRequest->scaledThumbnail( + _requestedSize); + _originalImage.save(localFile()); + _scaledImages.clear(); + for (const auto& n : callbacks) + n(); + callbacks.clear(); + }); } - for (const auto& p: _scaledImages) - if (p.first == size) - return p.second; - auto result = _originalImage.isNull() ? QImage() : _originalImage.scaled(size, - Qt::KeepAspectRatio, Qt::SmoothTransformation); + for (const auto& [scaledSize, scaledImage] : _scaledImages) + if (scaledSize == size) + return scaledImage; + auto result = _originalImage.isNull() + ? QImage() + : _originalImage.scaled(size, Qt::KeepAspectRatio, + Qt::SmoothTransformation); _scaledImages.emplace_back(size, result); return result; } -bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback) +bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback) { _uploadRequest = job; - if (!isJobRunning(_uploadRequest)) + if (!isJobPending(_uploadRequest)) return false; _uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest, - [job,callback] { callback(job->contentUri()); }); + [job, callback] { callback(job->contentUri()); }); return true; } @@ -181,8 +152,7 @@ bool Avatar::Private::checkUrl(const QUrl& url) const // FIXME: Make "mxc" a library-wide constant and maybe even make // the URL checker a Connection(?) method. - if (!url.isValid() || url.scheme() != "mxc" || url.path().count('/') != 1) - { + if (!url.isValid() || url.scheme() != "mxc" || url.path().count('/') != 1) { qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:" << url.toDisplayString(); _imageSource = Banned; @@ -190,18 +160,9 @@ bool Avatar::Private::checkUrl(const QUrl& url) const return _imageSource != Banned; } -QString cacheLocation() { - const auto cachePath = - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - + "/avatar/"; - QDir dir; - if (!dir.exists(cachePath)) - dir.mkpath(cachePath); - return cachePath; -} - -QString Avatar::Private::localFile() const { - static const auto cachePath = cacheLocation(); +QString Avatar::Private::localFile() const +{ + static const auto cachePath = cacheLocation(QStringLiteral("avatars")); return cachePath % _url.authority() % '_' % _url.fileName() % ".png"; } @@ -214,7 +175,7 @@ bool Avatar::updateUrl(const QUrl& newUrl) d->_url = newUrl; d->_imageSource = Private::Unknown; - if (isJobRunning(d->_thumbnailRequest)) + if (isJobPending(d->_thumbnailRequest)) d->_thumbnailRequest->abandon(); return true; } diff --git a/lib/avatar.h b/lib/avatar.h index c86345e3..c94dc369 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -1,61 +1,42 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include <QtGui/QIcon> +#include "util.h" + #include <QtCore/QUrl> +#include <QtGui/QIcon> #include <functional> -#include <memory> - -namespace QMatrixClient -{ - class Connection; - - class Avatar - { - public: - explicit Avatar(); - explicit Avatar(QUrl url); - Avatar(Avatar&&); - ~Avatar(); - Avatar& operator=(Avatar&&); - - using get_callback_t = std::function<void()>; - using upload_callback_t = std::function<void(QString)>; - - QImage get(Connection* connection, int dimension, - get_callback_t callback) const; - QImage get(Connection* connection, int w, int h, - get_callback_t callback) const; - - bool upload(Connection* connection, const QString& fileName, - upload_callback_t callback) const; - bool upload(Connection* connection, QIODevice* source, - upload_callback_t callback) const; - - QString mediaId() const; - QUrl url() const; - bool updateUrl(const QUrl& newUrl); - - private: - class Private; - std::unique_ptr<Private> d; - }; -} // namespace QMatrixClient + +namespace Quotient { +class Connection; + +class QUOTIENT_API Avatar { +public: + explicit Avatar(); + explicit Avatar(QUrl url); + + using get_callback_t = std::function<void()>; + using upload_callback_t = std::function<void(QUrl)>; + + QImage get(Connection* connection, int dimension, + get_callback_t callback) const; + QImage get(Connection* connection, int w, int h, + get_callback_t callback) const; + + bool upload(Connection* connection, const QString& fileName, + upload_callback_t callback) const; + bool upload(Connection* connection, QIODevice* source, + upload_callback_t callback) const; + + QString mediaId() const; + QUrl url() const; + bool updateUrl(const QUrl& newUrl); + +private: + class Private; + ImplPtr<Private> d; +}; +} // namespace Quotient diff --git a/lib/connection.cpp b/lib/connection.cpp index 6bdedf26..4547474a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1,62 +1,76 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2019 Ville Ranki <ville.ranki@iki.fi> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "connection.h" + +#include "accountregistry.h" #include "connectiondata.h" -#include "user.h" -#include "events/directchatevent.h" -#include "events/eventloader.h" +#include "qt_connection_util.h" #include "room.h" #include "settings.h" -#include "csapi/login.h" -#include "csapi/logout.h" -#include "csapi/receipts.h" -#include "csapi/leaving.h" +#include "user.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" #include "csapi/joining.h" -#include "csapi/to_device.h" +#include "csapi/leaving.h" +#include "csapi/logout.h" #include "csapi/room_send.h" -#include "jobs/syncjob.h" -#include "jobs/mediathumbnailjob.h" -#include "jobs/downloadfilejob.h" +#include "csapi/to_device.h" #include "csapi/voip.h" +#include "csapi/wellknown.h" +#include "csapi/whoami.h" -#include <QtNetwork/QDnsLookup> -#include <QtCore/QFile> +#include "events/directchatevent.h" +#include "jobs/downloadfilejob.h" +#include "jobs/mediathumbnailjob.h" +#include "jobs/syncjob.h" +#include <variant> + +#ifdef Quotient_E2EE_ENABLED +# include "database.h" +# include "keyverificationsession.h" + +# include "e2ee/qolmaccount.h" +# include "e2ee/qolminboundsession.h" +# include "e2ee/qolmsession.h" +# include "e2ee/qolmutility.h" +# include "e2ee/qolmutils.h" + +# include "events/keyverificationevent.h" +#endif // Quotient_E2EE_ENABLED + +#if QT_VERSION_MAJOR >= 6 +# include <qt6keychain/keychain.h> +#else +# include <qt5keychain/keychain.h> +#endif + +#include <QtCore/QCoreApplication> #include <QtCore/QDir> -#include <QtCore/QFileInfo> -#include <QtCore/QStandardPaths> -#include <QtCore/QStringBuilder> #include <QtCore/QElapsedTimer> +#include <QtCore/QFile> +#include <QtCore/QMimeDatabase> #include <QtCore/QRegularExpression> -#include <QtCore/QCoreApplication> +#include <QtCore/QStandardPaths> +#include <QtCore/QStringBuilder> +#include <QtNetwork/QDnsLookup> -using namespace QMatrixClient; +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();) - { - if (pred(it)) - { + for (auto it = hashMap.begin(); it != hashMap.end();) { + if (pred(it)) { removals.insert(it.key(), it.value()); it = hashMap.erase(it); } else @@ -65,479 +79,1180 @@ HashT erase_if(HashT& hashMap, Pred pred) return removals; } -#ifndef TRIM_RAW_DATA -#define TRIM_RAW_DATA 65535 +class Connection::Private { +public: + explicit Private(std::unique_ptr<ConnectionData>&& connection) + : data(move(connection)) + {} + + Connection* q = nullptr; + std::unique_ptr<ConnectionData> data; + // A complex key below is a pair of room name and whether its + // state is Invited. The spec mandates to keep Invited room state + // separately; specifically, we should keep objects for Invite and + // Leave state of the same room if the two happen to co-exist. + QHash<std::pair<QString, bool>, Room*> roomMap; + /// Mapping from serverparts to alias/room id mappings, + /// as of the last sync + QHash<QString, QString> roomAliasMap; + QVector<QString> roomIdsToForget; + QVector<QString> pendingStateRoomIds; + QMap<QString, User*> userMap; + DirectChatsMap directChats; + DirectChatUsersMap directChatUsers; + // The below two variables track local changes between sync completions. + // See https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events + DirectChatsMap dcLocalAdditions; + DirectChatsMap dcLocalRemovals; + UnorderedMap<QString, EventPtr> accountData; + QMetaObject::Connection syncLoopConnection {}; + int syncTimeout = -1; + +#ifdef Quotient_E2EE_ENABLED + QSet<QString> trackedUsers; + QSet<QString> outdatedUsers; + QHash<QString, QHash<QString, DeviceKeys>> deviceKeys; + QueryKeysJob *currentQueryKeysJob = nullptr; + bool encryptionUpdateRequired = false; + PicklingMode picklingMode = Unencrypted {}; + Database *database = nullptr; + QHash<QString, int> oneTimeKeysCount; + std::vector<std::unique_ptr<EncryptedEvent>> pendingEncryptedEvents; + void handleEncryptedToDeviceEvent(const EncryptedEvent& event); + bool processIfVerificationEvent(const Event &evt, bool encrypted); + + // A map from SenderKey to vector of InboundSession + UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions; + + QHash<QString, KeyVerificationSession*> verificationSessions; +#endif + + GetCapabilitiesJob* capabilitiesJob = nullptr; + GetCapabilitiesJob::Capabilities capabilities; + + QVector<GetLoginFlowsJob::LoginFlow> loginFlows; + +#ifdef Quotient_E2EE_ENABLED + std::unique_ptr<QOlmAccount> olmAccount; + bool isUploadingKeys = false; + bool firstSync = true; +#endif // Quotient_E2EE_ENABLED + + QPointer<GetWellknownJob> resolverJob = nullptr; + QPointer<GetLoginFlowsJob> loginFlowsJob = nullptr; + + SyncJob* syncJob = nullptr; + QPointer<LogoutJob> logoutJob = nullptr; + + bool cacheState = true; + bool cacheToBinary = + SettingsGroup("libQuotient").get("cache_type", + SettingsGroup("libQMatrixClient").get<QString>("cache_type")) + != "json"; + bool lazyLoading = false; + + /** \brief Check the homeserver and resolve it if needed, before connecting + * + * A single entry for functions that need to check whether the homeserver + * is valid before running. May execute connectFn either synchronously + * or asynchronously. In case of errors, emits resolveError() if + * the homeserver URL is not valid and cannot be resolved from userId, or + * the homeserver doesn't support the requested login flow. + * + * \param userId fully-qualified MXID to resolve HS from + * \param connectFn a function to execute once the HS URL is good + * \param flow optionally, a login flow that should be supported for + * connectFn to work; `none`, if there's no login flow + * requirements + * \sa resolveServer, resolveError + */ + void checkAndConnect(const QString &userId, + const std::function<void ()> &connectFn, + const std::optional<LoginFlow> &flow = none); + template <typename... LoginArgTs> + void loginToServer(LoginArgTs&&... loginArgs); + void completeSetup(const QString &mxId); + void removeRoom(const QString& roomId); + + void consumeRoomData(SyncDataList&& roomDataList, bool fromCache); + void consumeAccountData(Events&& accountDataEvents); + void consumePresenceData(Events&& presenceData); + void consumeToDeviceEvents(Events&& toDeviceEvents); + void consumeDevicesList(DevicesList&& devicesList); + + void packAndSendAccountData(EventPtr&& event) + { + const auto eventType = event->matrixType(); + q->callApi<SetAccountDataJob>(data->userId(), eventType, + event->contentJson()); + accountData[eventType] = std::move(event); + emit q->accountDataChanged(eventType); + } + + template <EventClass EventT, typename ContentT> + void packAndSendAccountData(ContentT&& content) + { + packAndSendAccountData( + makeEvent<EventT>(std::forward<ContentT>(content))); + } + QString topLevelStatePath() const + { + return q->stateCacheDir().filePath("state.json"); + } + +#ifdef Quotient_E2EE_ENABLED + void loadSessions() { + olmSessions = q->database()->loadOlmSessions(picklingMode); + } + void saveSession(const QOlmSession& session, const QString& senderKey) const + { + q->database()->saveOlmSession(senderKey, session.sessionId(), + session.pickle(picklingMode), + QDateTime::currentDateTime()); + } + + template <typename FnT> + std::pair<QString, QString> doDecryptMessage(const QOlmSession& session, + const QOlmMessage& message, + FnT&& andThen) const + { + const auto expectedMessage = session.decrypt(message); + if (expectedMessage) { + const auto result = + std::make_pair(*expectedMessage, session.sessionId()); + andThen(); + return result; + } + const auto errorLine = message.type() == QOlmMessage::PreKey + ? "Failed to decrypt prekey message:" + : "Failed to decrypt message:"; + qCDebug(E2EE) << errorLine << expectedMessage.error(); + return {}; + } + + std::pair<QString, QString> sessionDecryptMessage( + const QJsonObject& personalCipherObject, const QByteArray& senderKey) + { + const auto msgType = static_cast<QOlmMessage::Type>( + personalCipherObject.value(TypeKeyL).toInt(-1)); + if (msgType != QOlmMessage::General && msgType != QOlmMessage::PreKey) { + qCWarning(E2EE) << "Olm message has incorrect type" << msgType; + return {}; + } + QOlmMessage message { + personalCipherObject.value(BodyKeyL).toString().toLatin1(), msgType + }; + for (const auto& session : olmSessions[senderKey]) + if (msgType == QOlmMessage::General + || session->matchesInboundSessionFrom(senderKey, message)) { + return doDecryptMessage(*session, message, [this, &session] { + q->database()->setOlmSessionLastReceived( + session->sessionId(), QDateTime::currentDateTime()); + }); + } + + if (msgType == QOlmMessage::General) { + qCWarning(E2EE) << "Failed to decrypt message"; + return {}; + } + + qCDebug(E2EE) << "Creating new inbound session"; // Pre-key messages only + auto newSessionResult = + olmAccount->createInboundSessionFrom(senderKey, message); + if (!newSessionResult) { + qCWarning(E2EE) + << "Failed to create inbound session for" << senderKey + << "with error" << newSessionResult.error(); + return {}; + } + auto newSession = std::move(*newSessionResult); + if (olmAccount->removeOneTimeKeys(*newSession) != OLM_SUCCESS) { + qWarning(E2EE) << "Failed to remove one time key for session" + << newSession->sessionId(); + // Keep going though + } + return doDecryptMessage( + *newSession, message, [this, &senderKey, &newSession] { + saveSession(*newSession, senderKey); + olmSessions[senderKey].push_back(std::move(newSession)); + }); + } #endif -class Connection::Private -{ - public: - explicit Private(std::unique_ptr<ConnectionData>&& connection) - : data(move(connection)) - { } - Q_DISABLE_COPY(Private) - Private(Private&&) = delete; - Private operator=(Private&&) = delete; - - Connection* q = nullptr; - std::unique_ptr<ConnectionData> data; - // A complex key below is a pair of room name and whether its - // state is Invited. The spec mandates to keep Invited room state - // separately so we should, e.g., keep objects for Invite and - // Leave state of the same room. - QHash<QPair<QString, bool>, Room*> roomMap; - QVector<QString> roomIdsToForget; - QVector<Room*> firstTimeRooms; - QMap<QString, User*> userMap; - DirectChatsMap directChats; - DirectChatUsersMap directChatUsers; - std::unordered_map<QString, EventPtr> accountData; - QString userId; - - SyncJob* syncJob = nullptr; - - bool cacheState = true; - bool cacheToBinary = SettingsGroup("libqmatrixclient") - .value("cache_type").toString() != "json"; - - void connectWithToken(const QString& user, const QString& accessToken, - const QString& deviceId); - void broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals); - - template <typename EventT> - EventT* unpackAccountData() const - { - const auto& eventIt = accountData.find(EventT::matrixTypeId()); - return eventIt == accountData.end() - ? nullptr : weakPtrCast<EventT>(eventIt->second); + std::pair<EventPtr, QString> sessionDecryptMessage(const EncryptedEvent& encryptedEvent) + { +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return {}; +#else + if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey) + return {}; + + const auto identityKey = olmAccount->identityKeys().curve25519; + const auto personalCipherObject = + encryptedEvent.ciphertext(identityKey); + if (personalCipherObject.isEmpty()) { + qCDebug(E2EE) << "Encrypted event is not for the current device"; + return {}; + } + const auto [decrypted, olmSessionId] = + sessionDecryptMessage(personalCipherObject, + encryptedEvent.senderKey().toLatin1()); + if (decrypted.isEmpty()) { + qCDebug(E2EE) << "Problem with new session from senderKey:" + << encryptedEvent.senderKey() + << olmAccount->oneTimeKeys().keys; + return {}; } - void packAndSendAccountData(EventPtr&& event) - { - const auto eventType = event->matrixType(); - q->callApi<SetAccountDataJob>(userId, eventType, - event->contentJson()); - accountData[eventType] = std::move(event); - emit q->accountDataChanged(eventType); + auto&& decryptedEvent = + fromJson<EventPtr>(QJsonDocument::fromJson(decrypted.toUtf8())); + + if (auto sender = decryptedEvent->fullJson()[SenderKeyL].toString(); + sender != encryptedEvent.senderId()) { + qCWarning(E2EE) << "Found user" << sender + << "instead of sender" << encryptedEvent.senderId() + << "in Olm plaintext"; + return {}; } - template <typename EventT, typename ContentT> - void packAndSendAccountData(ContentT&& content) - { - packAndSendAccountData( - makeEvent<EventT>(std::forward<ContentT>(content))); + auto query = database->prepareQuery(QStringLiteral("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;")); + query.bindValue(":curveKey", encryptedEvent.contentJson()["sender_key"].toString()); + database->execute(query); + if (!query.next()) { + qCWarning(E2EE) << "Received olm message from unknown device" << encryptedEvent.contentJson()["sender_key"].toString(); + return {}; } + auto edKey = decryptedEvent->fullJson()["keys"]["ed25519"].toString(); + if (edKey.isEmpty() || query.value(QStringLiteral("edKey")).toString() != edKey) { + qCDebug(E2EE) << "Received olm message with invalid ed key"; + return {}; + } + + // TODO: keys to constants + const auto decryptedEventObject = decryptedEvent->fullJson(); + const auto recipient = decryptedEventObject.value("recipient"_ls).toString(); + if (recipient != data->userId()) { + qCDebug(E2EE) << "Found user" << recipient << "instead of us" + << data->userId() << "in Olm plaintext"; + return {}; + } + const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject() + .value(Ed25519Key).toString(); + if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) { + qCDebug(E2EE) << "Found key" << ourKey + << "instead of ours own ed25519 key" + << olmAccount->identityKeys().ed25519 + << "in Olm plaintext"; + return {}; + } + + return { std::move(decryptedEvent), olmSessionId }; +#endif // Quotient_E2EE_ENABLED + } +#ifdef Quotient_E2EE_ENABLED + bool isKnownCurveKey(const QString& userId, const QString& curveKey) const; + + void loadOutdatedUserDevices(); + void saveDevicesList(); + void loadDevicesList(); + + // This function assumes that an olm session with (user, device) exists + std::pair<QOlmMessage::Type, QByteArray> olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const; + bool createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const OneTimeKeys &oneTimeKeyObject); + QString curveKeyForUserDevice(const QString& userId, + const QString& device) const; + QJsonObject assembleEncryptedContent(QJsonObject payloadJson, + const QString& targetUserId, + const QString& targetDeviceId) const; +#endif + + void saveAccessTokenToKeychain() const + { + qCDebug(MAIN) << "Saving access token to keychain for" << q->userId(); + auto job = new QKeychain::WritePasswordJob(qAppName()); + job->setAutoDelete(true); + job->setKey(q->userId()); + job->setBinaryData(data->accessToken()); + job->start(); + //TODO error handling + } + + void dropAccessToken() + { + qCDebug(MAIN) << "Removing access token from keychain for" << q->userId(); + auto job = new QKeychain::DeletePasswordJob(qAppName()); + job->setAutoDelete(true); + job->setKey(q->userId()); + job->start(); + + auto pickleJob = new QKeychain::DeletePasswordJob(qAppName()); + pickleJob->setAutoDelete(true); + pickleJob->setKey(q->userId() + "-Pickle"_ls); + pickleJob->start(); + //TODO error handling + + data->setToken({}); + } }; Connection::Connection(const QUrl& server, QObject* parent) : QObject(parent) - , d(std::make_unique<Private>(std::make_unique<ConnectionData>(server))) + , d(makeImpl<Private>(std::make_unique<ConnectionData>(server))) { +#ifdef Quotient_E2EE_ENABLED + //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount); +#endif d->q = this; // All d initialization should occur before this line } -Connection::Connection(QObject* parent) - : Connection({}, parent) -{ } +Connection::Connection(QObject* parent) : Connection({}, parent) {} Connection::~Connection() { - qCDebug(MAIN) << "deconstructing connection object for" << d->userId; + qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); + Accounts.drop(this); } -void Connection::resolveServer(const QString& mxidOrDomain) +void Connection::resolveServer(const QString& mxid) { - // At this point we may have something as complex as - // @username:[IPv6:address]:port, or as simple as a plain domain name. + if (isJobPending(d->resolverJob)) + d->resolverJob->abandon(); - // Try to parse as an FQID; if there's no @ part, assume it's a domain name. - QRegularExpression parser( - "^(@.+?:)?" // Optional username (allow everything for compatibility) - "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address - "(:\\d{1,5})?$", // Optional port - QRegularExpression::UseUnicodePropertiesOption); // Because asian digits - auto match = parser.match(mxidOrDomain); - - QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2)); + auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid)); maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" - if (!match.hasMatch() || !maybeBaseUrl.isValid()) - { - emit resolveError( - tr("%1 is not a valid homeserver address") - .arg(maybeBaseUrl.toString())); + if (maybeBaseUrl.isEmpty() || !maybeBaseUrl.isValid()) { + emit resolveError(tr("%1 is not a valid homeserver address") + .arg(maybeBaseUrl.toString())); return; } - setHomeserver(maybeBaseUrl); - emit resolved(); - return; - - // FIXME, #178: The below code is incorrect and is no more executed. The - // correct server resolution should be done from .well-known/matrix/client - auto domain = maybeBaseUrl.host(); - qCDebug(MAIN) << "Finding the server" << domain; - // Check if the Matrix server has a dedicated service record. - auto* dns = new QDnsLookup(); - dns->setType(QDnsLookup::SRV); - dns->setName("_matrix._tcp." + domain); - - connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() { - QUrl baseUrl { maybeBaseUrl }; - if (dns->error() == QDnsLookup::NoError && - dns->serviceRecords().isEmpty()) - { - auto record = dns->serviceRecords().front(); - baseUrl.setHost(record.target()); - baseUrl.setPort(record.port()); - qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host() - << "is" << baseUrl.authority(); + qCDebug(MAIN) << "Finding the server" << maybeBaseUrl.host(); + + const auto& oldBaseUrl = d->data->baseUrl(); + d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call + d->resolverJob = callApi<GetWellknownJob>(); + // Connect to finished() to make sure baseUrl is restored in any case + connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl, oldBaseUrl] { + // Revert baseUrl so that setHomeserver() below triggers signals + // in case the base URL actually changed + d->data->setBaseUrl(oldBaseUrl); + if (d->resolverJob->error() == BaseJob::Abandoned) + return; + + if (d->resolverJob->error() != BaseJob::NotFound) { + if (!d->resolverJob->status().good()) { + qCWarning(MAIN) + << "Fetching .well-known file failed, FAIL_PROMPT"; + emit resolveError(tr("Failed resolving the homeserver")); + return; + } + QUrl baseUrl { d->resolverJob->data().homeserver.baseUrl }; + if (baseUrl.isEmpty()) { + qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT"; + emit resolveError( + tr("The homeserver base URL is not provided")); + return; + } + if (!baseUrl.isValid()) { + qCWarning(MAIN) << "base_url invalid, FAIL_ERROR"; + emit resolveError(tr("The homeserver base URL is invalid")); + return; + } + qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host() << "is" + << baseUrl.toString(); + setHomeserver(baseUrl); } else { - qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record" - << dns->name() << "- using the hostname as is"; + qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl + << "for base URL"; + setHomeserver(maybeBaseUrl); } - setHomeserver(baseUrl); - emit resolved(); - dns->deleteLater(); + Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver() }); - dns->lookup(); } -void Connection::connectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId) +inline UserIdentifier makeUserIdentifier(const QString& id) { - checkAndConnect(user, - [=] { - doConnectToServer(user, password, initialDeviceName, deviceId); - }); + return { QStringLiteral("m.id.user"), { { QStringLiteral("user"), id } } }; } -void Connection::doConnectToServer(const QString& user, const QString& password, + +inline UserIdentifier make3rdPartyIdentifier(const QString& medium, + const QString& address) +{ + return { QStringLiteral("m.id.thirdparty"), + { { QStringLiteral("medium"), medium }, + { QStringLiteral("address"), address } } }; +} + +void Connection::loginWithPassword(const QString& userId, + const QString& password, const QString& initialDeviceName, const QString& deviceId) { - auto loginJob = callApi<LoginJob>(QStringLiteral("m.login.password"), - UserIdentifier { QStringLiteral("m.id.user"), - {{ QStringLiteral("user"), user }} }, - password, /*token*/ "", deviceId, initialDeviceName); - connect(loginJob, &BaseJob::success, this, - [this, loginJob] { - d->connectWithToken(loginJob->userId(), loginJob->accessToken(), - loginJob->deviceId()); + d->checkAndConnect(userId, [=,this] { + d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId), + password, /*token*/ "", deviceId, initialDeviceName); + }, LoginFlows::Password); +} + +SsoSession* Connection::prepareForSso(const QString& initialDeviceName, + const QString& deviceId) +{ + return new SsoSession(this, initialDeviceName, deviceId); +} + +void Connection::loginWithToken(const QByteArray& loginToken, + const QString& initialDeviceName, + const QString& deviceId) +{ + Q_ASSERT(d->data->baseUrl().isValid() && d->loginFlows.contains(LoginFlows::Token)); + d->loginToServer(LoginFlows::Token.type, + none /*user is encoded in loginToken*/, "" /*password*/, + loginToken, deviceId, initialDeviceName); +} + +void Connection::assumeIdentity(const QString& mxId, const QString& accessToken, + const QString& deviceId) +{ + d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] { + d->data->setToken(accessToken.toLatin1()); + d->data->setDeviceId(deviceId); // Can't we deduce this from access_token? + auto* job = callApi<GetTokenOwnerJob>(); + connect(job, &BaseJob::success, this, [this, job, mxId] { + if (mxId != job->userId()) + qCWarning(MAIN).nospace() + << "The access_token owner (" << job->userId() + << ") is different from passed MXID (" << mxId << ")!"; + d->completeSetup(job->userId()); }); - connect(loginJob, &BaseJob::failure, this, - [this, loginJob] { - emit loginError(loginJob->errorString(), - loginJob->rawData(TRIM_RAW_DATA)); + connect(job, &BaseJob::failure, this, [this, job] { + emit loginError(job->errorString(), job->rawDataSample()); }); + }); } -void Connection::connectWithToken(const QString& userId, - const QString& accessToken, - const QString& deviceId) +void Connection::reloadCapabilities() { - checkAndConnect(userId, - [=] { d->connectWithToken(userId, accessToken, deviceId); }); + d->capabilitiesJob = callApi<GetCapabilitiesJob>(BackgroundRequest); + connect(d->capabilitiesJob, &BaseJob::success, this, [this] { + d->capabilities = d->capabilitiesJob->capabilities(); + + if (d->capabilities.roomVersions) { + qCDebug(MAIN) << "Room versions:" << defaultRoomVersion() + << "is default, full list:" << availableRoomVersions(); + emit capabilitiesLoaded(); + for (auto* r: std::as_const(d->roomMap)) + r->checkVersion(); + } else + qCWarning(MAIN) + << "The server returned an empty set of supported versions;" + " disabling version upgrade recommendations to reduce noise"; + }); + connect(d->capabilitiesJob, &BaseJob::failure, this, [this] { + if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest) + qCDebug(MAIN) << "Server doesn't support /capabilities;" + " version upgrade recommendations won't be issued"; + }); +} + +bool Connection::loadingCapabilities() const +{ + // (Ab)use the fact that room versions cannot be omitted after + // the capabilities have been loaded (see reloadCapabilities() above). + return !d->capabilities.roomVersions; } -void Connection::Private::connectWithToken(const QString& user, - const QString& accessToken, - const QString& deviceId) +template <typename... LoginArgTs> +void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) { - userId = user; + auto loginJob = + q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...); + connect(loginJob, &BaseJob::success, q, [this, loginJob] { + data->setToken(loginJob->accessToken().toLatin1()); + data->setDeviceId(loginJob->deviceId()); + completeSetup(loginJob->userId()); + saveAccessTokenToKeychain(); +#ifdef Quotient_E2EE_ENABLED + database->clear(); +#endif + }); + connect(loginJob, &BaseJob::failure, q, [this, loginJob] { + emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); + }); +} + +void Connection::Private::completeSetup(const QString& mxId) +{ + data->setUserId(mxId); q->user(); // Creates a User object for the local user - data->setToken(accessToken.toLatin1()); - data->setDeviceId(deviceId); + q->setObjectName(data->userId() % '/' % data->deviceId()); qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() - << "by user" << userId << "from device" << deviceId; + << "by user" << data->userId() + << "from device" << data->deviceId(); + Accounts.add(q); + connect(qApp, &QCoreApplication::aboutToQuit, q, &Connection::saveState); +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED + AccountSettings accountSettings(data->userId()); + + QKeychain::ReadPasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + QEventLoop loop; + QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error() == QKeychain::Error::EntryNotFound) { + picklingMode = Encrypted { RandomBuffer(128) }; + QKeychain::WritePasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + job.setBinaryData(std::get<Encrypted>(picklingMode).key); + QEventLoop loop; + QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error()) { + qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString(); + } + } else if(job.error() != QKeychain::Error::NoError) { + //TODO Error, do something + qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error(); + } else { + qCDebug(E2EE) << "Successfully loaded pickling key from keychain"; + picklingMode = Encrypted { job.binaryData() }; + } + + database = new Database(data->userId(), data->deviceId(), q); + + // init olmAccount + olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q); + connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); + + loadSessions(); + + if (database->accountPickle().isEmpty()) { + // create new account and save unpickle data + olmAccount->createNewAccount(); + auto job = q->callApi<UploadKeysJob>(olmAccount->deviceKeys()); + connect(job, &BaseJob::failure, q, [job]{ + qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString(); + }); + } else { + // account already existing + if (!olmAccount->unpickle(database->accountPickle(), picklingMode)) + qWarning(E2EE) + << "Could not unpickle Olm account, E2EE won't be available"; + } +#endif // Quotient_E2EE_ENABLED emit q->stateChanged(); emit q->connected(); - + q->reloadCapabilities(); } -void Connection::checkAndConnect(const QString& userId, - std::function<void()> connectFn) +void Connection::Private::checkAndConnect(const QString& userId, + const std::function<void()>& connectFn, + const std::optional<LoginFlow>& flow) { - if (d->data->baseUrl().isValid()) - { + if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) { connectFn(); return; } - // Not good to go, try to fix the homeserver URL. - if (userId.startsWith('@') && userId.indexOf(':') != -1) - { - connectSingleShot(this, &Connection::homeserverChanged, this, connectFn); - // NB: doResolveServer can emit resolveError, so this is a part of - // checkAndConnect function contract. - resolveServer(userId); + // Not good to go, try to ascertain the homeserver URL and flows + if (userId.startsWith('@') && userId.indexOf(':') != -1) { + q->resolveServer(userId); + if (flow) + connectSingleShot(q, &Connection::loginFlowsChanged, q, + [this, flow, connectFn] { + if (loginFlows.contains(*flow)) + 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)); + }); + else + connectSingleShot(q, &Connection::homeserverChanged, q, connectFn); } else - emit resolveError( - tr("%1 is an invalid homeserver URL") - .arg(d->data->baseUrl().toString())); + emit q->resolveError(tr("Please provide the fully-qualified user id" + " (such as @user:example.org) so that the" + " homeserver could be resolved; the current" + " homeserver URL(%1) is not good") + .arg(data->baseUrl().toDisplayString())); } void Connection::logout() { - auto job = callApi<LogoutJob>(); - connect( job, &LogoutJob::success, this, [this] { - stopSync(); - d->data->setToken({}); - emit stateChanged(); - emit loggedOut(); + // If there's an ongoing sync job, stop it (this also suspends sync loop) + const auto wasSyncing = bool(d->syncJob); + if (wasSyncing) + { + d->syncJob->abandon(); + d->syncJob = nullptr; + } + + d->logoutJob = callApi<LogoutJob>(); + emit stateChanged(); // isLoggedIn() == false from now + + connect(d->logoutJob, &LogoutJob::finished, this, [this, wasSyncing] { + if (d->logoutJob->status().good() + || d->logoutJob->error() == BaseJob::Unauthorised + || d->logoutJob->error() == BaseJob::ContentAccessError) { + if (d->syncLoopConnection) + disconnect(d->syncLoopConnection); + SettingsGroup("Accounts").remove(userId()); + d->dropAccessToken(); + emit loggedOut(); + deleteLater(); + } else { // logout() somehow didn't proceed - restore the session state + emit stateChanged(); + if (wasSyncing) + syncLoopIteration(); // Resume sync loop (or a single sync) + } }); } void Connection::sync(int timeout) { - if (d->syncJob) + if (d->syncJob) { + qCInfo(MAIN) << d->syncJob << "is already running"; return; + } + if (!isLoggedIn()) { + qCWarning(MAIN) << "Not logged in, not going to sync"; + return; + } - // Raw string: http://en.cppreference.com/w/cpp/language/string_literal - const auto filter = - QStringLiteral(R"({"room": { "timeline": { "limit": 100 } } })"); - auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest, - d->data->lastEvent(), filter, timeout); - connect( job, &SyncJob::success, this, [this, job] { + d->syncTimeout = timeout; + Filter filter; + filter.room.timeline.limit.emplace(100); + filter.room.state.lazyLoadMembers.emplace(d->lazyLoading); + auto job = d->syncJob = + callApi<SyncJob>(BackgroundRequest, d->data->lastEvent(), filter, + timeout); + connect(job, &SyncJob::success, this, [this, job] { onSyncSuccess(job->takeData()); d->syncJob = nullptr; emit syncDone(); }); - connect( job, &SyncJob::retryScheduled, this, - [this,job] (int retriesTaken, int nextInMilliseconds) - { - emit networkError(job->errorString(), job->rawData(TRIM_RAW_DATA), - retriesTaken, nextInMilliseconds); - }); - connect( job, &SyncJob::failure, this, [this, job] { - d->syncJob = nullptr; - if (job->error() == BaseJob::ContentAccessError) - { + connect(job, &SyncJob::retryScheduled, this, + [this, job](int retriesTaken, int nextInMilliseconds) { + emit networkError(job->errorString(), job->rawDataSample(), + retriesTaken, nextInMilliseconds); + }); + connect(job, &SyncJob::failure, this, [this, job] { + // SyncJob persists with retries on transient errors; if it fails, + // there's likely something serious enough to stop the loop. + stopSync(); + if (job->error() == BaseJob::Unauthorised) { qCWarning(SYNCJOB) - << "Sync job failed with ContentAccessError - login expired?"; - emit loginError(job->errorString(), job->rawData(TRIM_RAW_DATA)); - } - else - emit syncError(job->errorString(), job->rawData(TRIM_RAW_DATA)); + << "Sync job failed with Unauthorised - login expired?"; + emit loginError(job->errorString(), job->rawDataSample()); + } else + emit syncError(job->errorString(), job->rawDataSample()); }); } -void Connection::onSyncSuccess(SyncData &&data) { +void Connection::syncLoop(int timeout) +{ + if (d->syncLoopConnection && d->syncTimeout == timeout) { + qCInfo(MAIN) << "Attempt to run sync loop but there's one already " + "running; nothing will be done"; + return; + } + std::swap(d->syncTimeout, timeout); + if (d->syncLoopConnection) { + qCInfo(MAIN) << "Timeout for next syncs changed from" + << timeout << "to" << d->syncTimeout; + } else { + d->syncLoopConnection = connect(this, &Connection::syncDone, + this, &Connection::syncLoopIteration, + Qt::QueuedConnection); + syncLoopIteration(); // initial sync to start the loop + } +} + +void Connection::syncLoopIteration() +{ + if (isLoggedIn()) + sync(d->syncTimeout); + else + qCInfo(MAIN) << "Logged out, sync loop will stop now"; +} + +QJsonObject toJson(const DirectChatsMap& directChats) +{ + QJsonObject json; + for (auto it = directChats.begin(); it != directChats.end();) { + QJsonArray roomIds; + const auto* user = it.key(); + for (; it != directChats.end() && it.key() == user; ++it) + roomIds.append(*it); + json.insert(user->id(), roomIds); + } + return json; +} + +void Connection::onSyncSuccess(SyncData&& data, bool fromCache) +{ +#ifdef Quotient_E2EE_ENABLED + d->oneTimeKeysCount = data.deviceOneTimeKeysCount(); + if (d->oneTimeKeysCount[SignedCurve25519Key] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() + && !d->isUploadingKeys) { + d->isUploadingKeys = true; + d->olmAccount->generateOneTimeKeys( + d->olmAccount->maxNumberOfOneTimeKeys() / 2 - d->oneTimeKeysCount[SignedCurve25519Key]); + auto keys = d->olmAccount->oneTimeKeys(); + auto job = d->olmAccount->createUploadKeyRequest(keys); + run(job, ForegroundRequest); + connect(job, &BaseJob::success, this, + [this] { d->olmAccount->markKeysAsPublished(); }); + connect(job, &BaseJob::result, this, + [this] { d->isUploadingKeys = false; }); + } + if(d->firstSync) { + d->loadDevicesList(); + d->firstSync = false; + } + + d->consumeDevicesList(data.takeDevicesList()); +#endif // Quotient_E2EE_ENABLED + d->consumeToDeviceEvents(data.takeToDeviceEvents()); d->data->setLastEvent(data.nextBatch()); - for (auto&& roomData: data.takeRoomData()) - { - const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); - if (forgetIdx != -1) - { - d->roomIdsToForget.removeAt(forgetIdx); - if (roomData.joinState == JoinState::Leave) - { - qDebug(MAIN) << "Room" << roomData.roomId + d->consumeRoomData(data.takeRoomData(), fromCache); + d->consumeAccountData(data.takeAccountData()); + d->consumePresenceData(data.takePresenceData()); +#ifdef Quotient_E2EE_ENABLED + if(d->encryptionUpdateRequired) { + d->loadOutdatedUserDevices(); + d->encryptionUpdateRequired = false; + } +#endif +} + +void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, + bool fromCache) +{ + for (auto&& roomData: roomDataList) { + const auto forgetIdx = roomIdsToForget.indexOf(roomData.roomId); + if (forgetIdx != -1) { + roomIdsToForget.removeAt(forgetIdx); + if (roomData.joinState == JoinState::Leave) { + qDebug(MAIN) + << "Room" << roomData.roomId << "has been forgotten, ignoring /sync response for it"; continue; } qWarning(MAIN) << "Room" << roomData.roomId - << "has just been forgotten but /sync returned it in" - << toCString(roomData.joinState) - << "state - suspiciously fast turnaround"; + << "has just been forgotten but /sync returned it in" + << terse << roomData.joinState + << "state - suspiciously fast turnaround"; } - if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) - { - r->updateData(std::move(roomData)); - if (d->firstTimeRooms.removeOne(r)) - emit loadedRoomState(r); + if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) { + pendingStateRoomIds.removeOne(roomData.roomId); + // Update rooms one by one, giving time to update the UI. + QMetaObject::invokeMethod( + r, + [r, rd = std::move(roomData), fromCache] () mutable { + r->updateData(std::move(rd), fromCache); + }, + Qt::QueuedConnection); } - QCoreApplication::processEvents(); } - for (auto&& accountEvent: data.takeAccountData()) - { - if (is<DirectChatEvent>(*accountEvent)) - { - const auto usersToDCs = ptrCast<DirectChatEvent>(move(accountEvent)) - ->usersToDirectChats(); - DirectChatsMap removals = - erase_if(d->directChats, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.key()->id(), it.value()); +} + +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) { + switchOnType(*eventPtr, + [this](const DirectChatEvent& dce) { + // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events + const auto& usersToDCs = dce.usersToDirectChats(); + DirectChatsMap remoteRemovals = + remove_if(directChats, [&usersToDCs, this](auto it) { + return !( + usersToDCs.contains(it.key()->id(), it.value()) + || dcLocalAdditions.contains(it.key(), it.value())); + }); + remove_if(directChatUsers, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.value(), it.key()); }); - erase_if(d->directChatUsers, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.value()->id(), it.key()); - }); - if (MAIN().isDebugEnabled()) - for (auto it = removals.begin(); it != removals.end(); ++it) - qCDebug(MAIN) << it.value() - << "is no more a direct chat with" << it.key()->id(); - - DirectChatsMap additions; - for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) - { - if (auto* u = user(it.key())) - { - if (!d->directChats.contains(u, it.value())) - { - Q_ASSERT(!d->directChatUsers.contains(it.value(), u)); - additions.insert(u, it.value()); - d->directChats.insert(u, it.value()); - d->directChatUsers.insert(it.value(), u); - qCDebug(MAIN) << "Marked room" << it.value() - << "as a direct chat with" << u->id(); + // Remove from dcLocalRemovals what the server already has. + remove_if(dcLocalRemovals, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.key(), it.value()); + }); + if (MAIN().isDebugEnabled()) + for (auto it = remoteRemovals.begin(); + it != remoteRemovals.end(); ++it) { + qCDebug(MAIN) + << it.value() << "is no more a direct chat with" + << it.key()->id(); } - } else - qCWarning(MAIN) + + DirectChatsMap remoteAdditions; + for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) { + if (auto* u = q->user(it.key())) { + if (!directChats.contains(u, it.value()) + && !dcLocalRemovals.contains(u, it.value())) { + Q_ASSERT(!directChatUsers.contains(it.value(), u)); + remoteAdditions.insert(u, it.value()); + directChats.insert(u, it.value()); + directChatUsers.insert(it.value(), u); + qCDebug(MAIN) << "Marked room" << it.value() + << "as a direct chat with" << u->id(); + } + } else + qCWarning(MAIN) << "Couldn't get a user object for" << it.key(); - } - if (!additions.isEmpty() || !removals.isEmpty()) - emit directChatsListChanged(additions, removals); + } + // Remove from dcLocalAdditions what the server already has. + remove_if(dcLocalAdditions, [&remoteAdditions](auto it) { + return remoteAdditions.contains(it.key(), it.value()); + }); + if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) + emit q->directChatsListChanged(remoteAdditions, + remoteRemovals); + }, + // catch-all, passing eventPtr for a possible take-over + [this, &eventPtr](const Event& accountEvent) { + if (is<IgnoredUsersEvent>(accountEvent)) + qCDebug(MAIN) + << "Users ignored by" << data->userId() << "updated:" + << QStringList(q->ignoredUsers().values()).join(','); + + auto& currentData = accountData[accountEvent.matrixType()]; + // A polymorphic event-specific comparison might be a bit + // more efficient; maaybe do it another day + if (!currentData + || currentData->contentJson() != accountEvent.contentJson()) { + currentData = std::move(eventPtr); + qCDebug(MAIN) << "Updated account data of type" + << currentData->matrixType(); + emit q->accountDataChanged(currentData->matrixType()); + } + }); + } + if (!dcLocalAdditions.isEmpty() || !dcLocalRemovals.isEmpty()) { + qDebug(MAIN) << "Sending updated direct chats to the server:" + << dcLocalRemovals.size() << "removal(s)," + << dcLocalAdditions.size() << "addition(s)"; + q->callApi<SetAccountDataJob>(data->userId(), QStringLiteral("m.direct"), + toJson(directChats)); + dcLocalAdditions.clear(); + dcLocalRemovals.clear(); + } +} - continue; +void Connection::Private::consumePresenceData(Events&& presenceData) +{ + // To be implemented +} + +void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) +{ +#ifdef Quotient_E2EE_ENABLED + if (!toDeviceEvents.empty()) { + qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() + << "to-device events"; + for (auto&& tdEvt : toDeviceEvents) { + if (processIfVerificationEvent(*tdEvt, false)) + continue; + if (auto&& event = eventCast<EncryptedEvent>(std::move(tdEvt))) { + if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) << "Unsupported algorithm" << event->id() + << "for event" << event->algorithm(); + return; + } + if (isKnownCurveKey(event->senderId(), event->senderKey())) { + handleEncryptedToDeviceEvent(*event); + return; + } + trackedUsers += event->senderId(); + outdatedUsers += event->senderId(); + encryptionUpdateRequired = true; + pendingEncryptedEvents.push_back(std::move(event)); + } } - if (is<IgnoredUsersEvent>(*accountEvent)) - qCDebug(MAIN) << "Users ignored by" << d->userId << "updated:" - << QStringList::fromSet(ignoredUsers()).join(','); - - auto& currentData = d->accountData[accountEvent->matrixType()]; - // A polymorphic event-specific comparison might be a bit more - // efficient; maaybe do it another day - if (!currentData || - currentData->contentJson() != accountEvent->contentJson()) - { - currentData = std::move(accountEvent); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); - emit accountDataChanged(currentData->matrixType()); + } +#endif +} + +#ifdef Quotient_E2EE_ENABLED +bool Connection::Private::processIfVerificationEvent(const Event& evt, + bool encrypted) +{ + return switchOnType(evt, + [this, encrypted](const KeyVerificationRequestEvent& reqEvt) { + const auto sessionIter = verificationSessions.insert( + reqEvt.transactionId(), + new KeyVerificationSession(q->userId(), reqEvt, q, encrypted)); + emit q->newKeyVerificationSession(*sessionIter); + return true; + }, + [](const KeyVerificationDoneEvent&) { + return true; + }, + [this](const KeyVerificationEvent& kvEvt) { + if (auto* const session = + verificationSessions.value(kvEvt.transactionId())) { + session->handleEvent(kvEvt); + emit q->keyVerificationStateChanged(session, session->state()); + } + return true; + }, + false); +} + +void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) +{ + const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } + + if (processIfVerificationEvent(*decryptedEvent, true)) + return; + switchOnType(*decryptedEvent, + [this, &event, + olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), + olmSessionId); + } else { + qCDebug(E2EE) + << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + } + }, + [](const Event& evt) { + qCWarning(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); + }); +} +#endif + +void Connection::Private::consumeDevicesList(DevicesList&& devicesList) +{ +#ifdef Quotient_E2EE_ENABLED + bool hasNewOutdatedUser = false; + for(const auto &changed : devicesList.changed) { + if(trackedUsers.contains(changed)) { + outdatedUsers += changed; + hasNewOutdatedUser = true; } } + for(const auto &left : devicesList.left) { + trackedUsers -= left; + outdatedUsers -= left; + deviceKeys.remove(left); + } + if(hasNewOutdatedUser) { + loadOutdatedUserDevices(); + } +#endif } void Connection::stopSync() { - if (d->syncJob) + // If there's a sync loop, break it + disconnect(d->syncLoopConnection); + if (d->syncJob) // If there's an ongoing sync job, stop it too { - d->syncJob->abandon(); + if (d->syncJob->status().code == BaseJob::Pending) + d->syncJob->abandon(); d->syncJob = nullptr; } } -PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const -{ - return callApi<PostReceiptJob>(room->id(), "m.read", event->id()); -} +QString Connection::nextBatchToken() const { return d->data->lastEvent(); } JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { - auto job = callApi<JoinRoomJob>(roomAlias, serverNames); - connect(job, &JoinRoomJob::success, - this, [this, job] { provideRoom(job->roomId(), JoinState::Join); }); + 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()); + }); return job; } -void Connection::leaveRoom(Room* room) +LeaveRoomJob* Connection::leaveRoom(Room* room) { - callApi<LeaveRoomJob>(room->id()); + const auto& roomId = room->id(); + const auto job = callApi<LeaveRoomJob>(roomId); + if (room->joinState() == JoinState::Invite) { + // Workaround matrix-org/synapse#2181 - if the room is in invite state + // the invite may have been cancelled but Synapse didn't send it in + // `/sync`. See also #273 for the discussion in the library context. + d->pendingStateRoomIds.push_back(roomId); + connect(job, &LeaveRoomJob::success, this, [this, roomId] { + if (d->pendingStateRoomIds.removeOne(roomId)) { + qCDebug(MAIN) << "Forcing the room to Leave status"; + provideRoom(roomId, JoinState::Leave); + } + }); + } + return job; } inline auto splitMediaId(const QString& mediaId) { auto idParts = mediaId.split('/'); Q_ASSERT_X(idParts.size() == 2, __FUNCTION__, - ("'" + mediaId + - "' doesn't look like 'serverName/localMediaId'").toLatin1()); + ("'" + mediaId + "' doesn't look like 'serverName/localMediaId'") + .toLatin1()); 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) const + QSize requestedSize, + RunningPolicy policy) { auto idParts = splitMediaId(mediaId); - return callApi<MediaThumbnailJob>(policy, - idParts.front(), idParts.back(), requestedSize); + return callApi<MediaThumbnailJob>(policy, idParts.front(), idParts.back(), + requestedSize); } -MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, - QSize requestedSize, RunningPolicy policy) const +MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize, + RunningPolicy policy) { return getThumbnail(url.authority() + url.path(), requestedSize, policy); } -MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, - int requestedWidth, int requestedHeight, RunningPolicy policy) const +MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth, + int requestedHeight, + RunningPolicy policy) { return getThumbnail(url, QSize(requestedWidth, requestedHeight), policy); } -UploadContentJob* Connection::uploadContent(QIODevice* contentSource, - const QString& filename, const QString& contentType) const +UploadContentJob* +Connection::uploadContent(QIODevice* contentSource, const QString& filename, + const QString& overrideContentType) { + Q_ASSERT(contentSource != nullptr); + auto contentType = overrideContentType; + if (contentType.isEmpty()) { + contentType = QMimeDatabase() + .mimeTypeForFileNameAndData(filename, contentSource) + .name(); + if (!contentSource->open(QIODevice::ReadOnly)) { + qCWarning(MAIN) << "Couldn't open content source" << filename + << "for reading:" << contentSource->errorString(); + return nullptr; + } + } return callApi<UploadContentJob>(contentSource, filename, contentType); } UploadContentJob* Connection::uploadFile(const QString& fileName, - const QString& contentType) + const QString& overrideContentType) { auto sourceFile = new QFile(fileName); - if (!sourceFile->open(QIODevice::ReadOnly)) - { - qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName() - << "for reading"; - return nullptr; - } return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), - contentType); + overrideContentType); } -GetContentJob* Connection::getContent(const QString& mediaId) const +GetContentJob* Connection::getContent(const QString& mediaId) { auto idParts = splitMediaId(mediaId); return callApi<GetContentJob>(idParts.front(), idParts.back()); } -GetContentJob* Connection::getContent(const QUrl& url) const +GetContentJob* Connection::getContent(const QUrl& url) { return getContent(url.authority() + url.path()); } DownloadFileJob* Connection::downloadFile(const QUrl& url, - const QString& localFilename) const + const QString& localFilename) { auto mediaId = url.authority() + url.path(); auto idParts = splitMediaId(mediaId); - auto* job = callApi<DownloadFileJob>(idParts.front(), idParts.back(), - localFilename); + auto* job = + callApi<DownloadFileJob>(idParts.front(), idParts.back(), localFilename); return job; } -CreateRoomJob* Connection::createRoom(RoomVisibility visibility, - const QString& alias, const QString& name, const QString& topic, - QStringList invites, const QString& presetName, bool isDirect, - const QVector<CreateRoomJob::StateEvent>& initialState, - const QVector<CreateRoomJob::Invite3pid>& invite3pids, - const QJsonObject& creationContent) -{ - invites.removeOne(d->userId); // The creator is by definition in the room - auto job = callApi<CreateRoomJob>( - visibility == PublishRoom ? QStringLiteral("public") - : QStringLiteral("private"), - alias, name, topic, invites, invite3pids, QString(/*TODO: #233*/), - creationContent, initialState, presetName, isDirect); - connect(job, &BaseJob::success, this, [this,job] { - emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob* Connection::downloadFile( + const QUrl& url, const EncryptedFileMetadata& fileMetadata, + const QString& localFilename) +{ + auto mediaId = url.authority() + url.path(); + auto idParts = splitMediaId(mediaId); + return callApi<DownloadFileJob>(idParts.front(), idParts.back(), + fileMetadata, localFilename); +} +#endif + +CreateRoomJob* +Connection::createRoom(RoomVisibility visibility, const QString& alias, + const QString& name, const QString& topic, + QStringList invites, const QString& presetName, + const QString& roomVersion, bool isDirect, + const QVector<CreateRoomJob::StateEvent>& initialState, + const QVector<CreateRoomJob::Invite3pid>& invite3pids, + const QJsonObject& creationContent) +{ + invites.removeOne(userId()); // The creator is by definition in the room + auto job = callApi<CreateRoomJob>(visibility == PublishRoom + ? QStringLiteral("public") + : QStringLiteral("private"), + alias, name, topic, invites, invite3pids, + roomVersion, creationContent, + initialState, presetName, isDirect); + connect(job, &BaseJob::success, this, [this, job, invites, isDirect] { + auto* room = provideRoom(job->roomId(), JoinState::Join); + if (!room) { + Q_ASSERT_X(room, "Connection::createRoom", + "Failed to create a room"); + return; + } + emit createdRoom(room); + if (isDirect) + for (const auto& i : invites) + addToDirectChats(room, user(i)); }); return job; } void Connection::requestDirectChat(const QString& userId) { - if (auto* u = user(userId)) - requestDirectChat(u); - else - qCCritical(MAIN) - << "Connection::requestDirectChat: Couldn't get a user object for" - << userId; + doInDirectChat(userId, [this](Room* r) { emit directChatAvailable(r); }); } void Connection::requestDirectChat(User* u) { - doInDirectChat(u, [this] (Room* r) { emit directChatAvailable(r); }); + doInDirectChat(u, [this](Room* r) { emit directChatAvailable(r); }); } void Connection::doInDirectChat(const QString& userId, @@ -555,34 +1270,33 @@ void Connection::doInDirectChat(User* u, const std::function<void(Room*)>& operation) { Q_ASSERT(u); - const auto& userId = u->id(); - // There can be more than one DC; find the first valid, and delete invalid - // (left/forgotten) ones along the way. + const auto& otherUserId = u->id(); + // There can be more than one DC; find the first valid (existing and + // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; - for (auto it = d->directChats.find(u); - it != d->directChats.end() && it.key() == u; ++it) - { + for (auto it = d->directChats.constFind(u); + it != d->directChats.cend() && it.key() == u; ++it) { const auto& roomId = *it; - if (auto r = room(roomId, JoinState::Join)) - { + if (auto r = room(roomId, JoinState::Join)) { Q_ASSERT(r->id() == roomId); // A direct chat with yourself should only involve yourself :) - if (userId == d->userId && r->memberCount() > 1) + if (otherUserId == userId() && r->totalMemberCount() > 1) continue; - qCDebug(MAIN) << "Requested direct chat with" << userId + qCDebug(MAIN) << "Requested direct chat with" << otherUserId << "is already available as" << r->id(); operation(r); return; } - if (auto ir = invitation(roomId)) - { + if (auto ir = invitation(roomId)) { Q_ASSERT(ir->id() == roomId); auto j = joinRoom(ir->id()); - connect(j, &BaseJob::success, this, [this,roomId,userId,operation] { - qCDebug(MAIN) << "Joined the already invited direct chat with" - << userId << "as" << roomId; - operation(room(roomId, JoinState::Join)); - }); + connect(j, &BaseJob::success, this, + [this, roomId, otherUserId, operation] { + qCDebug(MAIN) + << "Joined the already invited direct chat with" + << otherUserId << "as" << roomId; + operation(room(roomId, JoinState::Join)); + }); return; } // Avoid reusing previously left chats but don't remove them @@ -590,36 +1304,36 @@ void Connection::doInDirectChat(User* u, if (room(roomId, JoinState::Leave)) continue; - qCWarning(MAIN) << "Direct chat with" << userId << "known as room" + qCWarning(MAIN) << "Direct chat with" << otherUserId << "known as room" << roomId << "is not valid and will be discarded"; // Postpone actual deletion until we finish iterating d->directChats. removals.insert(it.key(), it.value()); + // Add to the list of updates to send to the server upon the next sync. + d->dcLocalRemovals.insert(it.key(), it.value()); } - if (!removals.isEmpty()) - { - for (auto it = removals.cbegin(); it != removals.cend(); ++it) - { + if (!removals.isEmpty()) { + for (auto it = removals.cbegin(); it != removals.cend(); ++it) { d->directChats.remove(it.key(), it.value()); d->directChatUsers.remove(it.value(), const_cast<User*>(it.key())); // FIXME } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } - auto j = createDirectChat(userId); - connect(j, &BaseJob::success, this, [this,j,userId,operation] { - qCDebug(MAIN) << "Direct chat with" << userId - << "has been created as" << j->roomId(); + auto j = createDirectChat(otherUserId); + connect(j, &BaseJob::success, this, [this, j, otherUserId, operation] { + qCDebug(MAIN) << "Direct chat with" << otherUserId << "has been created as" + << j->roomId(); operation(room(j->roomId(), JoinState::Join)); }); - } CreateRoomJob* Connection::createDirectChat(const QString& userId, - const QString& topic, const QString& name) + const QString& topic, + const QString& name) { - return createRoom(UnpublishRoom, "", name, topic, {userId}, - "trusted_private_chat", true); + return createRoom(UnpublishRoom, {}, name, topic, { userId }, + QStringLiteral("trusted_private_chat"), {}, true); } ForgetRoomJob* Connection::forgetRoom(const QString& id) @@ -632,164 +1346,212 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) // a ForgetRoomJob is created in advance and can be returned in a probably // not-yet-started state (it will start once /leave completes). auto forgetJob = new ForgetRoomJob(id); - auto room = d->roomMap.value({id, false}); + auto room = d->roomMap.value({ id, false }); if (!room) - room = d->roomMap.value({id, true}); - if (room && room->joinState() != JoinState::Leave) - { - auto leaveJob = room->leaveRoom(); - connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] { - forgetJob->start(connectionData()); - // If the matching /sync response hasn't arrived yet, mark the room - // for explicit deletion - if (room->joinState() != JoinState::Leave) - d->roomIdsToForget.push_back(room->id()); - }); + room = d->roomMap.value({ id, true }); + if (room && room->joinState() != JoinState::Leave) { + auto leaveJob = leaveRoom(room); + connect(leaveJob, &BaseJob::result, this, + [this, leaveJob, forgetJob, room] { + if (leaveJob->error() == BaseJob::Success + || leaveJob->error() == BaseJob::NotFound) { + run(forgetJob); + // If the matching /sync response hasn't arrived yet, + // mark the room for explicit deletion + if (room->joinState() != JoinState::Leave) + d->roomIdsToForget.push_back(room->id()); + } else { + qCWarning(MAIN).nospace() + << "Error leaving room " << room->objectName() + << ": " << leaveJob->errorString(); + forgetJob->abandon(); + } + }); connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon); - } - else - forgetJob->start(connectionData()); - connect(forgetJob, &BaseJob::success, this, [this, id] - { - // Delete whatever instances of the room are still in the map. - for (auto f: {false, true}) - if (auto r = d->roomMap.take({ id, f })) - { - qCDebug(MAIN) << "Room" << r->objectName() - << "in state" << toCString(r->joinState()) - << "will be deleted"; - emit r->beforeDestruction(r); - r->deleteLater(); - } + } else + run(forgetJob); + 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::NotFound) + d->removeRoom(id); // Delete the room from roomMap + else + qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " + << forgetJob->errorString(); }); return forgetJob; } -SendToDeviceJob* Connection::sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap) const +SendToDeviceJob* Connection::sendToDevices( + const QString& eventType, const UsersToDevicesToContent& contents) { - QHash<QString, QHash<QString, QJsonObject>> json; - json.reserve(int(eventsMap.size())); - std::for_each(eventsMap.begin(), eventsMap.end(), - [&json] (const auto& userTodevicesToEvents) { - auto& jsonUser = json[userTodevicesToEvents.first]; - const auto& devicesToEvents = userTodevicesToEvents.second; - std::for_each(devicesToEvents.begin(), devicesToEvents.end(), - [&jsonUser] (const auto& deviceToEvents) { - jsonUser.insert(deviceToEvents.first, - deviceToEvents.second.contentJson()); - }); - }); - return callApi<SendToDeviceJob>(BackgroundRequest, - eventType, generateTxnId(), json); + return callApi<SendToDeviceJob>(BackgroundRequest, eventType, + generateTxnId(), contents); } SendMessageJob* Connection::sendMessage(const QString& roomId, - const RoomEvent& event) const + const RoomEvent& event) +{ + const auto txnId = event.transactionId().isEmpty() ? generateTxnId() + : event.transactionId(); + return callApi<SendMessageJob>(roomId, event.matrixType(), txnId, + event.contentJson()); +} + +QUrl Connection::homeserver() const { return d->data->baseUrl(); } + +QString Connection::domain() const { return userId().section(':', 1); } + +bool Connection::isUsable() const { return !loginFlows().isEmpty(); } + +QVector<GetLoginFlowsJob::LoginFlow> Connection::loginFlows() const { - const auto txnId = event.transactionId().isEmpty() - ? generateTxnId() : event.transactionId(); - return callApi<SendMessageJob>(roomId, event.matrixType(), - txnId, event.contentJson()); + return d->loginFlows; } -QUrl Connection::homeserver() const +bool Connection::supportsPasswordAuth() const { - return d->data->baseUrl(); + return d->loginFlows.contains(LoginFlows::Password); +} + +bool Connection::supportsSso() const +{ + return d->loginFlows.contains(LoginFlows::SSO); } Room* Connection::room(const QString& roomId, JoinStates states) const { - Room* room = d->roomMap.value({roomId, false}, nullptr); - if (states.testFlag(JoinState::Join) && - room && room->joinState() == JoinState::Join) + Room* room = d->roomMap.value({ roomId, false }, nullptr); + if (states.testFlag(JoinState::Join) && room + && room->joinState() == JoinState::Join) return room; if (states.testFlag(JoinState::Invite)) if (Room* invRoom = invitation(roomId)) return invRoom; - if (states.testFlag(JoinState::Leave) && - room && room->joinState() == JoinState::Leave) + if (states.testFlag(JoinState::Leave) && room + && room->joinState() == JoinState::Leave) return room; return nullptr; } +Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const +{ + const auto id = d->roomAliasMap.value(roomAlias); + if (!id.isEmpty()) + return room(id, states); + + qCWarning(MAIN) << "Room for alias" << roomAlias + << "is not found under account" << userId(); + return nullptr; +} + +void Connection::updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases) +{ + for (const auto& a : previousRoomAliases) + if (d->roomAliasMap.remove(a) == 0) + qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; + + for (const auto& a : roomAliases) { + auto& mappedId = d->roomAliasMap[a]; + if (!mappedId.isEmpty()) { + if (mappedId == roomId) + qCDebug(MAIN) + << "Alias" << a << "is already mapped to" << roomId; + else + qCWarning(MAIN) << "Alias" << a << "will be force-remapped from" + << mappedId << "to" << roomId; + } + mappedId = roomId; + } +} + Room* Connection::invitation(const QString& roomId) const { - return d->roomMap.value({roomId, true}, nullptr); + return d->roomMap.value({ roomId, true }, nullptr); } -User* Connection::user(const QString& userId) +User* Connection::user(const QString& uId) { - if (userId.isEmpty()) + if (uId.isEmpty()) return nullptr; - if (!userId.startsWith('@') || !userId.contains(':')) - { - qCCritical(MAIN) << "Malformed userId:" << userId; + if (const auto v = d->userMap.value(uId, nullptr)) + return v; + // Before creating a user object, check that the user id is well-formed + // (it's faster to just do a lookup above before validation) + if (!uId.startsWith('@') || serverPart(uId).isEmpty()) { + qCCritical(MAIN) << "Malformed userId:" << uId; return nullptr; } - if( d->userMap.contains(userId) ) - return d->userMap.value(userId); - auto* user = userFactory()(this, userId); - d->userMap.insert(userId, user); + auto* user = userFactory()(this, uId); + d->userMap.insert(uId, user); emit newUser(user); return user; } const User* Connection::user() const { - return d->userMap.value(d->userId, nullptr); + return d->userMap.value(userId(), nullptr); } -User* Connection::user() -{ - return user(d->userId); -} +User* Connection::user() { return user(userId()); } -QString Connection::userId() const -{ - return d->userId; -} +QString Connection::userId() const { return d->data->userId(); } -QString Connection::deviceId() const +QString Connection::deviceId() const { return d->data->deviceId(); } + +QByteArray Connection::accessToken() const { - return d->data->deviceId(); + // The logout job needs access token to do its job; so the token is + // kept inside d->data but no more exposed to the outside world. + return isJobPending(d->logoutJob) ? QByteArray() : d->data->accessToken(); } -QString Connection::token() const +bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } + +#ifdef Quotient_E2EE_ENABLED +QOlmAccount *Connection::olmAccount() const { - return accessToken(); + return d->olmAccount.get(); } +#endif // Quotient_E2EE_ENABLED -QByteArray Connection::accessToken() const +SyncJob* Connection::syncJob() const { return d->syncJob; } + +int Connection::millisToReconnect() const { - return d->data->accessToken(); + return d->syncJob ? d->syncJob->millisToRetry() : 0; } -SyncJob* Connection::syncJob() const +QVector<Room*> Connection::allRooms() const { - return d->syncJob; + QVector<Room*> result; + result.resize(d->roomMap.size()); + std::copy(d->roomMap.cbegin(), d->roomMap.cend(), result.begin()); + return result; } -int Connection::millisToReconnect() const +QVector<Room*> Connection::rooms(JoinStates joinStates) const { - return d->syncJob ? d->syncJob->millisToRetry() : 0; + QVector<Room*> result; + for (auto* r: qAsConst(d->roomMap)) + if (joinStates.testFlag(r->joinState())) + result.push_back(r); + return result; } -QHash< QPair<QString, bool>, Room* > Connection::roomMap() const +int Connection::roomsCount(JoinStates joinStates) 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; + // Using int to maintain compatibility with QML + // (consider also that QHash<>::size() returns int anyway). + return int(std::count_if(d->roomMap.cbegin(), d->roomMap.cend(), + [joinStates](Room* r) { + return joinStates.testFlag(r->joinState()); + })); } bool Connection::hasAccountData(const QString& type) const @@ -823,26 +1585,27 @@ void Connection::setAccountData(const QString& type, const QJsonObject& content) QHash<QString, QVector<Room*>> Connection::tagsToRooms() const { QHash<QString, QVector<Room*>> result; - for (auto* r: qAsConst(d->roomMap)) - { - for (const auto& tagName: r->tagNames()) + for (auto* r : qAsConst(d->roomMap)) { + const auto& tagNames = r->tagNames(); + for (const auto& tagName : tagNames) result[tagName].push_back(r); } for (auto it = result.begin(); it != result.end(); ++it) - std::sort(it->begin(), it->end(), - [t=it.key()] (Room* r1, Room* r2) { - return r1->tags().value(t) < r2->tags().value(t); - }); + std::sort(it->begin(), it->end(), [t = it.key()](Room* r1, Room* r2) { + return r1->tags().value(t) < r2->tags().value(t); + }); return result; } QStringList Connection::tagNames() const { - QStringList tags ({FavouriteTag}); - for (auto* r: qAsConst(d->roomMap)) - for (const auto& tag: r->tagNames()) + QStringList tags({ FavouriteTag }); + for (auto* r : qAsConst(d->roomMap)) { + const auto& tagNames = r->tagNames(); + for (const auto& tag : tagNames) if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); + } tags.push_back(LowPriorityTag); return tags; } @@ -850,36 +1613,27 @@ QStringList Connection::tagNames() const QVector<Room*> Connection::roomsWithTag(const QString& tagName) const { QVector<Room*> rooms; - std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms), - [&tagName] (Room* r) { return r->tags().contains(tagName); }); + std::copy_if(d->roomMap.cbegin(), d->roomMap.cend(), + std::back_inserter(rooms), + [&tagName](Room* r) { return r->tags().contains(tagName); }); return rooms; } -Connection::DirectChatsMap Connection::directChats() const +DirectChatsMap Connection::directChats() const { return d->directChats; } -QJsonObject toJson(const Connection::DirectChatsMap& directChats) -{ - QJsonObject json; - for (auto it = directChats.begin(); it != directChats.end();) - { - QJsonArray roomIds; - const auto* user = it.key(); - for (; it != directChats.end() && it.key() == user; ++it) - roomIds.append(*it); - json.insert(user->id(), roomIds); - } - return json; -} - -void Connection::Private::broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals) +// Removes room with given id from roomMap +void Connection::Private::removeRoom(const QString& roomId) { - q->callApi<SetAccountDataJob>(userId, QStringLiteral("m.direct"), - toJson(directChats)); - emit q->directChatsListChanged(additions, removals); + for (auto f : { false, true }) + if (auto r = roomMap.take({ roomId, f })) { + qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse + << r->joinState() << "will be deleted"; + emit r->beforeDestruction(r); + r->deleteLater(); + } } void Connection::addToDirectChats(const Room* room, User* user) @@ -890,29 +1644,30 @@ void Connection::addToDirectChats(const Room* room, User* user) Q_ASSERT(!d->directChatUsers.contains(room->id(), user)); d->directChats.insert(user, room->id()); d->directChatUsers.insert(room->id(), user); - DirectChatsMap additions { { user, room->id() } }; - d->broadcastDirectChatUpdates(additions, {}); + d->dcLocalAdditions.insert(user, room->id()); + emit directChatsListChanged({ { user, room->id() } }, {}); } void Connection::removeFromDirectChats(const QString& roomId, User* user) { Q_ASSERT(!roomId.isEmpty()); - if ((user != nullptr && !d->directChats.contains(user, roomId)) || - d->directChats.key(roomId) == nullptr) + if ((user != nullptr && !d->directChats.contains(user, roomId)) + || d->directChats.key(roomId) == nullptr) return; DirectChatsMap removals; - if (user != nullptr) - { - removals.insert(user, roomId); + if (user != nullptr) { d->directChats.remove(user, roomId); d->directChatUsers.remove(roomId, user); + removals.insert(user, roomId); + d->dcLocalRemovals.insert(user, roomId); } else { - removals = erase_if(d->directChats, - [&roomId] (auto it) { return it.value() == roomId; }); + removals = remove_if(d->directChats, + [&roomId](auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); + d->dcLocalRemovals += removals; } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } bool Connection::isDirectChat(const QString& roomId) const @@ -931,10 +1686,10 @@ bool Connection::isIgnored(const User* user) const return ignoredUsers().contains(user->id()); } -Connection::IgnoredUsersList Connection::ignoredUsers() const +IgnoredUsersList Connection::ignoredUsers() const { - const auto* event = d->unpackAccountData<IgnoredUsersEvent>(); - return event ? event->ignored_users() : IgnoredUsersList(); + const auto* event = accountData<IgnoredUsersEvent>(); + return event ? event->ignoredUsers() : IgnoredUsersList(); } void Connection::addToIgnoredUsers(const User* user) @@ -942,11 +1697,10 @@ void Connection::addToIgnoredUsers(const User* user) Q_ASSERT(user != nullptr); auto ignoreList = ignoredUsers(); - if (!ignoreList.contains(user->id())) - { + if (!ignoreList.contains(user->id())) { ignoreList.insert(user->id()); d->packAndSendAccountData<IgnoredUsersEvent>(ignoreList); - emit ignoredUsersListChanged({{ user->id() }}, {}); + emit ignoredUsersListChanged({ { user->id() } }, {}); } } @@ -955,70 +1709,81 @@ void Connection::removeFromIgnoredUsers(const User* user) Q_ASSERT(user != nullptr); auto ignoreList = ignoredUsers(); - if (ignoreList.remove(user->id()) != 0) - { + if (ignoreList.remove(user->id()) != 0) { d->packAndSendAccountData<IgnoredUsersEvent>(ignoreList); - emit ignoredUsersListChanged({}, {{ user->id() }}); + emit ignoredUsersListChanged({}, { { user->id() } }); } } -QMap<QString, User*> Connection::users() const -{ - return d->userMap; -} +QMap<QString, User*> Connection::users() const { return d->userMap; } const ConnectionData* Connection::connectionData() const { return d->data.get(); } -Room* Connection::provideRoom(const QString& id, JoinState joinState) +Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState) { // TODO: This whole function is a strong case for a RoomManager class. Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); - const auto roomKey = qMakePair(id, joinState == JoinState::Invite); + // If joinState is empty, all joinState == comparisons below are false. + const std::pair roomKey { id, joinState == JoinState::Invite }; auto* room = d->roomMap.value(roomKey, nullptr); - if (room) - { + if (room) { // Leave is a special case because in transition (5a) (see the .h file) // joinState == room->joinState but we still have to preempt the Invite // and emit a signal. For Invite and Join, there's no such problem. if (room->joinState() == joinState && joinState != JoinState::Leave) return room; + } else if (!joinState) { + // No Join and Leave, maybe Invite? + room = d->roomMap.value({ id, true }, nullptr); + if (room) + return room; + // No Invite either, setup a new room object in Join state + joinState = JoinState::Join; } - else - { - room = roomFactory()(this, id, joinState); - if (!room) - { + + if (!room) { + Q_ASSERT(joinState.has_value()); + room = roomFactory()(this, id, *joinState); + if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; return nullptr; } d->roomMap.insert(roomKey, room); - d->firstTimeRooms.push_back(room); - connect(room, &Room::beforeDestruction, - this, &Connection::aboutToDeleteRoom); + connect(room, &Room::beforeDestruction, this, + &Connection::aboutToDeleteRoom); + connect(room, &Room::baseStateLoaded, this, [this, room] { + emit loadedRoomState(room); + if (d->capabilities.roomVersions) + room->checkVersion(); + // Otherwise, the version will be checked in reloadCapabilities() + }); emit newRoom(room); } - if (joinState == JoinState::Invite) - { + if (!joinState) + return room; + + if (*joinState == JoinState::Invite) { // prev is either Leave or nullptr - auto* prev = d->roomMap.value({id, false}, nullptr); + auto* prev = d->roomMap.value({ id, false }, nullptr); emit invitedRoom(room, prev); - } - else - { - room->setJoinState(joinState); + } else { + room->setJoinState(*joinState); // Preempt the Invite room (if any) with a room in Join/Leave state. - auto* prevInvite = d->roomMap.take({id, true}); - if (joinState == JoinState::Join) + auto* prevInvite = d->roomMap.take({ id, true }); + if (*joinState == JoinState::Join) emit joinedRoom(room, prevInvite); - else if (joinState == JoinState::Leave) + else if (*joinState == JoinState::Leave) emit leftRoom(room, prevInvite); - if (prevInvite) - { - qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); + if (prevInvite) { + const auto dcUsers = prevInvite->directChatUsers(); + for (auto* u : dcUsers) + addToDirectChats(room, u); + qCDebug(MAIN) << "Deleting Invite state for room" + << prevInvite->id(); emit prevInvite->beforeDestruction(prevInvite); prevInvite->deleteLater(); } @@ -1037,18 +1802,12 @@ void Connection::setUserFactory(user_factory_t f) _userFactory = std::move(f); } -room_factory_t Connection::roomFactory() -{ - return _roomFactory; -} +room_factory_t Connection::roomFactory() { return _roomFactory; } -user_factory_t Connection::userFactory() -{ - return _userFactory; -} +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 { @@ -1057,163 +1816,672 @@ QByteArray Connection::generateTxnId() const void Connection::setHomeserver(const QUrl& url) { - if (homeserver() == url) - return; + if (isJobPending(d->resolverJob)) + d->resolverJob->abandon(); + if (isJobPending(d->loginFlowsJob)) + d->loginFlowsJob->abandon(); + d->loginFlows.clear(); - d->data->setBaseUrl(url); - emit homeserverChanged(homeserver()); -} + if (homeserver() != url) { + d->data->setBaseUrl(url); + emit homeserverChanged(homeserver()); + } -static constexpr int CACHE_VERSION_MAJOR = 8; -static constexpr int CACHE_VERSION_MINOR = 0; + // Whenever a homeserver is updated, retrieve available login flows from it + d->loginFlowsJob = callApi<GetLoginFlowsJob>(BackgroundRequest); + connect(d->loginFlowsJob, &BaseJob::result, this, [this] { + if (d->loginFlowsJob->status().good()) + d->loginFlows = d->loginFlowsJob->flows(); + else + d->loginFlows.clear(); + emit loginFlowsChanged(); + }); +} -void Connection::saveState(const QUrl &toFile) const +void Connection::saveRoomState(Room* r) const { + Q_ASSERT(r); if (!d->cacheState) return; - QElapsedTimer et; et.start(); + QFile outRoomFile { stateCacheDir().filePath( + SyncData::fileNameForRoom(r->id())) }; + if (outRoomFile.open(QFile::WriteOnly)) { + const auto data = + d->cacheToBinary + ? QCborValue::fromJsonValue(r->toJson()).toCbor() + : QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact); + outRoomFile.write(data.data(), data.size()); + qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); + } else { + qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() << ":" + << outRoomFile.errorString(); + } +} - QFileInfo stateFile { - toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() - }; - if (!stateFile.dir().exists()) - stateFile.dir().mkpath("."); +void Connection::saveState() const +{ + if (!d->cacheState) + return; - QFile outfile { stateFile.absoluteFilePath() }; - if (!outfile.open(QFile::WriteOnly)) - { - qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath() - << ":" << outfile.errorString(); + QElapsedTimer et; + et.start(); + + QFile outFile { d->topLevelStatePath() }; + if (!outFile.open(QFile::WriteOnly)) { + qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":" + << outFile.errorString(); qCWarning(MAIN) << "Caching the rooms state disabled"; d->cacheState = false; return; } - QJsonObject rootObj; + QJsonObject rootObj { + { QStringLiteral("cache_version"), + QJsonObject { + { QStringLiteral("major"), SyncData::cacheVersion().first }, + { QStringLiteral("minor"), SyncData::cacheVersion().second } } } + }; { - QJsonObject rooms; - QJsonObject inviteRooms; - for (const auto* i : roomMap()) // Pass on rooms in Leave state - { - if (i->joinState() == JoinState::Invite) - inviteRooms.insert(i->id(), i->toJson()); - else - rooms.insert(i->id(), i->toJson()); - QElapsedTimer et1; et1.start(); - QCoreApplication::processEvents(); - if (et1.elapsed() > 1) - qCDebug(PROFILER) << "processEvents() borrowed" << et1; + QJsonObject roomsJson; + QJsonObject inviteRoomsJson; + for (const auto* r: qAsConst(d->roomMap)) { + if (r->joinState() == JoinState::Leave) + continue; + (r->joinState() == JoinState::Invite ? inviteRoomsJson : roomsJson) + .insert(r->id(), QJsonValue::Null); } QJsonObject roomObj; - if (!rooms.isEmpty()) - roomObj.insert("join", rooms); - if (!inviteRooms.isEmpty()) - roomObj.insert("invite", inviteRooms); + if (!roomsJson.isEmpty()) + roomObj.insert(QStringLiteral("join"), roomsJson); + if (!inviteRoomsJson.isEmpty()) + roomObj.insert(QStringLiteral("invite"), inviteRoomsJson); - rootObj.insert("next_batch", d->data->lastEvent()); - rootObj.insert("rooms", roomObj); + rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent()); + rootObj.insert(QStringLiteral("rooms"), roomObj); } { QJsonArray accountDataEvents { - basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats)) + Event::basicJson(QStringLiteral("m.direct"), toJson(d->directChats)) }; - for (const auto &e : d->accountData) + for (const auto& e : d->accountData) accountDataEvents.append( - basicEventJson(e.first, e.second->contentJson())); + Event::basicJson(e.first, e.second->contentJson())); - rootObj.insert("account_data", - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); + rootObj.insert(QStringLiteral("account_data"), + QJsonObject { + { QStringLiteral("events"), accountDataEvents } }); } +#ifdef Quotient_E2EE_ENABLED + { + QJsonObject keysJson = toJson(d->oneTimeKeysCount); + rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson); + } +#endif - QJsonObject versionObj; - versionObj.insert("major", CACHE_VERSION_MAJOR); - versionObj.insert("minor", CACHE_VERSION_MINOR); - rootObj.insert("cache_version", versionObj); - - QJsonDocument json { rootObj }; - auto data = d->cacheToBinary ? json.toBinaryData() : - json.toJson(QJsonDocument::Compact); + const auto data = + d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() + : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; - outfile.write(data.data(), data.size()); - qCDebug(MAIN) << "State cache saved to" << outfile.fileName(); + outFile.write(data.data(), data.size()); + qCDebug(MAIN) << "State cache saved to" << outFile.fileName(); } -void Connection::loadState(const QUrl &fromFile) +void Connection::loadState() { if (!d->cacheState) return; - QElapsedTimer et; et.start(); - QFile file { - fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() - }; - if (!file.exists()) - { - qCDebug(MAIN) << "No state cache file found"; - return; - } - if(!file.open(QFile::ReadOnly)) - { - qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read"; - return; - } - QByteArray data = file.readAll(); + QElapsedTimer et; + et.start(); - auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : - QJsonDocument::fromJson(data); - if (jsonDoc.isNull()) - { - qCWarning(MAIN) << "Cache file broken, discarding"; + SyncData sync { d->topLevelStatePath() }; + if (sync.nextBatch().isEmpty()) // No token means no cache by definition return; - } - auto actualCacheVersionMajor = - jsonDoc.object() - .value("cache_version").toObject() - .value("major").toInt(); - if (actualCacheVersionMajor < CACHE_VERSION_MAJOR) - { - qCWarning(MAIN) - << "Major version of the cache file is" << actualCacheVersionMajor - << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache"; + + if (!sync.unresolvedRooms().isEmpty()) { + qCWarning(MAIN) << "State cache incomplete, discarding"; return; } - - SyncData sync; - sync.parseJson(jsonDoc); - onSyncSuccess(std::move(sync)); + // TODO: to handle load failures, instead of the above block: + // 1. Do initial sync on failed rooms without saving the nextBatch token + // 2. Do the sync across all rooms as normal + onSyncSuccess(std::move(sync), true); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } QString Connection::stateCachePath() const { - auto safeUserId = userId(); - safeUserId.replace(':', '_'); - return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - % '/' % safeUserId % "_state.json"; + return stateCacheDir().path() % '/'; } -bool Connection::cacheState() const +QDir Connection::stateCacheDir() const { - return d->cacheState; + auto safeUserId = userId(); + safeUserId.replace(':', '_'); + return cacheLocation(safeUserId); } +bool Connection::cacheState() const { return d->cacheState; } + void Connection::setCacheState(bool newValue) { - if (d->cacheState != newValue) - { + if (d->cacheState != newValue) { d->cacheState = newValue; emit cacheStateChanged(); } } +bool Connection::lazyLoading() const { return d->lazyLoading; } + +void Connection::setLazyLoading(bool newValue) +{ + if (d->lazyLoading != newValue) { + d->lazyLoading = newValue; + emit lazyLoadingChanged(); + } +} + +BaseJob* Connection::run(BaseJob* job, RunningPolicy runningPolicy) +{ + // Reparent to protect from #397, #398 and to prevent BaseJob* from being + // garbage-collected if made by or returned to QML/JavaScript. + job->setParent(this); + connect(job, &BaseJob::failure, this, &Connection::requestFailed); + job->initiate(d->data.get(), runningPolicy & BackgroundRequest); + return job; +} + void Connection::getTurnServers() { - auto job = callApi<GetTurnServerJob>(); - connect( job, &GetTurnServerJob::success, [=] { - emit turnServersChanged(job->data()); - }); + auto job = callApi<GetTurnServerJob>(); + connect(job, &GetTurnServerJob::success, this, + [this,job] { emit turnServersChanged(job->data()); }); +} + +const QString Connection::SupportedRoomVersion::StableTag = + QStringLiteral("stable"); + +QString Connection::defaultRoomVersion() const +{ + return d->capabilities.roomVersions + ? d->capabilities.roomVersions->defaultVersion + : QString(); +} + +QStringList Connection::stableRoomVersions() const +{ + QStringList l; + if (d->capabilities.roomVersions) { + const auto& allVersions = d->capabilities.roomVersions->available; + for (auto it = allVersions.begin(); it != allVersions.end(); ++it) + if (it.value() == SupportedRoomVersion::StableTag) + l.push_back(it.key()); + } + return l; +} + +bool Connection::canChangePassword() const +{ + // By default assume we can + return d->capabilities.changePassword + ? d->capabilities.changePassword->enabled + : true; +} + +inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, + const Connection::SupportedRoomVersion& v2) +{ + bool ok1 = false, ok2 = false; + const auto vNum1 = v1.id.toFloat(&ok1); + const auto vNum2 = v2.id.toFloat(&ok2); + return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id; +} + +QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const +{ + QVector<SupportedRoomVersion> result; + if (d->capabilities.roomVersions) { + const auto& allVersions = d->capabilities.roomVersions->available; + result.reserve(allVersions.size()); + for (auto it = allVersions.begin(); it != allVersions.end(); ++it) + result.push_back({ it.key(), it.value() }); + // Put stable versions over unstable; within each group, + // sort numeric versions as numbers, the rest as strings. + const auto mid = + std::partition(result.begin(), result.end(), + std::mem_fn(&SupportedRoomVersion::isStable)); + std::sort(result.begin(), mid, roomVersionLess); + std::sort(mid, result.end(), roomVersionLess); + } + return result; +} + +#ifdef Quotient_E2EE_ENABLED +void Connection::Private::loadOutdatedUserDevices() +{ + QHash<QString, QStringList> users; + for(const auto &user : outdatedUsers) { + users[user] += QStringList(); + } + if(currentQueryKeysJob) { + currentQueryKeysJob->abandon(); + currentQueryKeysJob = nullptr; + } + auto queryKeysJob = q->callApi<QueryKeysJob>(users); + currentQueryKeysJob = queryKeysJob; + connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){ + currentQueryKeysJob = nullptr; + const auto data = queryKeysJob->deviceKeys(); + for(const auto &[user, keys] : asKeyValueRange(data)) { + QHash<QString, Quotient::DeviceKeys> oldDevices = deviceKeys[user]; + deviceKeys[user].clear(); + for(const auto &device : keys) { + if(device.userId != user) { + qCWarning(E2EE) + << "mxId mismatch during device key verification:" + << device.userId << user; + continue; + } + if (!std::all_of(device.algorithms.cbegin(), + device.algorithms.cend(), + isSupportedAlgorithm)) { + qCWarning(E2EE) << "Unsupported encryption algorithms found" + << device.algorithms; + continue; + } + if (!verifyIdentitySignature(device, device.deviceId, + device.userId)) { + qCWarning(E2EE) << "Failed to verify devicekeys signature. " + "Skipping this device"; + continue; + } + if (oldDevices.contains(device.deviceId)) { + if (oldDevices[device.deviceId].keys["ed25519:" % device.deviceId] != device.keys["ed25519:" % device.deviceId]) { + qCDebug(E2EE) << "Device reuse detected. Skipping this device"; + continue; + } + } + deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys); + } + outdatedUsers -= user; + } + saveDevicesList(); + + for(size_t i = 0; i < pendingEncryptedEvents.size();) { + if (isKnownCurveKey( + pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), + pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) { + handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]); + pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i); + } else + ++i; + } + }); +} + +void Connection::Private::saveDevicesList() +{ + q->database()->transaction(); + auto query = q->database()->prepareQuery( + QStringLiteral("DELETE FROM tracked_users")); + q->database()->execute(query); + query.prepare(QStringLiteral( + "INSERT INTO tracked_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : trackedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); + } + query.prepare(QStringLiteral("DELETE FROM outdated_users")); + q->database()->execute(query); + query.prepare(QStringLiteral( + "INSERT INTO outdated_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : outdatedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); + } + + query.prepare(QStringLiteral( + "INSERT INTO tracked_devices" + "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified) " + "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);" + )); + for (const auto& user : deviceKeys.keys()) { + for (const auto& device : deviceKeys[user]) { + auto keys = device.keys.keys(); + auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1]; + auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1]; + + query.bindValue(":matrixId", user); + query.bindValue(":deviceId", device.deviceId); + query.bindValue(":curveKeyId", curveKeyId); + query.bindValue(":curveKey", device.keys[curveKeyId]); + query.bindValue(":edKeyId", edKeyId); + query.bindValue(":edKey", device.keys[edKeyId]); + // If the device gets saved here, it can't be verified + query.bindValue(":verified", false); + + q->database()->execute(query); + } + } + q->database()->commit(); } + +void Connection::Private::loadDevicesList() +{ + auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;")); + q->database()->execute(query); + while(query.next()) { + trackedUsers += query.value(0).toString(); + } + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;")); + q->database()->execute(query); + while(query.next()) { + outdatedUsers += query.value(0).toString(); + } + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;")); + q->database()->execute(query); + while(query.next()) { + deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys { + query.value("matrixId").toString(), + query.value("deviceId").toString(), + { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}, + {{query.value("curveKeyId").toString(), query.value("curveKey").toString()}, + {query.value("edKeyId").toString(), query.value("edKey").toString()}}, + {} // Signatures are not saved/loaded as they are not needed after initial validation + }; + } + +} + +void Connection::encryptionUpdate(Room *room) +{ + for(const auto &user : room->users()) { + if(!d->trackedUsers.contains(user->id())) { + d->trackedUsers += user->id(); + d->outdatedUsers += user->id(); + d->encryptionUpdateRequired = true; + } + } +} + +PicklingMode Connection::picklingMode() const +{ + return d->picklingMode; +} +#endif + +void Connection::saveOlmAccount() +{ +#ifdef Quotient_E2EE_ENABLED + qCDebug(E2EE) << "Saving olm account"; + d->database->setAccountPickle(d->olmAccount->pickle(d->picklingMode)); +#endif +} + +#ifdef Quotient_E2EE_ENABLED +QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) +{ + if (auto r = room(notification["room_id"].toString())) + if (auto event = + loadEvent<EncryptedEvent>(notification["event"].toObject())) + if (const auto decrypted = r->decryptMessage(*event)) + return decrypted->fullJson(); + return QJsonObject(); +} + +Database* Connection::database() const +{ + return d->database; +} + +UnorderedMap<QString, QOlmInboundGroupSessionPtr> +Connection::loadRoomMegolmSessions(const Room* room) const +{ + return database()->loadMegolmSessions(room->id(), picklingMode()); +} + +void Connection::saveMegolmSession(const Room* room, + const QOlmInboundGroupSession& session) const +{ + database()->saveMegolmSession(room->id(), session.sessionId(), + session.pickle(picklingMode()), + session.senderId(), session.olmSessionId()); +} + +QStringList Connection::devicesForUser(const QString& userId) const +{ + return d->deviceKeys[userId].keys(); +} + +QString Connection::Private::curveKeyForUserDevice(const QString& userId, + const QString& device) const +{ + return deviceKeys[userId][device].keys["curve25519:" % device]; +} + +QString Connection::edKeyForUserDevice(const QString& userId, + const QString& deviceId) const +{ + return d->deviceKeys[userId][deviceId].keys["ed25519:" % deviceId]; +} + +bool Connection::Private::isKnownCurveKey(const QString& userId, + const QString& curveKey) const +{ + auto query = database->prepareQuery( + QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId " + "AND curveKey=:curveKey")); + query.bindValue(":matrixId", userId); + query.bindValue(":curveKey", curveKey); + database->execute(query); + return query.next(); +} + +bool Connection::hasOlmSession(const QString& user, + const QString& deviceId) const +{ + const auto& curveKey = d->curveKeyForUserDevice(user, deviceId); + return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); +} + +std::pair<QOlmMessage::Type, QByteArray> Connection::Private::olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const +{ + const auto& curveKey = curveKeyForUserDevice(userId, device); + const auto& olmSession = olmSessions.at(curveKey).front(); + const auto result = olmSession->encrypt(message); + database->updateOlmSession(curveKey, olmSession->sessionId(), + olmSession->pickle(picklingMode)); + return { result.type(), result.toCiphertext() }; +} + +bool Connection::Private::createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const OneTimeKeys& oneTimeKeyObject) +{ + static QOlmUtility verifier; + qDebug(E2EE) << "Creating a new session for" << targetUserId + << targetDeviceId; + if (oneTimeKeyObject.isEmpty()) { + qWarning(E2EE) << "No one time key for" << targetUserId + << targetDeviceId; + return false; + } + auto* signedOneTimeKey = + std::get_if<SignedOneTimeKey>(&*oneTimeKeyObject.begin()); + if (!signedOneTimeKey) { + qWarning(E2EE) << "No signed one time key for" << targetUserId + << targetDeviceId; + return false; + } + // Verify contents of signedOneTimeKey - for that, drop `signatures` and + // `unsigned` and then verify the object against the respective signature + const auto signature = + signedOneTimeKey->signature(targetUserId, targetDeviceId); + if (!verifier.ed25519Verify( + q->edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), + signedOneTimeKey->toJsonForVerification(), + signature)) { + qWarning(E2EE) << "Failed to verify one-time-key signature for" + << targetUserId << targetDeviceId + << ". Skipping this device."; + return false; + } + const auto recipientCurveKey = + curveKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(); + auto session = + QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey, + signedOneTimeKey->key()); + if (!session) { + qCWarning(E2EE) << "Failed to create olm session for " + << recipientCurveKey << session.error(); + return false; + } + saveSession(**session, recipientCurveKey); + olmSessions[recipientCurveKey].push_back(std::move(*session)); + return true; +} + +QJsonObject Connection::Private::assembleEncryptedContent( + QJsonObject payloadJson, const QString& targetUserId, + const QString& targetDeviceId) const +{ + payloadJson.insert(SenderKeyL, data->userId()); +// eventJson.insert("sender_device"_ls, data->deviceId()); + payloadJson.insert("keys"_ls, + QJsonObject{ + { Ed25519Key, + QString(olmAccount->identityKeys().ed25519) } }); + payloadJson.insert("recipient"_ls, targetUserId); + payloadJson.insert( + "recipient_keys"_ls, + QJsonObject{ { Ed25519Key, + q->edKeyForUserDevice(targetUserId, targetDeviceId) } }); + const auto [type, cipherText] = olmEncryptMessage( + targetUserId, targetDeviceId, + QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encrypted { + { curveKeyForUserDevice(targetUserId, targetDeviceId), + QJsonObject { { "type"_ls, type }, + { "body"_ls, QString(cipherText) } } } + }; + return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519) + .contentJson(); +} + +void Connection::sendSessionKeyToDevices( + const QString& roomId, const QByteArray& sessionId, + const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices, + int index) +{ + qDebug(E2EE) << "Sending room key to devices:" << sessionId + << sessionKey.toHex(); + QHash<QString, QHash<QString, QString>> hash; + for (const auto& [userId, deviceId] : asKeyValueRange(devices)) + if (!hasOlmSession(userId, deviceId)) { + hash[userId].insert(deviceId, "signed_curve25519"_ls); + qDebug(E2EE) << "Adding" << userId << deviceId + << "to keys to claim"; + } + + if (hash.isEmpty()) + return; + + auto job = callApi<ClaimKeysJob>(hash); + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] { + QHash<QString, QHash<QString, QJsonObject>> usersToDevicesToContent; + for (const auto oneTimeKeys = job->oneTimeKeys(); + const auto& [targetUserId, targetDeviceId] : + asKeyValueRange(devices)) { + if (!hasOlmSession(targetUserId, targetDeviceId) + && !d->createOlmSession( + targetUserId, targetDeviceId, + oneTimeKeys[targetUserId][targetDeviceId])) + continue; + + // Noisy but nice for debugging +// qDebug(E2EE) << "Creating the payload for" << targetUserId +// << targetDeviceId << sessionId << sessionKey.toHex(); + const auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, + roomId, sessionId, sessionKey) + .fullJson(); + + usersToDevicesToContent[targetUserId][targetDeviceId] = + d->assembleEncryptedContent(keyEventJson, targetUserId, + targetDeviceId); + } + if (!usersToDevicesToContent.empty()) { + sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent); + QVector<std::tuple<QString, QString, QString>> receivedDevices; + receivedDevices.reserve(devices.size()); + for (const auto& [user, device] : asKeyValueRange(devices)) + receivedDevices.push_back( + { user, device, d->curveKeyForUserDevice(user, device) }); + + database()->setDevicesReceivedKey(roomId, receivedDevices, + sessionId, index); + } + }); +} + +QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession( + const QString& roomId) const +{ + return d->database->loadCurrentOutboundMegolmSession(roomId, + d->picklingMode); +} + +void Connection::saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const +{ + d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode, + session); +} + +void Connection::startKeyVerificationSession(const QString& deviceId) +{ + auto* const session = new KeyVerificationSession(userId(), deviceId, this); + emit newKeyVerificationSession(session); +} + +void Connection::sendToDevice(const QString& targetUserId, + const QString& targetDeviceId, const Event& event, + bool encrypted) +{ + const auto contentJson = + encrypted ? d->assembleEncryptedContent(event.fullJson(), targetUserId, + targetDeviceId) + : event.contentJson(); + sendToDevices(encrypted ? EncryptedEvent::TypeId : event.type(), + { { targetUserId, { { targetDeviceId, contentJson } } } }); +} + +bool Connection::isVerifiedSession(const QString& megolmSessionId) const +{ + auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", megolmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto olmSessionId = query.value("olmSessionId").toString(); + query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", olmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto curveKey = query.value("senderKey"_ls).toString(); + query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls); + query.bindValue(":curveKey", curveKey); + database()->execute(query); + return query.next() && query.value("verified").toBool(); +} +#endif diff --git a/lib/connection.h b/lib/connection.h index b06fb143..75faf370 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -1,704 +1,919 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "quotient_common.h" +#include "ssosession.h" +#include "util.h" + #include "csapi/create_room.h" -#include "joinstate.h" +#include "csapi/login.h" + #include "events/accountdataevents.h" +#include <QtCore/QDir> #include <QtCore/QObject> -#include <QtCore/QUrl> #include <QtCore/QSize> +#include <QtCore/QUrl> #include <functional> -#include <memory> -namespace QMatrixClient +#ifdef Quotient_E2EE_ENABLED +#include "e2ee/e2ee.h" +#include "e2ee/qolmoutboundsession.h" +#include "keyverificationsession.h" +#include "events/keyverificationevent.h" +#endif + +Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) + +namespace Quotient { + +class Room; +class User; +class ConnectionData; +class RoomEvent; + +class SyncJob; +class SyncData; +class RoomMessagesJob; +class PostReceiptJob; +class ForgetRoomJob; +class MediaThumbnailJob; +class JoinRoomJob; +class UploadContentJob; +class GetContentJob; +class DownloadFileJob; +class SendToDeviceJob; +class SendMessageJob; +class LeaveRoomJob; +class Database; +struct EncryptedFileMetadata; + +class QOlmAccount; +class QOlmInboundGroupSession; + +using LoginFlow = GetLoginFlowsJob::LoginFlow; + +/// Predefined login flows +namespace LoginFlows { + inline const LoginFlow Password { "m.login.password" }; + inline const LoginFlow SSO { "m.login.sso" }; + inline const LoginFlow Token { "m.login.token" }; +} + +// To simplify comparisons of LoginFlows + +inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs) { - class Room; - class User; - class ConnectionData; - class RoomEvent; - - class SyncJob; - class SyncData; - class RoomMessagesJob; - class PostReceiptJob; - class ForgetRoomJob; - class MediaThumbnailJob; - class JoinRoomJob; - class UploadContentJob; - class GetContentJob; - class DownloadFileJob; - class SendToDeviceJob; - class SendMessageJob; - - /** Create a single-shot connection that triggers on the signal and - * then self-disconnects - * - * Only supports DirectConnection type. + return lhs.type == rhs.type; +} + +inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs) +{ + return !(lhs == rhs); +} + +class Connection; + +using room_factory_t = + std::function<Room*(Connection*, const QString&, JoinState)>; +using user_factory_t = std::function<User*(Connection*, const QString&)>; + +/** The default factory to create room objects + * + * Just a wrapper around operator new. + * \sa Connection::setRoomFactory, Connection::setRoomType + */ +template <typename T = Room> +auto defaultRoomFactory(Connection* c, const QString& id, JoinState js) +{ + return new T(c, id, js); +} + +/** The default factory to create user objects + * + * Just a wrapper around operator new. + * \sa Connection::setUserFactory, Connection::setUserType + */ +template <typename T = User> +auto defaultUserFactory(Connection* c, const QString& id) +{ + return new T(id, c); +} + +// Room ids, rather than room pointers, are used in the direct chat +// map types because the library keeps Invite rooms separate from +// rooms in Join and Leave state; and direct chats in account data +// are stored with no regard to their state. +using DirectChatsMap = QMultiHash<const User*, QString>; +using DirectChatUsersMap = QMultiHash<QString, User*>; +using IgnoredUsersList = IgnoredUsersEvent::value_type; + +class QUOTIENT_API Connection : public QObject { + Q_OBJECT + + Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) + Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) + Q_PROPERTY(QString domain READ domain NOTIFY stateChanged STORED false) + Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) + Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) + Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY stateChanged STORED false) + Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) + Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(QVector<GetLoginFlowsJob::LoginFlow> loginFlows READ loginFlows NOTIFY loginFlowsChanged) + Q_PROPERTY(bool isUsable READ isUsable NOTIFY loginFlowsChanged STORED false) + Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false) + 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 UsersToDevicesToContent = QHash<QString, QHash<QString, QJsonObject>>; + + enum RoomVisibility { + PublishRoom, + UnpublishRoom + }; // FIXME: Should go inside CreateRoomJob + + explicit Connection(QObject* parent = nullptr); + explicit Connection(const QUrl& server, QObject* parent = nullptr); + ~Connection() override; + + /// Get all rooms known within this Connection + /*! + * This includes Invite, Join and Leave rooms, in no particular order. + * \note Leave rooms will only show up in the list if they have been left + * in the same running session. The library doesn't cache left rooms + * between runs and it doesn't retrieve the full list of left rooms + * from the server. + * \sa rooms, room, roomsWithTag + */ + Q_INVOKABLE QVector<Quotient::Room*> allRooms() const; + + /// Get rooms that have either of the given join state(s) + /*! + * This method returns, in no particular order, rooms which join state + * matches the mask passed in \p joinStates. + * \note Similar to allRooms(), this won't retrieve the full list of + * Leave rooms from the server. + * \sa allRooms, room, roomsWithTag + */ + Q_INVOKABLE QVector<Quotient::Room*> + rooms(Quotient::JoinStates joinStates) const; + + /// Get the total number of rooms in the given join state(s) + Q_INVOKABLE int roomsCount(Quotient::JoinStates joinStates) const; + + /** Check whether the account has data of the given type + * Direct chats map is not supported by this method _yet_. */ - template <typename SenderT1, typename SignalT, - typename ReceiverT2, typename SlotT> - inline auto connectSingleShot(SenderT1* sender, SignalT signal, - ReceiverT2* receiver, SlotT slot) + bool hasAccountData(const QString& type) const; + + //! \brief Get a generic account data event of the given type + //! + //! \return an account data event of the given type stored on the server, + //! or nullptr if there's none of that type. + //! \note Direct chats map cannot be retrieved using this method _yet_; + //! use directChats() instead. + const EventPtr& accountData(const QString& type) const; + + //! \brief Get an account data event of the given type + //! + //! \return the account data content for the given event type stored + //! on the server, or a default-constructed object if there's none + //! of that type. + //! \note Direct chats map cannot be retrieved using this method _yet_; + //! use directChats() instead. + template <EventClass EventT> + const EventT* accountData() const { - QMetaObject::Connection connection; - connection = QObject::connect(sender, signal, receiver, slot, - Qt::DirectConnection); - Q_ASSERT(connection); - QObject::connect(sender, signal, receiver, - [connection] { QObject::disconnect(connection); }, - Qt::DirectConnection); - return connection; + return eventCast<EventT>(accountData(EventT::TypeId)); } - class Connection; + /** Get account data as a JSON object + * This returns the content part of the account data event + * of the given type. Direct chats map cannot be retrieved using + * this method _yet_; use directChats() instead. + */ + Q_INVOKABLE QJsonObject accountDataJson(const QString& type) const; + + /** Set a generic account data event of the given type */ + void setAccountData(EventPtr&& event); + + Q_INVOKABLE void setAccountData(const QString& type, + const QJsonObject& content); + + /** Get all Invited and Joined rooms grouped by tag + * \return a hashmap from tag name to a vector of room pointers, + * sorted by their order in the tag - details are at + * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 + */ + QHash<QString, QVector<Room*>> tagsToRooms() const; + + /** Get all room tags known on this connection */ + QStringList tagNames() const; + + /** Get the list of rooms with the specified tag */ + QVector<Room*> roomsWithTag(const QString& tagName) const; + + /*! \brief Mark the room as a direct chat with the user + * + * This function marks \p room as a direct chat with \p user. + * Emits the signal synchronously, without waiting to complete + * synchronisation with the server. + * + * \sa directChatsListChanged + */ + void addToDirectChats(const Room* room, User* user); + + /*! \brief Unmark the room from direct chats + * + * This function removes the room id from direct chats either for + * a specific \p user or for all users if \p user in nullptr. + * The room id is used to allow removal of, e.g., ids of forgotten + * rooms; a Room object need not exist. Emits the signal + * immediately, without waiting to complete synchronisation with + * the server. + * + * \sa directChatsListChanged + */ + void removeFromDirectChats(const QString& roomId, User* user = nullptr); + + /** Check whether the room id corresponds to a direct chat */ + bool isDirectChat(const QString& roomId) const; + + /** Get the whole map from users to direct chat rooms */ + DirectChatsMap directChats() const; + + /** Retrieve the list of users the room is a direct chat with + * @return The list of users for which this room is marked as + * a direct chat; an empty list if the room is not a direct chat + */ + QList<User*> directChatUsers(const Room* room) const; + + /** Check whether a particular user is in the ignore list */ + Q_INVOKABLE bool isIgnored(const Quotient::User* user) const; + + /** Get the whole list of ignored users */ + Q_INVOKABLE Quotient::IgnoredUsersList ignoredUsers() const; + + /** Add the user to the ignore list + * The change signal is emitted synchronously, without waiting + * to complete synchronisation with the server. + * + * \sa ignoredUsersListChanged + */ + Q_INVOKABLE void addToIgnoredUsers(const Quotient::User* user); + + /** Remove the user from the ignore list */ + /** Similar to adding, the change signal is emitted synchronously. + * + * \sa ignoredUsersListChanged + */ + Q_INVOKABLE void removeFromIgnoredUsers(const Quotient::User* user); + + /** Get the full list of users known to this account */ + QMap<QString, User*> users() const; + + /** Get the base URL of the homeserver to connect to */ + QUrl homeserver() const; + /** Get the domain name used for ids/aliases on the server */ + QString domain() const; + /** Check if the homeserver is known to be reachable and working */ + bool isUsable() const; + /** Get the list of supported login flows */ + QVector<GetLoginFlowsJob::LoginFlow> loginFlows() const; + /** Check whether the current homeserver supports password auth */ + bool supportsPasswordAuth() const; + /** Check whether the current homeserver supports SSO */ + bool supportsSso() const; + /** Find a room by its id and a mask of applicable states */ + Q_INVOKABLE Quotient::Room* + room(const QString& roomId, + Quotient::JoinStates states = JoinState::Invite | JoinState::Join) const; + /** Find a room by its alias and a mask of applicable states */ + Q_INVOKABLE Quotient::Room* + roomByAlias(const QString& roomAlias, + Quotient::JoinStates states = JoinState::Invite + | JoinState::Join) const; + /** Update the internal map of room aliases to IDs */ + /// This is used to maintain the internal index of room aliases. + /// It does NOT change aliases on the server, + /// \sa Room::setLocalAliases + void updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases); + Q_INVOKABLE Quotient::Room* invitation(const QString& roomId) const; + Q_INVOKABLE Quotient::User* user(const QString& uId); + const User* user() const; + User* user(); + QString userId() const; + QString deviceId() const; + QByteArray accessToken() const; + bool isLoggedIn() const; +#ifdef Quotient_E2EE_ENABLED + QOlmAccount* olmAccount() const; + Database* database() const; + PicklingMode picklingMode() const; + + UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions( + const Room* room) const; + void saveMegolmSession(const Room* room, + const QOlmInboundGroupSession& session) const; + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( + const QString& roomId) const; + void saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const; + + QString edKeyForUserDevice(const QString& userId, + const QString& deviceId) const; + bool hasOlmSession(const QString& user, const QString& deviceId) const; + + // This assumes that an olm session already exists. If it doesn't, no message is sent. + void sendToDevice(const QString& targetUserId, const QString& targetDeviceId, + const Event& event, bool encrypted); + + /// Returns true if this megolm session comes from a verified device + bool isVerifiedSession(const QString& megolmSessionId) const; + + void sendSessionKeyToDevices(const QString& roomId, + const QByteArray& sessionId, + const QByteArray& sessionKey, + const QMultiHash<QString, QString>& devices, + int index); + + QJsonObject decryptNotification(const QJsonObject ¬ification); + QStringList devicesForUser(const QString& userId) const; +#endif // Quotient_E2EE_ENABLED + Q_INVOKABLE Quotient::SyncJob* syncJob() const; + Q_INVOKABLE int millisToReconnect() const; + + Q_INVOKABLE void getTurnServers(); + + struct SupportedRoomVersion { + QString id; + QString status; + + static const QString StableTag; // "stable", as of CS API 0.5 + bool isStable() const { return status == StableTag; } + + friend QDebug operator<<(QDebug dbg, const SupportedRoomVersion& v) + { + QDebugStateSaver _(dbg); + return dbg.nospace() << v.id << '/' << v.status; + } + }; + + /// Get the room version recommended by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QString defaultRoomVersion() const; + /// Get the room version considered stable by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QStringList stableRoomVersions() const; + /// Get all room versions supported by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QVector<SupportedRoomVersion> availableRoomVersions() const; + + /// Indicate if the user can change its password from the client. + /// This is often not the case when SSO is enabled. + /// \sa loadingCapabilities + bool canChangePassword() const; + + /** + * Call this before first sync to load from previously saved file. + */ + Q_INVOKABLE void loadState(); + /** + * This method saves the current state of rooms (but not messages + * in them) to a local cache file, so that it could be loaded by + * loadState() on a next run of the client. + */ + Q_INVOKABLE void saveState() const; + + /// This method saves the current state of a single room. + void saveRoomState(Room* r) const; + + /// Get the default directory path to save the room state to + /** \sa stateCacheDir */ + Q_INVOKABLE QString stateCachePath() const; + + /// Get the default directory to save the room state to + /** + * This function returns the default directory to store the cached + * room state, defined as follows: + * \code + * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + + * _safeUserId + "_state.json" \endcode where `_safeUserId` is userId() with + * `:` (colon) replaced by + * `_` (underscore), as colons are reserved characters on Windows. + * \sa loadState, saveState, stateCachePath + */ + QDir stateCacheDir() const; + + /** Whether or not the rooms state should be cached locally + * \sa loadState(), saveState() + */ + bool cacheState() const; + void setCacheState(bool newValue); + + bool lazyLoading() const; + void setLazyLoading(bool newValue); - using room_factory_t = std::function<Room*(Connection*, const QString&, - JoinState)>; - using user_factory_t = std::function<User*(Connection*, const QString&)>; + /*! Start a pre-created job object on this connection */ + Q_INVOKABLE BaseJob* run(BaseJob* job, + RunningPolicy runningPolicy = ForegroundRequest); - /** The default factory to create room objects + /*! Start a job of a specified type with specified arguments and policy + * + * This is a universal method to create and start a job of a type passed + * as a template parameter. The policy allows to fine-tune the way + * the job is executed - as of this writing it means a choice + * between "foreground" and "background". + * + * \param runningPolicy controls how the job is executed + * \param jobArgs arguments to the job constructor * - * Just a wrapper around operator new. - * \sa Connection::setRoomFactory, Connection::setRoomType + * \sa BaseJob::isBackground. QNetworkRequest::BackgroundRequestAttribute */ - template <typename T = Room> - static inline room_factory_t defaultRoomFactory() + template <typename JobT, typename... JobArgTs> + JobT* callApi(RunningPolicy runningPolicy, JobArgTs&&... jobArgs) { - return [](Connection* c, const QString& id, JoinState js) - { - return new T(c, id, js); - }; + auto job = new JobT(std::forward<JobArgTs>(jobArgs)...); + run(job, runningPolicy); + return job; } - /** The default factory to create user objects + /*! Start a job of a specified type with specified arguments * - * Just a wrapper around operator new. - * \sa Connection::setUserFactory, Connection::setUserType + * This is an overload that runs the job with "foreground" policy. */ - template <typename T = User> - static inline user_factory_t defaultUserFactory() + template <typename JobT, typename... JobArgTs> + JobT* callApi(JobArgTs&&... jobArgs) { - return [](Connection* c, const QString& id) - { - return new T(id, c); - }; + return callApi<JobT>(ForegroundRequest, + std::forward<JobArgTs>(jobArgs)...); } - /** Enumeration with flags defining the network job running policy - * So far only background/foreground flags are available. + /*! Get a request URL for a job with specified type and arguments * - * \sa Connection::callApi - */ - enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; - - class Connection: public QObject { - Q_OBJECT - - /** Whether or not the rooms state should be cached locally - * \sa loadState(), saveState() - */ - Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) - Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) - Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) - Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) - Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) - Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) - public: - // Room ids, rather than room pointers, are used in the direct chat - // map types because the library keeps Invite rooms separate from - // rooms in Join and Leave state; and direct chats in account data - // are stored with no regard to their state. - using DirectChatsMap = QMultiHash<const User*, QString>; - using DirectChatUsersMap = QMultiHash<QString, User*>; - using IgnoredUsersList = IgnoredUsersEvent::content_type; - - using UsersToDevicesToEvents = - std::unordered_map<QString, - std::unordered_map<QString, const Event&>>; - - enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob - - explicit Connection(QObject* parent = nullptr); - explicit Connection(const QUrl& server, QObject* parent = nullptr); - virtual ~Connection(); - - /** 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 - */ - QHash<QPair<QString, bool>, Room*> roomMap() const; - - /** Check whether the account has data of the given type - * Direct chats map is not supported by this method _yet_. - */ - bool hasAccountData(const QString& type) const; - - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ - const EventPtr& accountData(const QString& type) const; - - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ - template <typename EventT> - const typename EventT::content_type accountData() const - { - if (const auto& eventPtr = accountData(EventT::matrixTypeId())) - return eventPtr->content(); - return {}; - } - - /** Get account data as a JSON object - * This returns the content part of the account data event - * of the given type. Direct chats map cannot be retrieved using - * this method _yet_; use directChats() instead. - */ - Q_INVOKABLE QJsonObject accountDataJson(const QString& type) const; - - /** Set a generic account data event of the given type */ - void setAccountData(EventPtr&& event); - - Q_INVOKABLE void setAccountData(const QString& type, - const QJsonObject& content); - - /** Get all Invited and Joined rooms grouped by tag - * \return a hashmap from tag name to a vector of room pointers, - * sorted by their order in the tag - details are at - * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 - */ - QHash<QString, QVector<Room*>> tagsToRooms() const; - - /** Get all room tags known on this connection */ - QStringList tagNames() const; - - /** Get the list of rooms with the specified tag */ - QVector<Room*> roomsWithTag(const QString& tagName) const; - - /** Mark the room as a direct chat with the user - * This function marks \p room as a direct chat with \p user. - * Emits the signal synchronously, without waiting to complete - * synchronisation with the server. - * - * \sa directChatsListChanged - */ - void addToDirectChats(const Room* room, User* user); - - /** Unmark the room from direct chats - * This function removes the room id from direct chats either for - * a specific \p user or for all users if \p user in nullptr. - * The room id is used to allow removal of, e.g., ids of forgotten - * rooms; a Room object need not exist. Emits the signal - * immediately, without waiting to complete synchronisation with - * the server. - * - * \sa directChatsListChanged - */ - void removeFromDirectChats(const QString& roomId, - User* user = nullptr); - - /** Check whether the room id corresponds to a direct chat */ - bool isDirectChat(const QString& roomId) const; - - /** Get the whole map from users to direct chat rooms */ - DirectChatsMap directChats() const; - - /** Retrieve the list of users the room is a direct chat with - * @return The list of users for which this room is marked as - * a direct chat; an empty list if the room is not a direct chat - */ - QList<User*> directChatUsers(const Room* room) const; - - /** Check whether a particular user is in the ignore list */ - bool isIgnored(const User* user) const; - - /** Get the whole list of ignored users */ - IgnoredUsersList ignoredUsers() const; - - /** Add the user to the ignore list - * The change signal is emitted synchronously, without waiting - * to complete synchronisation with the server. - * - * \sa ignoredUsersListChanged - */ - void addToIgnoredUsers(const User* user); - - /** Remove the user from the ignore list - * Similar to adding, the change signal is emitted synchronously. - * - * \sa ignoredUsersListChanged - */ - void removeFromIgnoredUsers(const User* user); - - /** Get the full list of users known to this account */ - QMap<QString, User*> users() const; - - QUrl homeserver() const; - Q_INVOKABLE Room* room(const QString& roomId, - JoinStates states = JoinState::Invite|JoinState::Join) const; - Q_INVOKABLE Room* invitation(const QString& roomId) const; - Q_INVOKABLE User* user(const QString& userId); - const User* user() const; - User* user(); - QString userId() const; - QString deviceId() const; - QByteArray accessToken() const; - Q_INVOKABLE SyncJob* syncJob() const; - Q_INVOKABLE int millisToReconnect() const; - - [[deprecated("Use accessToken() instead")]] - Q_INVOKABLE QString token() const; - Q_INVOKABLE void getTurnServers(); - - /** - * Call this before first sync to load from previously saved file. - * - * \param fromFile A local path to read the state from. Uses QUrl - * to be QML-friendly. Empty parameter means using a path - * defined by stateCachePath(). - */ - Q_INVOKABLE void loadState(const QUrl &fromFile = {}); - /** - * This method saves the current state of rooms (but not messages - * in them) to a local cache file, so that it could be loaded by - * loadState() on a next run of the client. - * - * \param toFile A local path to save the state to. Uses QUrl to be - * QML-friendly. Empty parameter means using a path defined by - * stateCachePath(). - */ - Q_INVOKABLE void saveState(const QUrl &toFile = {}) const; - - /** - * The default path to store the cached room state, defined as - * follows: - * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json" - * where `_safeUserId` is userId() with `:` (colon) replaced with - * `_` (underscore) - * /see loadState(), saveState() - */ - Q_INVOKABLE QString stateCachePath() const; - - bool cacheState() const; - void setCacheState(bool newValue); - - /** Start a job of a specified type with specified arguments and policy - * - * This is a universal method to start a job of a type passed - * as a template parameter. The policy allows to fine-tune the way - * the job is executed - as of this writing it means a choice - * between "foreground" and "background". - * - * \param runningPolicy controls how the job is executed - * \param jobArgs arguments to the job constructor - * - * \sa BaseJob::isBackground. QNetworkRequest::BackgroundRequestAttribute - */ - template <typename JobT, typename... JobArgTs> - JobT* callApi(RunningPolicy runningPolicy, - JobArgTs&&... jobArgs) const - { - auto job = new JobT(std::forward<JobArgTs>(jobArgs)...); - connect(job, &BaseJob::failure, this, &Connection::requestFailed); - job->start(connectionData(), runningPolicy&BackgroundRequest); - return job; - } - - /** Start a job of a specified type with specified arguments - * - * This is an overload that calls the job with "foreground" policy. - */ - template <typename JobT, typename... JobArgTs> - JobT* callApi(JobArgTs&&... jobArgs) const - { - return callApi<JobT>(ForegroundRequest, - std::forward<JobArgTs>(jobArgs)...); - } - - /** Generate a new transaction id. Transaction id's are unique within - * a single Connection object - */ - Q_INVOKABLE QByteArray generateTxnId() const; - - /// Set a room factory function - static void setRoomFactory(room_factory_t f); - - /// Set a user factory function - static void setUserFactory(user_factory_t f); - - /// Get a room factory function - static room_factory_t roomFactory(); - - /// Get a user factory function - static user_factory_t userFactory(); - - /// Set the room factory to default with the overriden room type - template <typename T> - static void setRoomType() { setRoomFactory(defaultRoomFactory<T>()); } - - /// Set the user factory to default with the overriden user type - template <typename T> - static void setUserType() { setUserFactory(defaultUserFactory<T>()); } - - public slots: - /** Set the homeserver base URL */ - void setHomeserver(const QUrl& baseUrl); - - /** Determine and set the homeserver from domain or MXID */ - void resolveServer(const QString& mxidOrDomain); - - void connectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId = {}); - void connectWithToken(const QString& userId, const QString& accessToken, - const QString& deviceId); - - /** @deprecated Use stopSync() instead */ - void disconnectFromServer() { stopSync(); } - void logout(); - - void sync(int timeout = -1); - void stopSync(); - - virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, - QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; - MediaThumbnailJob* getThumbnail(const QUrl& url, - QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; - MediaThumbnailJob* getThumbnail(const QUrl& url, - int requestedWidth, int requestedHeight, - RunningPolicy policy = BackgroundRequest) const; - - // QIODevice* should already be open - UploadContentJob* uploadContent(QIODevice* contentSource, - const QString& filename = {}, - const QString& contentType = {}) const; - UploadContentJob* uploadFile(const QString& fileName, - const QString& contentType = {}); - GetContentJob* getContent(const QString& mediaId) const; - GetContentJob* getContent(const QUrl& url) const; - // If localFilename is empty, a temporary file will be created - DownloadFileJob* downloadFile(const QUrl& url, - const QString& localFilename = {}) const; - - /** - * \brief Create a room (generic method) - * This method allows to customize room entirely to your liking, - * providing all the attributes the original CS API provides. - */ - CreateRoomJob* createRoom(RoomVisibility visibility, - const QString& alias, const QString& name, const QString& topic, - QStringList invites, const QString& presetName = {}, - bool isDirect = false, - const QVector<CreateRoomJob::StateEvent>& initialState = {}, - const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, - const QJsonObject& creationContent = {}); - - /** Get a direct chat with a single user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. - * - * \sa directChatAvailable - */ - void requestDirectChat(const QString& userId); - - /** Get a direct chat with a single user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. - * - * \sa directChatAvailable - */ - void requestDirectChat(User* u); - - /** Run an operation in a direct chat with the user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. Instead of emitting a signal it executes the passed - * function object with the direct chat room as its parameter. - */ - void doInDirectChat(const QString& userId, - const std::function<void(Room*)>& operation); - - /** Run an operation in a direct chat with the user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. Instead of emitting a signal it executes the passed - * function object with the direct chat room as its parameter. - */ - void doInDirectChat(User* u, - const std::function<void(Room*)>& operation); - - /** Create a direct chat with a single user, optional name and topic - * A room will always be created, unlike in requestDirectChat. - * It is advised to use requestDirectChat as a default way of getting - * one-on-one with a person, and only use createDirectChat when - * a new creation is explicitly desired. - */ - CreateRoomJob* createDirectChat(const QString& userId, - const QString& topic = {}, const QString& name = {}); - - virtual JoinRoomJob* joinRoom(const QString& roomAlias, - const QStringList& serverNames = {}); - - /** Sends /forget to the server and also deletes room locally. - * This method is in Connection, not in Room, since it's a - * room lifecycle operation, and Connection is an acting room manager. - * It ensures that the local user is not a member of a room (running /leave, - * if necessary) then issues a /forget request and if that one doesn't fail - * deletion of the local Room object is ensured. - * \param id - the room id to forget - * \return - the ongoing /forget request to the server; note that the - * success() signal of this request is connected to deleteLater() - * of a respective room so by the moment this finishes, there might be no - * Room object anymore. - */ - ForgetRoomJob* forgetRoom(const QString& id); - - SendToDeviceJob* sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap) const; - - /** \deprecated This method is experimental and may be removed any time */ - SendMessageJob* sendMessage(const QString& roomId, - const RoomEvent& event) const; - - // Old API that will be abolished any time soon. DO NOT USE. - - /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */ - virtual PostReceiptJob* postReceipt(Room* room, - RoomEvent* event) const; - /** @deprecated Use callApi<LeaveRoomJob>() or Room::leaveRoom() instead */ - virtual void leaveRoom( Room* room ); - - signals: - /** - * @deprecated - * This was a signal resulting from a successful resolveServer(). - * Since Connection now provides setHomeserver(), the HS URL - * may change even without resolveServer() invocation. Use - * homeserverChanged() instead of resolved(). You can also use - * connectToServer and connectWithToken without the HS URL set in - * advance (i.e. without calling resolveServer), as they now trigger - * server name resolution from MXID if the server URL is not valid. - */ - void resolved(); - void resolveError(QString error); - - void homeserverChanged(QUrl baseUrl); - - void connected(); - void reconnected(); //< \deprecated Use connected() instead - void loggedOut(); - /** Login data or state have changed - * - * This is a common change signal for userId, deviceId and - * accessToken - these properties normally only change at - * a successful login and logout and are constant at other times. - */ - void stateChanged(); - void loginError(QString message, QByteArray details); - - /** A network request (job) failed - * - * @param request - the pointer to the failed job - */ - void requestFailed(BaseJob* request); - - /** A network request (job) failed due to network problems - * - * This is _only_ emitted when the job will retry on its own; - * once it gives up, requestFailed() will be emitted. - * - * @param message - message about the network problem - * @param details - raw error details, if any available - * @param retriesTaken - how many retries have already been taken - * @param nextRetryInMilliseconds - when the job will retry again - */ - void networkError(QString message, QByteArray details, - int retriesTaken, int nextRetryInMilliseconds); - - void syncDone(); - void syncError(QString message, QByteArray details); - - void newUser(User* user); - - /** - * \group Signals emitted on room transitions - * - * Note: Rooms in Invite state are always stored separately from - * rooms in Join/Leave state, because of special treatment of - * invite_state in Matrix CS API (see The Spec on /sync for details). - * Therefore, objects below are: r - room in Join/Leave state; - * i - room in Invite state - * - * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) - * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) - * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) - * 4. Invite -> Join: - * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) - * 4a. Leave and Invite -> Join: - * joinedRoom(r,i), aboutToDeleteRoom(i) - * 5. Invite -> Leave: - * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) - * 5a. Leave and Invite -> Leave: - * leftRoom(r,i), aboutToDeleteRoom(i) - * 6. Join -> Leave: leftRoom(r) - * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) - * 8. Leave -> Join: joinedRoom(r) - * The following transitions are only possible via forgetRoom() - * so far; if a room gets forgotten externally, sync won't tell - * about it: - * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) - */ - - /** A new room object has been created */ - void newRoom(Room* room); - - /** A room invitation is seen for the first time - * - * If the same room is in Left state, it's passed in prev. Beware - * that initial sync will trigger this signal for all rooms in - * Invite state. - */ - void invitedRoom(Room* room, Room* prev); - - /** A joined room is seen for the first time - * - * It's not the same as receiving a room in "join" section of sync - * response (rooms will be there even after joining); it's also - * not (exactly) the same as actual joining action of a user (all - * rooms coming in initial sync will trigger this signal too). If - * this room was in Invite state before, the respective object is - * passed in prev (and it will be deleted shortly afterwards). - */ - void joinedRoom(Room* room, Room* prev); - - /** A room has just been left - * - * If this room has been in Invite state (as in case of rejecting - * an invitation), the respective object will be passed in prev - * (and will be deleted shortly afterwards). Note that, similar - * to invitedRoom and joinedRoom, this signal is triggered for all - * Left rooms upon initial sync (not only those that were left - * right before the sync). - */ - void leftRoom(Room* room, Room* prev); - - /** The room object is about to be deleted */ - void aboutToDeleteRoom(Room* room); - - /** The room has just been created by createRoom or requestDirectChat - * - * This signal is not emitted in usual room state transitions, - * only as an outcome of room creation operations invoked by - * the client. - * \note requestDirectChat doesn't necessarily create a new chat; - * use directChatAvailable signal if you just need to obtain - * a direct chat room. - */ - void createdRoom(Room* room); - - /** The first sync for the room has been completed - * - * This signal is emitted after the room has been synced the first - * time. This is the right signal to connect to if you need to - * access the room state (name, aliases, members); state transition - * signals (newRoom, joinedRoom etc.) come earlier, when the room - * has just been created. - */ - void loadedRoomState(Room* room); - - /** Account data (except direct chats) have changed */ - void accountDataChanged(QString type); - - /** The direct chat room is ready for using - * This signal is emitted upon any successful outcome from - * requestDirectChat. - */ - void directChatAvailable(Room* directChat); - - /** The list of direct chats has changed - * This signal is emitted every time when the mapping of users - * to direct chat rooms is changed (because of either local updates - * or a different list arrived from the server). - */ - void directChatsListChanged(DirectChatsMap additions, - DirectChatsMap removals); - - void ignoredUsersListChanged(IgnoredUsersList additions, - IgnoredUsersList removals); - - void cacheStateChanged(); - void turnServersChanged(const QJsonObject& servers); - - protected: - /** - * @brief Access the underlying ConnectionData class - */ - const ConnectionData* connectionData() const; - - /** - * @brief Find a (possibly new) Room object for the specified id - * Use this method whenever you need to find a Room object in - * the local list of rooms. Note that this does not interact with - * the server; in particular, does not automatically create rooms - * on the server. - * @return a pointer to a Room object with the specified id; nullptr - * if roomId is empty or roomFactory() failed to create a Room object. - */ - Room* provideRoom(const QString& roomId, JoinState joinState); - - /** - * Completes loading sync data. - */ - void onSyncSuccess(SyncData &&data); - - private: - class Private; - std::unique_ptr<Private> d; - - /** - * A single entry for functions that need to check whether the - * homeserver is valid before running. May either execute connectFn - * synchronously or asynchronously (if tryResolve is true and - * a DNS lookup is initiated); in case of errors, emits resolveError - * if the homeserver URL is not valid and cannot be resolved from - * userId. - * - * @param userId - fully-qualified MXID to resolve HS from - * @param connectFn - a function to execute once the HS URL is good - */ - void checkAndConnect(const QString& userId, - std::function<void()> connectFn); - void doConnectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId = {}); - - static room_factory_t _roomFactory; - static user_factory_t _userFactory; - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::Connection*) + * This calls JobT::makeRequestUrl() prepending the connection's homeserver + * to the list of arguments. + */ + template <typename JobT, typename... JobArgTs> + QUrl getUrlForApi(JobArgTs&&... jobArgs) const + { + return JobT::makeRequestUrl(homeserver(), + std::forward<JobArgTs>(jobArgs)...); + } + + //! \brief Start a local HTTP server and generate a single sign-on URL + //! + //! This call does the preparatory steps to carry out single sign-on + //! sequence + //! \sa https://matrix.org/docs/guides/sso-for-client-developers + //! \return A proxy object holding two URLs: one for SSO on the chosen + //! homeserver and another for the local callback address. Normally + //! you won't need the callback URL unless you proxy the response + //! with a custom UI. You do not need to delete the SsoSession + //! object; the Connection that issued it will dispose of it once + //! the login sequence completes (with any outcome). + Q_INVOKABLE SsoSession* prepareForSso(const QString& initialDeviceName, + const QString& deviceId = {}); + + /** Generate a new transaction id. Transaction id's are unique within + * a single Connection object + */ + Q_INVOKABLE QByteArray generateTxnId() const; + + /// Set a room factory function + static void setRoomFactory(room_factory_t f); + + /// Set a user factory function + static void setUserFactory(user_factory_t f); + + /// Get a room factory function + static room_factory_t roomFactory(); + + /// Get a user factory function + static user_factory_t userFactory(); + + /// Set the room factory to default with the overriden room type + template <typename T> + static void setRoomType() + { + setRoomFactory(defaultRoomFactory<T>); + } + + /// Set the user factory to default with the overriden user type + template <typename T> + static void setUserType() + { + setUserFactory(defaultUserFactory<T>); + } + + /// Saves the olm account data to disk. Usually doesn't need to be called manually. + void saveOlmAccount(); + +public Q_SLOTS: + /// \brief Set the homeserver base URL and retrieve its login flows + /// + /// \sa LoginFlowsJob, loginFlows, loginFlowsChanged, homeserverChanged + void setHomeserver(const QUrl& baseUrl); + + /// \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 + * + * Before logging in, this method checks if the homeserver is valid and + * supports the password login flow. If the homeserver is invalid but + * a full user MXID is provided, this method calls resolveServer() using + * this MXID. + * + * \sa resolveServer, resolveError, loginError + */ + void loginWithPassword(const QString& userId, const QString& password, + const QString& initialDeviceName, + const QString& deviceId = {}); + /** \brief Log in using a login token + * + * One usual case for this method is the final stage of logging in via SSO. + * Unlike loginWithPassword() and assumeIdentity(), this method cannot + * resolve the server from the user name because the full user MXID is + * encoded in the login token. Callers should ensure the homeserver + * sanity in advance. + */ + void loginWithToken(const QByteArray& loginToken, + const QString& initialDeviceName, + const QString& deviceId = {}); + /** \brief Use an existing access token to connect to the homeserver + * + * Similar to loginWithPassword(), this method checks that the homeserver + * URL is valid and tries to resolve it from the MXID in case it is not. + */ + void assumeIdentity(const QString& mxId, const QString& accessToken, + const QString& deviceId); + /// Explicitly request capabilities from the server + void reloadCapabilities(); + + /// Find out if capabilites are still loading from the server + bool loadingCapabilities() const; + + void logout(); + + void sync(int timeout = -1); + void syncLoop(int timeout = 30000); + + void stopSync(); + QString nextBatchToken() const; + + Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const; + + virtual MediaThumbnailJob* + getThumbnail(const QString& mediaId, QSize requestedSize, + RunningPolicy policy = BackgroundRequest); + MediaThumbnailJob* getThumbnail(const QUrl& url, QSize requestedSize, + RunningPolicy policy = BackgroundRequest); + MediaThumbnailJob* getThumbnail(const QUrl& url, int requestedWidth, + int requestedHeight, + RunningPolicy policy = BackgroundRequest); + + // QIODevice* should already be open + UploadContentJob* uploadContent(QIODevice* contentSource, + const QString& filename = {}, + const QString& overrideContentType = {}); + UploadContentJob* uploadFile(const QString& fileName, + const QString& overrideContentType = {}); + GetContentJob* getContent(const QString& mediaId); + GetContentJob* getContent(const QUrl& url); + // If localFilename is empty, a temporary file will be created + DownloadFileJob* downloadFile(const QUrl& url, + const QString& localFilename = {}); + +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob* downloadFile(const QUrl& url, + const EncryptedFileMetadata& fileMetadata, + const QString& localFilename = {}); +#endif + /** + * \brief Create a room (generic method) + * This method allows to customize room entirely to your liking, + * providing all the attributes the original CS API provides. + */ + CreateRoomJob* + createRoom(RoomVisibility visibility, const QString& alias, + const QString& name, const QString& topic, QStringList invites, + const QString& presetName = {}, const QString& roomVersion = {}, + bool isDirect = false, + const QVector<CreateRoomJob::StateEvent>& initialState = {}, + const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, + const QJsonObject& creationContent = {}); + + /** Get a direct chat with a single user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. + * + * \sa directChatAvailable + */ + void requestDirectChat(const QString& userId); + + /** Get a direct chat with a single user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. + * + * \sa directChatAvailable + */ + void requestDirectChat(User* u); + + /** Run an operation in a direct chat with the user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. Instead of emitting a signal it executes the passed + * function object with the direct chat room as its parameter. + */ + void doInDirectChat(const QString& userId, + const std::function<void(Room*)>& operation); + + /** Run an operation in a direct chat with the user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. Instead of emitting a signal it executes the passed + * function object with the direct chat room as its parameter. + */ + void doInDirectChat(User* u, const std::function<void(Room*)>& operation); + + /** Create a direct chat with a single user, optional name and topic + * A room will always be created, unlike in requestDirectChat. + * It is advised to use requestDirectChat as a default way of getting + * one-on-one with a person, and only use createDirectChat when + * a new creation is explicitly desired. + */ + CreateRoomJob* createDirectChat(const QString& userId, + const QString& topic = {}, + const QString& name = {}); + + virtual JoinRoomJob* joinRoom(const QString& roomAlias, + const QStringList& serverNames = {}); + + /** Sends /forget to the server and also deletes room locally. + * This method is in Connection, not in Room, since it's a + * room lifecycle operation, and Connection is an acting room manager. + * It ensures that the local user is not a member of a room (running /leave, + * if necessary) then issues a /forget request and if that one doesn't fail + * deletion of the local Room object is ensured. + * \param id - the room id to forget + * \return - the ongoing /forget request to the server; note that the + * success() signal of this request is connected to deleteLater() + * of a respective room so by the moment this finishes, there might be no + * Room object anymore. + */ + ForgetRoomJob* forgetRoom(const QString& id); + + SendToDeviceJob* sendToDevices(const QString& eventType, + const UsersToDevicesToContent& contents); + + /** \deprecated This method is experimental and may be removed any time */ + SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event); + + /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ + virtual LeaveRoomJob* leaveRoom(Room* room); + +#ifdef Quotient_E2EE_ENABLED + void startKeyVerificationSession(const QString& deviceId); + + void encryptionUpdate(Room *room); +#endif + +Q_SIGNALS: + /// \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); + void loginFlowsChanged(); + void capabilitiesLoaded(); + + void connected(); + void loggedOut(); + /** Login data or state have changed + * + * This is a common change signal for userId, deviceId and + * accessToken - these properties normally only change at + * a successful login and logout and are constant at other times. + */ + void stateChanged(); + void loginError(QString message, QString details); + + /** A network request (job) failed + * + * @param request - the pointer to the failed job + */ + void requestFailed(Quotient::BaseJob* request); + + /** A network request (job) failed due to network problems + * + * This is _only_ emitted when the job will retry on its own; + * once it gives up, requestFailed() will be emitted. + * + * @param message - message about the network problem + * @param details - raw error details, if any available + * @param retriesTaken - how many retries have already been taken + * @param nextRetryInMilliseconds - when the job will retry again + */ + void networkError(QString message, QString details, int retriesTaken, + int nextRetryInMilliseconds); + + void syncDone(); + void syncError(QString message, QString details); + + void newUser(Quotient::User* user); + + /** + * \group Signals emitted on room transitions + * + * Note: Rooms in Invite state are always stored separately from + * rooms in Join/Leave state, because of special treatment of + * invite_state in Matrix CS API (see The Spec on /sync for details). + * Therefore, objects below are: r - room in Join/Leave state; + * i - room in Invite state + * + * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) + * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) + * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) + * 4. Invite -> Join: + * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) + * 4a. Leave and Invite -> Join: + * joinedRoom(r,i), aboutToDeleteRoom(i) + * 5. Invite -> Leave: + * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) + * 5a. Leave and Invite -> Leave: + * leftRoom(r,i), aboutToDeleteRoom(i) + * 6. Join -> Leave: leftRoom(r) + * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) + * 8. Leave -> Join: joinedRoom(r) + * The following transitions are only possible via forgetRoom() + * so far; if a room gets forgotten externally, sync won't tell + * about it: + * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) + */ + + /** A new room object has been created */ + void newRoom(Quotient::Room* room); + + /** A room invitation is seen for the first time + * + * If the same room is in Left state, it's passed in prev. Beware + * that initial sync will trigger this signal for all rooms in + * Invite state. + */ + void invitedRoom(Quotient::Room* room, Quotient::Room* prev); + + /** A joined room is seen for the first time + * + * It's not the same as receiving a room in "join" section of sync + * response (rooms will be there even after joining); it's also + * not (exactly) the same as actual joining action of a user (all + * rooms coming in initial sync will trigger this signal too). If + * this room was in Invite state before, the respective object is + * passed in prev (and it will be deleted shortly afterwards). + */ + void joinedRoom(Quotient::Room* room, Quotient::Room* prev); + + /** A room has just been left + * + * If this room has been in Invite state (as in case of rejecting + * an invitation), the respective object will be passed in prev + * (and will be deleted shortly afterwards). Note that, similar + * to invitedRoom and joinedRoom, this signal is triggered for all + * Left rooms upon initial sync (not only those that were left + * right before the sync). + */ + void leftRoom(Quotient::Room* room, Quotient::Room* prev); + + /** The room object is about to be deleted */ + void aboutToDeleteRoom(Quotient::Room* room); + + /** The room has just been created by createRoom or requestDirectChat + * + * This signal is not emitted in usual room state transitions, + * only as an outcome of room creation operations invoked by + * the client. + * \note requestDirectChat doesn't necessarily create a new chat; + * use directChatAvailable signal if you just need to obtain + * a direct chat room. + */ + void createdRoom(Quotient::Room* room); + + /** The first sync for the room has been completed + * + * This signal is emitted after the room has been synced the first + * time. This is the right signal to connect to if you need to + * access the room state (name, aliases, members); state transition + * signals (newRoom, joinedRoom etc.) come earlier, when the room + * has just been created. + */ + void loadedRoomState(Quotient::Room* room); + + /** Account data (except direct chats) have changed */ + void accountDataChanged(QString type); + + /** The direct chat room is ready for using + * This signal is emitted upon any successful outcome from + * requestDirectChat. + */ + void directChatAvailable(Quotient::Room* directChat); + + /** The list of direct chats has changed + * This signal is emitted every time when the mapping of users + * to direct chat rooms is changed (because of either local updates + * or a different list arrived from the server). + */ + void directChatsListChanged(Quotient::DirectChatsMap additions, + Quotient::DirectChatsMap removals); + + void ignoredUsersListChanged(Quotient::IgnoredUsersList additions, + Quotient::IgnoredUsersList removals); + + void cacheStateChanged(); + void lazyLoadingChanged(); + void turnServersChanged(const QJsonObject& servers); + void devicesListLoaded(); + +#ifdef Quotient_E2EE_ENABLED + void newKeyVerificationSession(KeyVerificationSession* session); + void keyVerificationStateChanged( + const KeyVerificationSession* session, + Quotient::KeyVerificationSession::State state); + void sessionVerified(const QString& userId, const QString& deviceId); +#endif + +protected: + /** + * @brief Access the underlying ConnectionData class + */ + const ConnectionData* connectionData() const; + + /** Get a Room object for the given id in the given state + * + * Use this method when you need a Room object in the local list + * of rooms, with the given state. Note that this does not interact + * with the server; in particular, does not automatically create + * rooms on the server. This call performs necessary join state + * transitions; e.g., if it finds a room in Invite but + * `joinState == JoinState::Join` then the Invite room object + * will be deleted and a new room object with Join state created. + * In contrast, switching between Join and Leave happens within + * the same object. + * \param roomId room id (not alias!) + * \param joinState desired (target) join state of the room; if + * omitted, any state will be found and return unchanged, or a + * new Join room created. + * @return a pointer to a Room object with the specified id and the + * specified state; nullptr if roomId is empty or if roomFactory() + * failed to create a Room object. + */ + Room* provideRoom(const QString& roomId, + Omittable<JoinState> joinState = none); + + /** + * Completes loading sync data. + */ + void onSyncSuccess(SyncData&& data, bool fromCache = false); + +protected Q_SLOTS: + void syncLoopIteration(); + +private: + class Private; + ImplPtr<Private> d; + + static room_factory_t _roomFactory; + static user_factory_t _userFactory; +}; +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::DirectChatsMap) +Q_DECLARE_METATYPE(Quotient::IgnoredUsersList) diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index eb516ef7..aca218be 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -1,57 +1,107 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "connectiondata.h" -#include "networkaccessmanager.h" #include "logging.h" +#include "networkaccessmanager.h" +#include "jobs/basejob.h" -using namespace QMatrixClient; +#include <QtCore/QTimer> +#include <QtCore/QPointer> -struct ConnectionData::Private -{ - explicit Private(const QUrl& url) : baseUrl(url) { } +#include <array> +#include <queue> + +using namespace Quotient; + +class ConnectionData::Private { +public: + explicit Private(QUrl url) : baseUrl(std::move(url)) + { + rateLimiter.setSingleShot(true); + } QUrl baseUrl; QByteArray accessToken; QString lastEvent; + QString userId; QString deviceId; + std::vector<QString> needToken; mutable unsigned int txnCounter = 0; - const qint64 id = QDateTime::currentMSecsSinceEpoch(); + const qint64 txnBase = QDateTime::currentMSecsSinceEpoch(); + + QString id() const { return userId + '/' + deviceId; } + + using job_queue_t = std::queue<QPointer<BaseJob>>; + std::array<job_queue_t, 2> jobs; // 0 - foreground, 1 - background + QTimer rateLimiter; }; ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique<Private>(baseUrl)) -{ } + : d(makeImpl<Private>(std::move(baseUrl))) +{ + // Each lambda invocation below takes no more than one job from the + // queues (first foreground, then background) and resumes it; then + // restarts the rate limiter timer with duration 0, effectively yielding + // to the event loop and then resuming until both queues are empty. + QObject::connect(&d->rateLimiter, &QTimer::timeout, [this] { + // TODO: Consider moving out all job->sendRequest() invocations to + // a dedicated thread + d->rateLimiter.setInterval(0); + for (auto& q : d->jobs) + while (!q.empty()) { + const auto job = q.front(); + q.pop(); + if (!job || job->error() == BaseJob::Abandoned) + continue; + if (job->error() != BaseJob::Pending) { + qCCritical(MAIN) + << "Job" << job + << "is in the wrong status:" << job->status(); + Q_ASSERT(false); + job->setStatus(BaseJob::Pending); + } + job->sendRequest(); + d->rateLimiter.start(); + return; + } + qCDebug(MAIN) << d->id() << "job queues are empty"; + }); +} -ConnectionData::~ConnectionData() = default; +ConnectionData::~ConnectionData() +{ + d->rateLimiter.disconnect(); + d->rateLimiter.stop(); +} -QByteArray ConnectionData::accessToken() const +void ConnectionData::submit(BaseJob* job) { - return d->accessToken; + job->setStatus(BaseJob::Pending); + if (!d->rateLimiter.isActive()) { + QTimer::singleShot(0, job, &BaseJob::sendRequest); + return; + } + d->jobs[size_t(job->isBackground())].emplace(job); + qCDebug(MAIN) << job << "queued," << d->jobs.front().size() << "+" + << d->jobs.back().size() << "total jobs in" << d->id() + << "queues"; } -QUrl ConnectionData::baseUrl() const +void ConnectionData::limitRate(std::chrono::milliseconds nextCallAfter) { - return d->baseUrl; + qCDebug(MAIN) << "Jobs for" << (d->userId + "/" + d->deviceId) + << "suspended for" << nextCallAfter.count() << "ms"; + d->rateLimiter.start(nextCallAfter); } +QByteArray ConnectionData::accessToken() const { return d->accessToken; } + +QUrl ConnectionData::baseUrl() const { return d->baseUrl; } + QNetworkAccessManager* ConnectionData::nam() const { return NetworkAccessManager::instance(); @@ -68,41 +118,37 @@ 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; -} +const QString& ConnectionData::deviceId() const { return d->deviceId; } -void ConnectionData::setPort(int port) -{ - d->baseUrl.setPort(port); - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} +const QString& ConnectionData::userId() const { return d->userId; } -const QString& ConnectionData::deviceId() const +bool ConnectionData::needsToken(const QString& requestName) const { - return d->deviceId; + return std::find(d->needToken.cbegin(), d->needToken.cend(), requestName) + != d->needToken.cend(); } void ConnectionData::setDeviceId(const QString& deviceId) { d->deviceId = deviceId; - qCDebug(MAIN) << "updated deviceId to" << d->deviceId; } -QString ConnectionData::lastEvent() const +void ConnectionData::setUserId(const QString& userId) { d->userId = userId; } + +void ConnectionData::setNeedsToken(const QString& requestName) { - return d->lastEvent; + d->needToken.push_back(requestName); } +QString ConnectionData::lastEvent() const { return d->lastEvent; } + void ConnectionData::setLastEvent(QString identifier) { - d->lastEvent = identifier; + d->lastEvent = std::move(identifier); } QByteArray ConnectionData::generateTxnId() const { - return QByteArray::number(d->id) + 'q' + - QByteArray::number(++d->txnCounter); + return d->deviceId.toLatin1() + QByteArray::number(d->txnBase) + + QByteArray::number(++d->txnCounter); } diff --git a/lib/connectiondata.h b/lib/connectiondata.h index 7a2f2e90..75fc332f 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -1,55 +1,48 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "util.h" + #include <QtCore/QUrl> -#include <memory> +#include <chrono> class QNetworkAccessManager; -namespace QMatrixClient -{ - class ConnectionData - { - public: - explicit ConnectionData(QUrl baseUrl); - virtual ~ConnectionData(); - - QByteArray accessToken() const; - QUrl baseUrl() const; - const QString& deviceId() const; - - QNetworkAccessManager* nam() const; - void setBaseUrl(QUrl baseUrl); - void setToken(QByteArray accessToken); - void setHost( QString host ); - void setPort( int port ); - void setDeviceId(const QString& deviceId); - - QString lastEvent() const; - void setLastEvent( QString identifier ); - - QByteArray generateTxnId() const; - - private: - struct Private; - std::unique_ptr<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { +class BaseJob; + +class ConnectionData { +public: + explicit ConnectionData(QUrl baseUrl); + virtual ~ConnectionData(); + + void submit(BaseJob* job); + void limitRate(std::chrono::milliseconds nextCallAfter); + + QByteArray accessToken() const; + QUrl baseUrl() const; + const QString& deviceId() const; + const QString& userId() const; + bool needsToken(const QString& requestName) const; + QNetworkAccessManager* nam() const; + + void setBaseUrl(QUrl baseUrl); + void setToken(QByteArray accessToken); + void setDeviceId(const QString& deviceId); + void setUserId(const QString& userId); + void setNeedsToken(const QString& requestName); + + QString lastEvent() const; + void setLastEvent(QString identifier); + + QByteArray generateTxnId() const; + +private: + class Private; + ImplPtr<Private> d; +}; +} // namespace Quotient diff --git a/lib/converters.cpp b/lib/converters.cpp index 41a9a65e..b0e3a4b6 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -1,59 +1,41 @@ -/****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "converters.h" +#include "logging.h" #include <QtCore/QVariant> -using namespace QMatrixClient; - -QJsonValue QMatrixClient::variantToJson(const QVariant& v) +void Quotient::_impl::warnUnknownEnumValue(const QString& stringValue, + const char* enumTypeName) { - return QJsonValue::fromVariant(v); + qWarning(EVENTS).noquote() + << "Unknown" << enumTypeName << "value:" << stringValue; } -QJsonObject QMatrixClient::toJson(const QVariantMap& map) +void Quotient::_impl::reportEnumOutOfBounds(uint32_t v, const char* enumTypeName) { - return QJsonObject::fromVariantMap(map); + qCritical(MAIN).noquote() + << "Value" << v << "is out of bounds for enumeration" << enumTypeName; } -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QJsonObject QMatrixClient::toJson(const QVariantHash& hMap) +QJsonValue Quotient::JsonConverter<QVariant>::dump(const QVariant& v) { - return QJsonObject::fromVariantHash(hMap); + return QJsonValue::fromVariant(v); } -#endif -QVariant FromJson<QVariant>::operator()(const QJsonValue& jv) const +QVariant Quotient::JsonConverter<QVariant>::load(const QJsonValue& jv) { return jv.toVariant(); } -QMap<QString, QVariant> -FromJson<QMap<QString, QVariant>>::operator()(const QJsonValue& jv) const +QJsonObject Quotient::toJson(const QVariantHash& vh) { - return jv.toObject().toVariantMap(); + return QJsonObject::fromVariantHash(vh); } -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QHash<QString, QVariant> -FromJson<QHash<QString, QVariant>>::operator()(const QJsonValue& jv) const +template<> +QVariantHash Quotient::fromJson(const QJsonValue& jv) { return jv.toObject().toVariantHash(); } -#endif diff --git a/lib/converters.h b/lib/converters.h index 53855a1f..0fb36320 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -1,436 +1,544 @@ -/****************************************************************************** -* Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "omittable.h" #include "util.h" -#include <QtCore/QJsonObject> +#include <QtCore/QDate> #include <QtCore/QJsonArray> // Includes <QtCore/QJsonValue> #include <QtCore/QJsonDocument> -#include <QtCore/QDate> -#include <QtCore/QUrlQuery> +#include <QtCore/QJsonObject> #include <QtCore/QSet> +#include <QtCore/QUrlQuery> #include <QtCore/QVector> -#include <unordered_map> +#include <type_traits> #include <vector> -#if 0 // Waiting for C++17 -#include <experimental/optional> +#include <variant> +class QVariant; + +namespace Quotient { template <typename T> -using optional = std::experimental::optional<T>; -#endif +struct JsonObjectConverter { + // To be implemented in specialisations + static void dumpTo(QJsonObject&, const T&) = delete; + static void fillFrom(const QJsonObject&, T&) = delete; +}; -// Enable std::unordered_map<QString, T> -namespace std -{ - template <> struct hash<QString> - { - size_t operator()(const QString& s) const Q_DECL_NOEXCEPT - { - return qHash(s -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - , uint(qGlobalQHashSeed()) -#endif - ); +template <typename PodT, typename JsonT> +PodT fromJson(const JsonT&); + +template <typename T> +struct JsonObjectUnpacker { + // By default, revert to fromJson() so that one could provide a single + // fromJson<T, QJsonObject> specialisation instead of specialising + // the entire JsonConverter; if a different type of JSON value is needed + // (e.g., an array), specialising JsonConverter is inevitable + static T load(const QJsonValue& jv) { return fromJson<T>(jv.toObject()); } + static T load(const QJsonDocument& jd) { return fromJson<T>(jd.object()); } +}; + +//! \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 : JsonObjectUnpacker<T> { + // Unfortunately, if constexpr doesn't work with dump() and T::toJson + // because trying to check invocability of T::toJson hits a hard + // (non-SFINAE) compilation error if the member is not there. Hence a bit + // more verbose SFINAE construct in _impl::JsonExporter. + static auto dump(const T& data) + { + if constexpr (requires() { data.toJson(); }) + return data.toJson(); + else { + QJsonObject jo; + JsonObjectConverter<T>::dumpTo(jo, data); + return jo; } - }; + } + + using JsonObjectUnpacker<T>::load; + static T load(const QJsonObject& jo) + { + // 'else' below are required to suppress code generation for unused + // branches - 'return' is not enough + if constexpr (std::is_same_v<T, QJsonObject>) + return jo; + else if constexpr (std::is_constructible_v<T, QJsonObject>) + return T(jo); + else { + T pod; + JsonObjectConverter<T>::fillFrom(jo, pod); + return pod; + } + } +}; + +template <typename T> +inline auto toJson(const T& pod) +// -> can return anything from which QJsonValue or, in some cases, QJsonDocument +// is constructible +{ + if constexpr (std::is_constructible_v<QJsonValue, T>) + return pod; // No-op if QJsonValue can be directly constructed + else + return JsonConverter<T>::dump(pod); } -class QVariant; +template <typename T> +inline void fillJson(QJsonObject& json, const T& data) +{ + JsonObjectConverter<T>::dumpTo(json, data); +} -namespace QMatrixClient +template <typename PodT, typename JsonT> +inline PodT fromJson(const JsonT& json) { - // This catches anything implicitly convertible to QJsonValue/Object/Array - inline auto toJson(const QJsonValue& val) { return val; } - inline auto toJson(const QJsonObject& o) { return o; } - inline auto toJson(const QJsonArray& arr) { return arr; } - // Special-case QString to avoid ambiguity between QJsonValue - // and QVariant (also, QString.isEmpty() is used in _impl::AddNode<> below) - inline auto toJson(const QString& s) { return s; } - - inline QJsonArray toJson(const QStringList& strings) - { - return QJsonArray::fromStringList(strings); - } + // JsonT here can be whatever the respective JsonConverter specialisation + // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject + return JsonConverter<PodT>::load(json); +} + +// Convenience fromJson() overload that deduces PodT instead of requiring +// the coder to explicitly type it. It still enforces the +// overwrite-everything semantics of fromJson(), unlike fillFromJson() + +template <typename JsonT, typename PodT> +inline void fromJson(const JsonT& json, PodT& pod) +{ + pod = fromJson<PodT>(json); +} + +template <typename T> +inline void fillFromJson(const QJsonValue& jv, T& pod) +{ + if constexpr (requires() { JsonObjectConverter<T>::fillFrom({}, pod); }) { + JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); + return; + } else if (!jv.isUndefined()) + pod = fromJson<T>(jv); +} + +namespace _impl { + void warnUnknownEnumValue(const QString& stringValue, + const char* enumTypeName); + void reportEnumOutOfBounds(uint32_t v, const char* enumTypeName); +} + +//! \brief Facility string-to-enum converter +//! +//! This is to simplify enum loading from JSON - just specialise +//! Quotient::fromJson() and call this function from it, passing (aside from +//! the JSON value for the enum - that must be a string, not an int) any +//! iterable container of string'y values (const char*, QLatin1String, etc.) +//! matching respective enum values, 0-based. +//! \sa enumToJsonString +template <typename EnumT, typename EnumStringValuesT> +EnumT enumFromJsonString(const QString& s, const EnumStringValuesT& enumValues, + EnumT defaultValue) +{ + static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>); + if (const auto it = std::find(cbegin(enumValues), cend(enumValues), s); + it != cend(enumValues)) + return EnumT(it - cbegin(enumValues)); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(EnumT())); + return defaultValue; +} + +//! \brief Facility enum-to-string converter +//! +//! This does the same as enumFromJsonString, the other way around. +//! \note The source enumeration must not have gaps in values, or \p enumValues +//! has to match those gaps (i.e., if the source enumeration is defined +//! as <tt>{ Value1 = 1, Value2 = 3, Value3 = 5 }</tt> then \p enumValues +//! should be defined as <tt>{ "", "Value1", "", "Value2", "", "Value3" +//! }</tt> (mind the gap at value 0, in particular). +//! \sa enumFromJsonString +template <typename EnumT, typename EnumStringValuesT> +QString enumToJsonString(EnumT v, const EnumStringValuesT& enumValues) +{ + static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>); + if (v < size(enumValues)) + return enumValues[v]; + + _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), + qt_getEnumName(EnumT())); + Q_ASSERT(false); + return {}; +} - inline QString toJson(const QByteArray& bytes) +//! \brief Facility converter for flags +//! +//! This is very similar to enumFromJsonString, except that the target +//! enumeration is assumed to be of a 'flag' kind - i.e. its values must be +//! a power-of-two sequence starting from 1, without gaps, so exactly 1,2,4,8,16 +//! and so on. +//! \note Unlike enumFromJsonString, the values start from 1 and not from 0, +//! with 0 being used for an invalid value by default. +//! \note This function does not support flag combinations. +//! \sa QUO_DECLARE_FLAGS, QUO_DECLARE_FLAGS_NS +template <typename FlagT, typename FlagStringValuesT> +FlagT flagFromJsonString(const QString& s, const FlagStringValuesT& flagValues, + FlagT defaultValue = FlagT(0U)) +{ + // Enums based on signed integers don't make much sense for flag types + static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>); + if (const auto it = std::find(cbegin(flagValues), cend(flagValues), s); + it != cend(flagValues)) + return FlagT(1U << (it - cbegin(flagValues))); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(FlagT())); + return defaultValue; +} + +template <typename FlagT, typename FlagStringValuesT> +QString flagToJsonString(FlagT v, const FlagStringValuesT& flagValues) +{ + static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>); + if (const auto offset = + qCountTrailingZeroBits(std::underlying_type_t<FlagT>(v)); + offset < size(flagValues)) // { - return bytes.constData(); + return flagValues[offset]; } - // QVariant is outrageously omnivorous - it consumes whatever is not - // exactly matching the signature of other toJson overloads. The trick - // below disables implicit conversion to QVariant through its numerous - // non-explicit constructors. - QJsonValue variantToJson(const QVariant& v); - template <typename T> - inline auto toJson(T&& /* const QVariant& or QVariant&& */ var) - -> std::enable_if_t<std::is_same<std::decay_t<T>, QVariant>::value, - QJsonValue> + _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), + qt_getEnumName(FlagT())); + Q_ASSERT(false); + return {}; +} + +// Specialisations + +template<> +inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); } + +template <> +inline int fromJson(const QJsonValue& jv) { return jv.toInt(); } + +template <> +inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); } + +template <> +inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); } + +template <> +inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } + +template <> +inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } + +//! Use fromJson<QString> and then toLatin1()/toUtf8()/... to make QByteArray +//! +//! QJsonValue can only convert to QString and there's ambiguity whether +//! conversion to QByteArray should use (fast but very limited) toLatin1() or +//! (all encompassing and conforming to the JSON spec but slow) toUtf8(). +template <> +inline QByteArray fromJson(const QJsonValue& jv) = delete; + +template <> +inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); } + +template <> +inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); } + +inline QJsonValue toJson(const QDateTime& val) +{ + return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); +} +template <> +inline QDateTime fromJson(const QJsonValue& jv) +{ + return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); +} + +inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); } +template <> +inline QDate fromJson(const QJsonValue& jv) +{ + return fromJson<QDateTime>(jv).date(); +} + +// 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<QUrl> { + static auto load(const QJsonValue& jv) { - return variantToJson(var); + return QUrl(jv.toString()); } - QJsonObject toJson(const QMap<QString, QVariant>& map); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - QJsonObject toJson(const QHash<QString, QVariant>& hMap); -#endif - - template <typename T> - inline QJsonArray toJson(const std::vector<T>& vals) + static auto dump(const QUrl& url) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + return url.toString(QUrl::FullyEncoded); } +}; + +template <> +struct QUOTIENT_API JsonConverter<QVariant> { + static QJsonValue dump(const QVariant& v); + static QVariant load(const QJsonValue& jv); +}; + +template <typename... Ts> +inline QJsonValue toJson(const std::variant<Ts...>& v) +{ + // std::visit requires all overloads to return the same type - and + // QJsonValue is a perfect candidate for that same type (assuming that + // variants never occur on the top level in Matrix API) + return std::visit( + [](const auto& value) { return QJsonValue { toJson(value) }; }, v); +} - template <typename T> - inline QJsonArray toJson(const QVector<T>& vals) +template <typename T> +struct JsonConverter<std::variant<QString, T>> { + static std::variant<QString, T> load(const QJsonValue& jv) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + if (jv.isString()) + return fromJson<QString>(jv); + return fromJson<T>(jv); } +}; - template <typename T> - inline QJsonObject toJson(const QSet<T>& set) +template <typename T> +struct JsonConverter<Omittable<T>> { + static QJsonValue dump(const Omittable<T>& from) { - QJsonObject json; - for (auto e: set) - json.insert(toJson(e), QJsonObject{}); - return json; + return from.has_value() ? toJson(from.value()) : QJsonValue(); } - - template <typename T> - inline QJsonObject toJson(const QHash<QString, T>& hashMap) + static Omittable<T> load(const QJsonValue& jv) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + if (jv.isUndefined()) + return none; + return fromJson<T>(jv); } +}; - template <typename T> - inline QJsonObject toJson(const std::unordered_map<QString, T>& hashMap) +template <typename VectorT, typename T = typename VectorT::value_type> +struct JsonArrayConverter { + static auto dump(const VectorT& vals) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + QJsonArray ja; + for (const auto& v : vals) + ja.push_back(toJson(v)); + return ja; + } + static auto load(const QJsonArray& ja) + { + VectorT vect; + vect.reserve(typename VectorT::size_type(ja.size())); + // NB: Make sure to pass QJsonValue to fromJson<> so that it could + // hit the correct overload and not fall back to the generic fromJson + // that treats everything as an object. See also the explanation in + // the commit introducing these lines. + for (const QJsonValue v : ja) + vect.push_back(fromJson<T>(v)); + return vect; } + static auto load(const QJsonValue& jv) { return load(jv.toArray()); } + static auto load(const QJsonDocument& jd) { return load(jd.array()); } +}; - template <typename T> - struct FromJsonObject - { - T operator()(const QJsonObject& jo) const { return T(jo); } - }; +template <typename T> +struct JsonConverter<std::vector<T>> + : public JsonArrayConverter<std::vector<T>> {}; - template <typename T> - struct FromJson - { - T operator()(const QJsonValue& jv) const - { - return FromJsonObject<T>()(jv.toObject()); - } - T operator()(const QJsonDocument& jd) const - { - return FromJsonObject<T>()(jd.object()); - } - }; +#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 <typename T> - inline auto fromJson(const QJsonValue& jv) +template <> +struct JsonConverter<QStringList> : public JsonArrayConverter<QStringList> { + static auto dump(const QStringList& sl) { - return FromJson<T>()(jv); + return QJsonArray::fromStringList(sl); } +}; - template <typename T> - inline auto fromJson(const QJsonDocument& jd) +template <> +struct JsonObjectConverter<QSet<QString>> { + static void dumpTo(QJsonObject& json, const QSet<QString>& s) { - return FromJson<T>()(jd); + for (const auto& e : s) + json.insert(e, QJsonObject {}); } - - template <> struct FromJson<bool> + static void fillFrom(const QJsonObject& json, QSet<QString>& s) { - auto operator()(const QJsonValue& jv) const { return jv.toBool(); } - }; + s.reserve(s.size() + json.size()); + for (auto it = json.begin(); it != json.end(); ++it) + s.insert(it.key()); + } +}; - template <> struct FromJson<int> +template <typename HashMapT> +struct HashMapFromJson { + static void dumpTo(QJsonObject& json, const HashMapT& hashMap) { - auto operator()(const QJsonValue& jv) const { return jv.toInt(); } - }; + for (auto it = hashMap.begin(); it != hashMap.end(); ++it) + json.insert(it.key(), toJson(it.value())); + } + static void fillFrom(const QJsonObject& jo, HashMapT& h) + { + h.reserve(h.size() + jo.size()); + // NB: the QJsonValue cast below is for the same reason as in + // JsonArrayConverter + for (auto it = jo.begin(); it != jo.end(); ++it) + h[it.key()] = fromJson<typename HashMapT::mapped_type>( + QJsonValue(it.value())); + } +}; - template <> struct FromJson<double> - { - auto operator()(const QJsonValue& jv) const { return jv.toDouble(); } - }; +template <typename T, typename HashT> +struct JsonObjectConverter<std::unordered_map<QString, T, HashT>> + : public HashMapFromJson<std::unordered_map<QString, T, HashT>> {}; - template <> struct FromJson<float> - { - auto operator()(const QJsonValue& jv) const { return float(jv.toDouble()); } - }; +template <typename T> +struct JsonObjectConverter<QHash<QString, T>> + : public HashMapFromJson<QHash<QString, T>> {}; - template <> struct FromJson<qint64> - { - auto operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); } - }; +QJsonObject QUOTIENT_API toJson(const QVariantHash& vh); +template <> +QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv); - template <> struct FromJson<QString> - { - auto operator()(const QJsonValue& jv) const { return jv.toString(); } - }; +// Conditional insertion into a QJsonObject - template <> struct FromJson<QDateTime> - { - auto operator()(const QJsonValue& jv) const - { - return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); - } - }; +constexpr bool IfNotEmpty = false; - template <> struct FromJson<QDate> +namespace _impl { + template <typename ValT> + inline void addTo(QJsonObject& o, const QString& k, ValT&& v) { - auto operator()(const QJsonValue& jv) const - { - return fromJson<QDateTime>(jv).date(); - } - }; + o.insert(k, toJson(v)); + } - template <> struct FromJson<QJsonArray> + template <typename ValT> + inline void addTo(QUrlQuery& q, const QString& k, ValT&& v) { - auto operator()(const QJsonValue& jv) const - { - return jv.toArray(); - } - }; + q.addQueryItem(k, QStringLiteral("%1").arg(v)); + } - template <> struct FromJson<QByteArray> + // OpenAPI is entirely JSON-based, which means representing bools as + // textual true/false, rather than 1/0. + inline void addTo(QUrlQuery& q, const QString& k, bool v) { - auto operator()(const QJsonValue& jv) const - { - return fromJson<QString>(jv).toLatin1(); - } - }; + q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false")); + } - template <> struct FromJson<QVariant> + inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v) { - QVariant operator()(const QJsonValue& jv) const; - }; + q.addQueryItem(k, v.toEncoded()); + } - template <typename VectorT> - struct ArrayFromJson + inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) { - auto operator()(const QJsonArray& ja) const - { - using size_type = typename VectorT::size_type; - VectorT vect; vect.resize(size_type(ja.size())); - std::transform(ja.begin(), ja.end(), - vect.begin(), FromJson<typename VectorT::value_type>()); - return vect; - } - auto operator()(const QJsonValue& jv) const - { - return operator()(jv.toArray()); - } - auto operator()(const QJsonDocument& jd) const - { - return operator()(jd.array()); - } - }; - - template <typename T> - struct FromJson<std::vector<T>> : ArrayFromJson<std::vector<T>> - { }; - - template <typename T> - struct FromJson<QVector<T>> : ArrayFromJson<QVector<T>> - { }; + for (const auto& v : vals) + q.addQueryItem(k, v); + } - template <typename T> struct FromJson<QList<T>> - { - auto operator()(const QJsonValue& jv) const + // This one is for types that don't have isEmpty() and for all types + // when Force is true + template <typename ValT, bool Force = true, typename = bool> + struct AddNode { + template <typename ContT, typename ForwardedT> + static void impl(ContT& container, const QString& key, + ForwardedT&& value) { - const auto jsonArray = jv.toArray(); - QList<T> sl; sl.reserve(jsonArray.size()); - std::transform(jsonArray.begin(), jsonArray.end(), - std::back_inserter(sl), FromJson<T>()); - return sl; + addTo(container, key, std::forward<ForwardedT>(value)); } }; - template <> struct FromJson<QStringList> : FromJson<QList<QString>> { }; - - template <> struct FromJson<QMap<QString, QVariant>> - { - QMap<QString, QVariant> operator()(const QJsonValue& jv) const; - }; - - template <typename T> struct FromJson<QSet<T>> - { - auto operator()(const QJsonValue& jv) const + // This one is for types that have isEmpty() when Force is false + template <typename ValT> + struct AddNode<ValT, IfNotEmpty, decltype(std::declval<ValT>().isEmpty())> { + template <typename ContT, typename ForwardedT> + static void impl(ContT& container, const QString& key, + ForwardedT&& value) { - const auto json = jv.toObject(); - QSet<T> s; s.reserve(json.size()); - for (auto it = json.begin(); it != json.end(); ++it) - s.insert(it.key()); - return s; + if (!value.isEmpty()) + addTo(container, key, std::forward<ForwardedT>(value)); } }; - template <typename HashMapT> - struct HashMapFromJson - { - auto operator()(const QJsonObject& jo) const - { - HashMapT h; h.reserve(jo.size()); - for (auto it = jo.begin(); it != jo.end(); ++it) - h[it.key()] = - fromJson<typename HashMapT::mapped_type>(it.value()); - return h; - } - auto operator()(const QJsonValue& jv) const - { - return operator()(jv.toObject()); - } - auto operator()(const QJsonDocument& jd) const + // This one unfolds Omittable<> (also only when IfNotEmpty is requested) + template <typename ValT> + struct AddNode<Omittable<ValT>, IfNotEmpty> { + template <typename ContT, typename OmittableT> + static void impl(ContT& container, const QString& key, + const OmittableT& value) { - return operator()(jd.object()); + if (value) + addTo(container, key, *value); } }; +} // namespace _impl + +/*! Add a key-value pair to QJsonObject or QUrlQuery + * + * Adds a key-value pair(s) specified by \p key and \p value to + * \p container, optionally (in case IfNotEmpty is passed for the first + * template parameter) taking into account the value "emptiness". + * With IfNotEmpty, \p value is NOT added to the container if and only if: + * - it has a method `isEmpty()` and `value.isEmpty() == true`, or + * - it's an `Omittable<>` and `value.omitted() == true`. + * + * If \p container is a QUrlQuery, an attempt to fit \p value into it is + * made as follows: + * - if \p value is a QJsonObject, \p key is ignored and pairs from \p value + * are copied to \p container, assuming that the value in each pair + * is a string; + * - if \p value is a QStringList, it is "exploded" into a list of key-value + * pairs with key equal to \p key and value taken from each list item; + * - if \p value is a bool, its OpenAPI (i.e. JSON) representation is added + * to the query (`true` or `false`, respectively). + * + * \tparam Force add the pair even if the value is empty. This is true + * by default; passing IfNotEmpty or false for this parameter + * enables emptiness checks as described above + */ +template <bool Force = true, typename ContT, typename ValT> +inline void addParam(ContT& container, const QString& key, ValT&& value) +{ + _impl::AddNode<std::decay_t<ValT>, Force>::impl(container, key, + std::forward<ValT>(value)); +} - template <typename T> - struct FromJson<std::unordered_map<QString, T>> - : HashMapFromJson<std::unordered_map<QString, T>> - { }; - - template <typename T> - struct FromJson<QHash<QString, T>> : HashMapFromJson<QHash<QString, T>> - { }; - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - template <> struct FromJson<QHash<QString, QVariant>> - { - QHash<QString, QVariant> operator()(const QJsonValue& jv) const; - }; -#endif - - // Conditional insertion into a QJsonObject - - namespace _impl - { - template <typename ValT> - inline void addTo(QJsonObject& o, const QString& k, ValT&& v) - { o.insert(k, toJson(v)); } - - template <typename ValT> - inline void addTo(QUrlQuery& q, const QString& k, ValT&& v) - { q.addQueryItem(k, QStringLiteral("%1").arg(v)); } - - // OpenAPI is entirely JSON-based, which means representing bools as - // textual true/false, rather than 1/0. - inline void addTo(QUrlQuery& q, const QString& k, bool v) - { - q.addQueryItem(k, v ? QStringLiteral("true") - : QStringLiteral("false")); - } - - inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) - { - for (const auto& v: vals) - q.addQueryItem(k, v); - } - - inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals) - { - for (auto it = vals.begin(); it != vals.end(); ++it) - q.addQueryItem(it.key(), it.value().toString()); +// This is a facility function to convert camelCase method/variable names +// used throughout Quotient to snake_case JSON keys - see usage in +// single_key_value.h and event.h (DEFINE_CONTENT_GETTER macro). +inline auto toSnakeCase(QLatin1String s) +{ + QString result { s }; + for (auto it = result.begin(); it != result.end(); ++it) + if (it->isUpper()) { + const auto offset = static_cast<int>(it - result.begin()); + result.insert(offset, '_'); // NB: invalidates iterators + it = result.begin() + offset + 1; + *it = it->toLower(); } - - // This one is for types that don't have isEmpty() - template <typename ValT, bool Force = true, typename = bool> - struct AddNode - { - template <typename ContT, typename ForwardedT> - static void impl(ContT& container, const QString& key, - ForwardedT&& value) - { - addTo(container, key, std::forward<ForwardedT>(value)); - } - }; - - // This one is for types that have isEmpty() - template <typename ValT> - struct AddNode<ValT, false, - decltype(std::declval<ValT>().isEmpty())> - { - template <typename ContT, typename ForwardedT> - static void impl(ContT& container, const QString& key, - ForwardedT&& value) - { - if (!value.isEmpty()) - AddNode<ValT>::impl(container, - key, std::forward<ForwardedT>(value)); - } - }; - - // This is a special one that unfolds Omittable<> - template <typename ValT, bool Force> - struct AddNode<Omittable<ValT>, Force> - { - template <typename ContT, typename OmittableT> - static void impl(ContT& container, - const QString& key, const OmittableT& value) - { - if (!value.omitted()) - AddNode<ValT>::impl(container, key, value.value()); - else if (Force) // Edge case, no value but must put something - AddNode<ValT>::impl(container, key, QString{}); - } - }; - -#if 0 - // This is a special one that unfolds optional<> - template <typename ValT, bool Force> - struct AddNode<optional<ValT>, Force> - { - template <typename ContT, typename OptionalT> - static void impl(ContT& container, - const QString& key, const OptionalT& value) - { - if (value) - AddNode<ValT>::impl(container, key, value.value()); - else if (Force) // Edge case, no value but must put something - AddNode<ValT>::impl(container, key, QString{}); - } - }; -#endif - - } // namespace _impl - - static constexpr bool IfNotEmpty = false; - - template <bool Force = true, typename ContT, typename ValT> - inline void addParam(ContT& container, const QString& key, ValT&& value) - { - _impl::AddNode<std::decay_t<ValT>, Force> - ::impl(container, key, std::forward<ValT>(value)); - } -} // namespace QMatrixClient + return result; +} +} // namespace Quotient diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 5021c73a..8c71f6c5 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -4,29 +4,57 @@ #include "account-data.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetAccountDataJobName = QStringLiteral("SetAccountDataJob"); +SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, + const QJsonObject& content) + : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"), + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", + type)) +{ + setRequestData({ toJson(content) }); +} -SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) - : BaseJob(HttpVerb::Put, SetAccountDataJobName, - basePath % "/user/" % userId % "/account_data/" % type) +QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& type) { - setRequestData(Data(toJson(content))); + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v3", "/user/", + userId, "/account_data/", type)); } -static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob"); +GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) + : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"), + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", + type)) +{} + +SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, + const QString& type, + const QJsonObject& content) + : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"), + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", + roomId, "/account_data/", type)) +{ + setRequestData({ toJson(content) }); +} -SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) - : BaseJob(HttpVerb::Put, SetAccountDataPerRoomJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) +QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, + const QString& userId, + const QString& roomId, + const QString& type) { - setRequestData(Data(toJson(content))); + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v3", "/user/", + userId, "/rooms/", roomId, + "/account_data/", type)); } +GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, + const QString& type) + : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"), + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", + roomId, "/account_data/", type)) +{} diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index f3656a14..70d4e492 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -6,53 +6,122 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations - - /// Set some account_data for the user. - /// - /// 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``. - class SetAccountDataJob : public BaseJob - { - public: - /*! Set some account_data for the user. - * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. - * \param type - * The event type of the account_data to set. Custom types should be - * namespaced to avoid clashes. - * \param content - * The content of the account_data - */ - explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); - }; - - /// Set some account_data for the user. - /// - /// 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``. - class SetAccountDataPerRoomJob : public BaseJob - { - public: - /*! Set some account_data for the user. - * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. - * \param roomId - * The id of the room to set account_data on. - * \param type - * The event type of the account_data to set. Custom types should be - * namespaced to avoid clashes. - * \param content - * The content of the account_data - */ - explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); - }; -} // namespace QMatrixClient +namespace Quotient { + +/*! \brief Set some account data for the user. + * + * Set some account data for the client. This config is only visible to the user + * that set the account data. The config will be available to clients through + * the top-level `account_data` field in the homeserver response to + * [/sync](#get_matrixclientv3sync). + */ +class QUOTIENT_API SetAccountDataJob : public BaseJob { +public: + /*! \brief Set some account data for the user. + * + * \param userId + * The ID of the user to set account data for. The access token must be + * authorized to make requests for this user ID. + * + * \param type + * The event type of the account data to set. Custom types should be + * namespaced to avoid clashes. + * + * \param content + * The content of the account data. + */ + explicit SetAccountDataJob(const QString& userId, const QString& type, + const QJsonObject& content = {}); +}; + +/*! \brief Get some account data for the user. + * + * Get some account data for the client. This config is only visible to the user + * that set the account data. + */ +class QUOTIENT_API GetAccountDataJob : public BaseJob { +public: + /*! \brief Get some account data for the user. + * + * \param userId + * The ID of the user to get account data for. The access token must be + * authorized to make requests for this user ID. + * + * \param type + * The event type of the account data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataJob(const QString& userId, const QString& type); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAccountDataJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& type); +}; + +/*! \brief Set some account data for the user that is specific to a room. + * + * 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 delivered + * to clients in the per-room entries via [/sync](#get_matrixclientv3sync). + */ +class QUOTIENT_API SetAccountDataPerRoomJob : public BaseJob { +public: + /*! \brief Set some account data for the user that is specific to a room. + * + * \param userId + * The ID of the user to set account data for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to set account data on. + * + * \param type + * The event type of the account data to set. Custom types should be + * namespaced to avoid clashes. + * + * \param content + * The content of the account data. + */ + explicit SetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, const QString& type, + const QJsonObject& content = {}); +}; + +/*! \brief Get some account data for the user that is specific to a room. + * + * Get some account data for the client on a given room. This config is only + * visible to the user that set the account data. + */ +class QUOTIENT_API GetAccountDataPerRoomJob : public BaseJob { +public: + /*! \brief Get some account data for the user that is specific to a room. + * + * \param userId + * The ID of the user to get account data for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to get account data for. + * + * \param type + * The event type of the account data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, + const QString& type); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAccountDataPerRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId, const QString& type); +}; + +} // namespace Quotient diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 6066d4d9..322212db 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -4,98 +4,16 @@ #include "admin.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct FromJsonObject<GetWhoIsJob::ConnectionInfo> - { - GetWhoIsJob::ConnectionInfo operator()(const QJsonObject& jo) const - { - GetWhoIsJob::ConnectionInfo result; - result.ip = - fromJson<QString>(jo.value("ip"_ls)); - result.lastSeen = - fromJson<qint64>(jo.value("last_seen"_ls)); - result.userAgent = - fromJson<QString>(jo.value("user_agent"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<GetWhoIsJob::SessionInfo> - { - GetWhoIsJob::SessionInfo operator()(const QJsonObject& jo) const - { - GetWhoIsJob::SessionInfo result; - result.connections = - fromJson<QVector<GetWhoIsJob::ConnectionInfo>>(jo.value("connections"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<GetWhoIsJob::DeviceInfo> - { - GetWhoIsJob::DeviceInfo operator()(const QJsonObject& jo) const - { - GetWhoIsJob::DeviceInfo result; - result.sessions = - fromJson<QVector<GetWhoIsJob::SessionInfo>>(jo.value("sessions"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class GetWhoIsJob::Private -{ - public: - QString userId; - QHash<QString, DeviceInfo> devices; -}; +using namespace Quotient; QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/admin/whois/" % userId); + makePath("/_matrix/client/v3", + "/admin/whois/", userId)); } -static const auto GetWhoIsJobName = QStringLiteral("GetWhoIsJob"); - GetWhoIsJob::GetWhoIsJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetWhoIsJobName, - basePath % "/admin/whois/" % userId) - , d(new Private) -{ -} - -GetWhoIsJob::~GetWhoIsJob() = default; - -const QString& GetWhoIsJob::userId() const -{ - return d->userId; -} - -const QHash<QString, GetWhoIsJob::DeviceInfo>& GetWhoIsJob::devices() const -{ - return d->devices; -} - -BaseJob::Status GetWhoIsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->devices = fromJson<QHash<QString, DeviceInfo>>(json.value("devices"_ls)); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"), + makePath("/_matrix/client/v3", "/admin/whois/", userId)) +{} diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h index d35f3ee3..c53ddd7e 100644 --- a/lib/csapi/admin.h +++ b/lib/csapi/admin.h @@ -6,93 +6,108 @@ #include "jobs/basejob.h" -#include <QtCore/QVector> -#include <QtCore/QHash> -#include "converters.h" +namespace Quotient { + +/*! \brief Gets information about a particular user. + * + * Gets information about a particular user. + * + * This API may be restricted to only be called by the user being looked + * up, or by a server admin. Server-local administrator privileges are not + * specified in this document. + */ +class QUOTIENT_API GetWhoIsJob : public BaseJob { +public: + // Inner data structures -namespace QMatrixClient -{ - // Operations + /// Gets information about a particular user. + /// + /// This API may be restricted to only be called by the user being looked + /// up, or by a server admin. Server-local administrator privileges are not + /// specified in this document. + struct ConnectionInfo { + /// Most recently seen IP address of the session. + QString ip; + /// Unix timestamp that the session was last active. + Omittable<qint64> lastSeen; + /// User agent string last seen in the session. + QString userAgent; + }; /// Gets information about a particular user. /// + /// This API may be restricted to only be called by the user being looked + /// up, or by a server admin. Server-local administrator privileges are not + /// specified in this document. + struct SessionInfo { + /// Information particular connections in the session. + QVector<ConnectionInfo> connections; + }; + /// Gets information about a particular user. - /// + /// /// This API may be restricted to only be called by the user being looked /// up, or by a server admin. Server-local administrator privileges are not /// specified in this document. - class GetWhoIsJob : public BaseJob - { - public: - // Inner data structures - - /// Gets information about a particular user. - /// - /// This API may be restricted to only be called by the user being looked - /// up, or by a server admin. Server-local administrator privileges are not - /// specified in this document. - struct ConnectionInfo - { - /// Most recently seen IP address of the session. - QString ip; - /// Unix timestamp that the session was last active. - Omittable<qint64> lastSeen; - /// User agent string last seen in the session. - QString userAgent; - }; - - /// Gets information about a particular user. - /// - /// This API may be restricted to only be called by the user being looked - /// up, or by a server admin. Server-local administrator privileges are not - /// specified in this document. - struct SessionInfo - { - /// Information particular connections in the session. - QVector<ConnectionInfo> connections; - }; - - /// Gets information about a particular user. - /// - /// This API may be restricted to only be called by the user being looked - /// up, or by a server admin. Server-local administrator privileges are not - /// specified in this document. - struct DeviceInfo - { - /// A user's sessions (i.e. what they did with an access token from one login). - QVector<SessionInfo> sessions; - }; - - // Construction/destruction - - /*! Gets information about a particular user. - * \param userId - * The user to look up. - */ - explicit GetWhoIsJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetWhoIsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetWhoIsJob() override; - - // Result properties - - /// The Matrix user ID of the user. - const QString& userId() const; - /// Each key is an identitfier for one of the user's devices. - const QHash<QString, DeviceInfo>& devices() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct DeviceInfo { + /// A user's sessions (i.e. what they did with an access token from one + /// login). + QVector<SessionInfo> sessions; }; -} // namespace QMatrixClient + + // Construction/destruction + + /*! \brief Gets information about a particular user. + * + * \param userId + * The user to look up. + */ + explicit GetWhoIsJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetWhoIsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The Matrix user ID of the user. + QString userId() const { return loadFromJson<QString>("user_id"_ls); } + + /// Each key is an identifier for one of the user's devices. + QHash<QString, DeviceInfo> devices() const + { + return loadFromJson<QHash<QString, DeviceInfo>>("devices"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetWhoIsJob::ConnectionInfo> { + static void fillFrom(const QJsonObject& jo, + GetWhoIsJob::ConnectionInfo& result) + { + fromJson(jo.value("ip"_ls), result.ip); + fromJson(jo.value("last_seen"_ls), result.lastSeen); + fromJson(jo.value("user_agent"_ls), result.userAgent); + } +}; + +template <> +struct JsonObjectConverter<GetWhoIsJob::SessionInfo> { + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result) + { + fromJson(jo.value("connections"_ls), result.connections); + } +}; + +template <> +struct JsonObjectConverter<GetWhoIsJob::DeviceInfo> { + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result) + { + fromJson(jo.value("sessions"_ls), result.sessions); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index f62002a6..aa55d934 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -4,178 +4,97 @@ #include "administrative_contact.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct FromJsonObject<GetAccount3PIDsJob::ThirdPartyIdentifier> - { - GetAccount3PIDsJob::ThirdPartyIdentifier operator()(const QJsonObject& jo) const - { - GetAccount3PIDsJob::ThirdPartyIdentifier result; - result.medium = - fromJson<QString>(jo.value("medium"_ls)); - result.address = - fromJson<QString>(jo.value("address"_ls)); - result.validatedAt = - fromJson<qint64>(jo.value("validated_at"_ls)); - result.addedAt = - fromJson<qint64>(jo.value("added_at"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class GetAccount3PIDsJob::Private -{ - public: - QVector<ThirdPartyIdentifier> threepids; -}; +using namespace Quotient; QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/account/3pid"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/v3", "/account/3pid")); } -static const auto GetAccount3PIDsJobName = QStringLiteral("GetAccount3PIDsJob"); - GetAccount3PIDsJob::GetAccount3PIDsJob() - : BaseJob(HttpVerb::Get, GetAccount3PIDsJobName, - basePath % "/account/3pid") - , d(new Private) -{ -} - -GetAccount3PIDsJob::~GetAccount3PIDsJob() = default; + : BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"), + makePath("/_matrix/client/v3", "/account/3pid")) +{} -const QVector<GetAccount3PIDsJob::ThirdPartyIdentifier>& GetAccount3PIDsJob::threepids() const +Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) + : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), + makePath("/_matrix/client/v3", "/account/3pid")) { - return d->threepids; + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("three_pid_creds"), threePidCreds); + setRequestData({ _dataJson }); } -BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) +Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), + makePath("/_matrix/client/v3", "/account/3pid/add")) { - auto json = data.object(); - d->threepids = fromJson<QVector<ThirdPartyIdentifier>>(json.value("threepids"_ls)); - return Success; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret); + addParam<>(_dataJson, QStringLiteral("sid"), sid); + setRequestData({ _dataJson }); } -namespace QMatrixClient -{ - // Converters - - QJsonObject toJson(const Post3PIDsJob::ThreePidCredentials& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; - } -} // namespace QMatrixClient - -static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob"); - -Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind) - : BaseJob(HttpVerb::Post, Post3PIDsJobName, - basePath % "/account/3pid") -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); - addParam<IfNotEmpty>(_data, QStringLiteral("bind"), bind); - setRequestData(_data); +Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer, + const QString& idAccessToken, const QString& sid) + : BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"), + makePath("/_matrix/client/v3", "/account/3pid/bind")) +{ + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret); + addParam<>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken); + addParam<>(_dataJson, QStringLiteral("sid"), sid); + setRequestData({ _dataJson }); } -static const auto Delete3pidFromAccountJobName = QStringLiteral("Delete3pidFromAccountJob"); - -Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, const QString& address) - : BaseJob(HttpVerb::Post, Delete3pidFromAccountJobName, - basePath % "/account/3pid/delete") -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(_data); +Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer) + : BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"), + makePath("/_matrix/client/v3", "/account/3pid/delete")) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); + addExpectedKey("id_server_unbind_result"); } -class RequestTokenTo3PIDEmailJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenTo3PIDEmailJobName = QStringLiteral("RequestTokenTo3PIDEmailJob"); - -RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenTo3PIDEmailJobName, - basePath % "/account/3pid/email/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("email"), email); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); +Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer) + : BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"), + makePath("/_matrix/client/v3", "/account/3pid/unbind")) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); + addExpectedKey("id_server_unbind_result"); } -RequestTokenTo3PIDEmailJob::~RequestTokenTo3PIDEmailJob() = default; - -const Sid& RequestTokenTo3PIDEmailJob::data() const +RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( + const EmailValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"), + makePath("/_matrix/client/v3", + "/account/3pid/email/requestToken"), + false) { - return d->data; + setRequestData({ toJson(body) }); } -BaseJob::Status RequestTokenTo3PIDEmailJob::parseJson(const QJsonDocument& data) +RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( + const MsisdnValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"), + makePath("/_matrix/client/v3", + "/account/3pid/msisdn/requestToken"), + false) { - d->data = fromJson<Sid>(data); - return Success; + setRequestData({ toJson(body) }); } - -class RequestTokenTo3PIDMSISDNJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenTo3PIDMSISDNJobName = QStringLiteral("RequestTokenTo3PIDMSISDNJob"); - -RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenTo3PIDMSISDNJobName, - basePath % "/account/3pid/msisdn/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("country"), country); - addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); -} - -RequestTokenTo3PIDMSISDNJob::~RequestTokenTo3PIDMSISDNJob() = default; - -const Sid& RequestTokenTo3PIDMSISDNJob::data() const -{ - return d->data; -} - -BaseJob::Status RequestTokenTo3PIDMSISDNJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Sid>(data); - return Success; -} - diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index 3fb3d44c..27334850 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -4,234 +4,385 @@ #pragma once +#include "csapi/definitions/auth_data.h" +#include "csapi/definitions/request_email_validation.h" +#include "csapi/definitions/request_msisdn_validation.h" +#include "csapi/definitions/request_token_response.h" + #include "jobs/basejob.h" -#include "csapi/../identity/definitions/sid.h" -#include "converters.h" -#include <QtCore/QVector> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Gets a list of a user's third party identifiers. + * + * Gets a list of the third party identifiers that the homeserver has + * associated with the user's account. + * + * This is *not* the same as the list of third party identifiers bound to + * the user's Matrix ID in identity servers. + * + * Identifiers in this list may be used by the homeserver as, for example, + * identifiers that it will accept to reset the user's account password. + */ +class QUOTIENT_API GetAccount3PIDsJob : public BaseJob { +public: + // Inner data structures - /// Gets a list of a user's third party identifiers. - /// /// Gets a list of the third party identifiers that the homeserver has /// associated with the user's account. - /// + /// /// This is *not* the same as the list of third party identifiers bound to /// the user's Matrix ID in identity servers. - /// + /// /// Identifiers in this list may be used by the homeserver as, for example, /// identifiers that it will accept to reset the user's account password. - class GetAccount3PIDsJob : public BaseJob - { - public: - // Inner data structures - - /// Gets a list of the third party identifiers that the homeserver has - /// associated with the user's account. - /// - /// This is *not* the same as the list of third party identifiers bound to - /// the user's Matrix ID in identity servers. - /// - /// Identifiers in this list may be used by the homeserver as, for example, - /// identifiers that it will accept to reset the user's account password. - struct ThirdPartyIdentifier - { - /// The medium of the third party identifier. - QString medium; - /// The third party identifier address. - QString address; - /// The timestamp, in milliseconds, when the identifier was - /// validated by the identity server. - qint64 validatedAt; - /// The timestamp, in milliseconds, when the homeserver associated the third party identifier with the user. - qint64 addedAt; - }; - - // Construction/destruction - - explicit GetAccount3PIDsJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetAccount3PIDsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetAccount3PIDsJob() override; - - // Result properties - - /// Gets a list of the third party identifiers that the homeserver has - /// associated with the user's account. - /// - /// This is *not* the same as the list of third party identifiers bound to - /// the user's Matrix ID in identity servers. - /// - /// Identifiers in this list may be used by the homeserver as, for example, - /// identifiers that it will accept to reset the user's account password. - const QVector<ThirdPartyIdentifier>& threepids() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct ThirdPartyIdentifier { + /// The medium of the third party identifier. + QString medium; + /// The third party identifier address. + QString address; + /// The timestamp, in milliseconds, when the identifier was + /// validated by the identity server. + qint64 validatedAt; + /// The timestamp, in milliseconds, when the homeserver associated the + /// third party identifier with the user. + qint64 addedAt; }; - /// Adds contact information to the user's account. + // Construction/destruction + + /// Gets a list of a user's third party identifiers. + explicit GetAccount3PIDsJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAccount3PIDsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// Gets a list of the third party identifiers that the homeserver has + /// associated with the user's account. /// - /// Adds contact information to the user's account. - class Post3PIDsJob : public BaseJob + /// This is *not* the same as the list of third party identifiers bound to + /// the user's Matrix ID in identity servers. + /// + /// Identifiers in this list may be used by the homeserver as, for example, + /// identifiers that it will accept to reset the user's account password. + QVector<ThirdPartyIdentifier> threepids() const { - public: - // Inner data structures - - /// The third party credentials to associate with the account. - struct ThreePidCredentials - { - /// The client secret used in the session with the identity server. - QString clientSecret; - /// The identity server to use. - QString idServer; - /// The session identifier given by the identity server. - QString sid; - }; - - // Construction/destruction - - /*! Adds contact information to the user's account. - * \param threePidCreds - * The third party credentials to associate with the account. - * \param bind - * Whether the homeserver should also bind this third party - * identifier to the account's Matrix ID with the passed identity - * server. Default: ``false``. - */ - explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = false); - }; + return loadFromJson<QVector<ThirdPartyIdentifier>>("threepids"_ls); + } +}; - /// Deletes a third party identifier from the user's account - /// - /// Removes a third party identifier from the user's account. This might not - /// cause an unbind of the identifier from the identity server. - class Delete3pidFromAccountJob : public BaseJob +template <> +struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> { + static void fillFrom(const QJsonObject& jo, + GetAccount3PIDsJob::ThirdPartyIdentifier& result) { - public: - /*! Deletes a third party identifier from the user's account - * \param medium - * The medium of the third party identifier being removed. - * \param address - * The third party address being removed. - */ - explicit Delete3pidFromAccountJob(const QString& medium, const QString& address); + fromJson(jo.value("medium"_ls), result.medium); + fromJson(jo.value("address"_ls), result.address); + fromJson(jo.value("validated_at"_ls), result.validatedAt); + fromJson(jo.value("added_at"_ls), result.addedAt); + } +}; + +/*! \brief Adds contact information to the user's account. + * + * Adds contact information to the user's account. + * + * 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. + */ +class QUOTIENT_API Post3PIDsJob : public BaseJob { +public: + // Inner data structures + + /// The third party credentials to associate with the account. + struct ThreePidCredentials { + /// The client secret used in the session with the identity server. + QString clientSecret; + /// The identity server to use. + 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. + QString idAccessToken; + /// The session identifier given by the identity server. + QString sid; }; - /// Begins the validation process for an email address for association with the user's account. + // Construction/destruction + + /*! \brief Adds contact information to the user's account. + * + * \param threePidCreds + * The third party credentials to associate with the account. + */ + explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds); + + // Result properties + + /// 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 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. /// - /// Proxies the Identity Service API ``validate/email/requestToken``, but - /// first checks that the given email address is **not** 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. - class RequestTokenTo3PIDEmailJob : public BaseJob + /// 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 (ie: r0.5.0). + QUrl submitUrl() const { return loadFromJson<QUrl>("submit_url"_ls); } +}; + +template <> +struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> { + static void dumpTo(QJsonObject& jo, + const Post3PIDsJob::ThreePidCredentials& pod) { - public: - /*! Begins the validation process for an email address for association with the user's account. - * \param clientSecret - * 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 - * must not be empty. - * \param email - * The email address to validate. - * \param sendAttempt - * 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 - * 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 - * email (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenTo3PIDEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenTo3PIDEmailJob() override; - - // Result properties - - /// An email was sent to the given address. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("id_access_token"), pod.idAccessToken); + addParam<>(jo, QStringLiteral("sid"), pod.sid); + } +}; - /// Begins the validation process for a phone number for association with the user's account. - /// - /// Proxies the Identity Service API ``validate/msisdn/requestToken``, but - /// first checks that the given phone number is **not** 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. - class RequestTokenTo3PIDMSISDNJob : public BaseJob +/*! \brief Adds contact information to the user's account. + * + * 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 + * identity server. + * + * Homeservers should prevent the caller from adding a 3PID to their account if + * it has already been added to another user's account on the homeserver. + */ +class QUOTIENT_API Add3PIDJob : public BaseJob { +public: + /*! \brief Adds contact information to the user's account. + * + * \param clientSecret + * The client secret used in the session with the homeserver. + * + * \param sid + * The session identifier given by the homeserver. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit Add3PIDJob(const QString& clientSecret, const QString& sid, + const Omittable<AuthenticationData>& auth = none); +}; + +/*! \brief Binds a 3PID to the user's account through an Identity Service. + * + * Binds a 3PID to the user's account through the specified identity server. + * + * Homeservers should not prevent this request from succeeding if another user + * has bound the 3PID. Homeservers should simply proxy any errors received by + * the identity server to the caller. + * + * Homeservers should track successful binds so they can be unbound later. + */ +class QUOTIENT_API Bind3PIDJob : public BaseJob { +public: + /*! \brief Binds a 3PID to the user's account through an Identity Service. + * + * \param clientSecret + * The client secret used in the session with the identity server. + * + * \param idServer + * The identity server to use. + * + * \param idAccessToken + * An access token previously registered with the identity server. + * + * \param sid + * The session identifier given by the identity server. + */ + explicit Bind3PIDJob(const QString& clientSecret, const QString& idServer, + const QString& idAccessToken, const QString& sid); +}; + +/*! \brief Deletes a third party identifier from the user's account + * + * 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` + * parameter because the homeserver is expected to sign the request to the + * identity server instead. + */ +class QUOTIENT_API Delete3pidFromAccountJob : public BaseJob { +public: + /*! \brief Deletes a third party identifier from the user's account + * + * \param medium + * The medium of the third party identifier being removed. + * + * \param address + * The third party address being removed. + * + * \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`. + */ + explicit Delete3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer = {}); + + // 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 + /// 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 { - public: - /*! Begins the validation process for a phone number for association with the user's account. - * \param clientSecret - * 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 - * must not be empty. - * \param country - * The two-letter uppercase ISO country code that the number in - * ``phone_number`` should be parsed as if it were dialled from. - * \param phoneNumber - * The phone number to validate. - * \param sendAttempt - * 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`` - * 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 - * they desire a new SMS (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenTo3PIDMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenTo3PIDMSISDNJob() override; - - // Result properties - - /// An SMS message was sent to the given phone number. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<QString>("id_server_unbind_result"_ls); + } +}; + +/*! \brief Removes a user's third party identifier from an identity server. + * + * 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` + * parameter because the homeserver is expected to sign the request to the + * identity server instead. + */ +class QUOTIENT_API Unbind3pidFromAccountJob : public BaseJob { +public: + /*! \brief Removes a user's third party identifier from an identity server. + * + * \param medium + * The medium of the third party identifier being removed. + * + * \param address + * The third party address being removed. + * + * \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`. + */ + explicit Unbind3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer = {}); + + // 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 + /// refuses to support the request or the homeserver was not able to + /// determine an identity server to unbind from. + QString idServerUnbindResult() const + { + return loadFromJson<QString>("id_server_unbind_result"_ls); + } +}; + +/*! \brief Begins the validation process for an email address for association + * with the user's account. + * + * The homeserver must check that the given email address is **not** + * 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`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) + * endpoint. The homeserver should validate + * the email itself, either by sending a validation email itself or by using + * a service it has control over. + */ +class QUOTIENT_API RequestTokenTo3PIDEmailJob : public BaseJob { +public: + /*! \brief Begins the validation process for an email address for + * association with the user's account. + * + * \param body + * The homeserver must check that the given email address is **not** + * 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`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) + * 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); + + // Result properties + + /// An email was sent to the given address. Note that this may be an + /// email containing the validation token or it may be informing the + /// user of an error. + RequestTokenResponse response() const + { + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Begins the validation process for a phone number for association with + * the user's account. + * + * The homeserver must check that the given phone number is **not** + * 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`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) + * 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. + */ +class QUOTIENT_API RequestTokenTo3PIDMSISDNJob : public BaseJob { +public: + /*! \brief Begins the validation process for a phone number for association + * with the user's account. + * + * \param body + * The homeserver must check that the given phone number is **not** + * 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`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) + * 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); + + // Result properties + + /// An SMS message was sent to the given phone number. + RequestTokenResponse response() const + { + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index f40e2f05..dff7e032 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -4,22 +4,18 @@ #include "appservice_room_directory.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto UpdateAppserviceRoomDirectoryVsibilityJobName = QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"); - -UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, const QString& roomId, const QString& visibility) - : BaseJob(HttpVerb::Put, UpdateAppserviceRoomDirectoryVsibilityJobName, - basePath % "/directory/list/appservice/" % networkId % "/" % roomId) +using namespace Quotient; + +UpdateAppserviceRoomDirectoryVisibilityJob:: + UpdateAppserviceRoomDirectoryVisibilityJob(const QString& networkId, + const QString& roomId, + const QString& visibility) + : BaseJob(HttpVerb::Put, + QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"), + makePath("/_matrix/client/v3", "/directory/list/appservice/", + networkId, "/", roomId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("visibility"), visibility); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("visibility"), visibility); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index f35198b3..d6268979 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -6,36 +6,41 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Updates a room's visibility in the application service's room + * directory. + * + * Updates the visibility of a given room on the application service's room + * directory. + * + * 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. + */ +class QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob { +public: + /*! \brief Updates a room's visibility in the application service's room + * directory. + * + * \param networkId + * The protocol (network) ID to update the room list for. This would + * have been provided by the application service as being listed as + * a supported protocol. + * + * \param roomId + * The room ID to add to the directory. + * + * \param visibility + * Whether the room should be visible (public) in the directory + * or not (private). + */ + explicit UpdateAppserviceRoomDirectoryVisibilityJob( + const QString& networkId, const QString& roomId, + const QString& visibility); +}; - /// Updates a room's visibility in the application service's room directory. - /// - /// Updates the visibility of a given room on the application service's room - /// directory. - /// - /// 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. - class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob - { - public: - /*! Updates a room's visibility in the application service's room directory. - * \param networkId - * The protocol (network) ID to update the room list for. This would - * have been provided by the application service as being listed as - * a supported protocol. - * \param roomId - * The room ID to add to the directory. - * \param visibility - * Whether the room should be visible (public) in the directory - * or not (private). - */ - explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, const QString& roomId, const QString& visibility); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 4065207b..e04075b7 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -4,34 +4,26 @@ #include "banning.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto BanJobName = QStringLiteral("BanJob"); - -BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) - : BaseJob(HttpVerb::Post, BanJobName, - basePath % "/rooms/" % roomId % "/ban") +BanJob::BanJob(const QString& roomId, const QString& userId, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("BanJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/ban")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } -static const auto UnbanJobName = QStringLiteral("UnbanJob"); - -UnbanJob::UnbanJob(const QString& roomId, const QString& userId) - : BaseJob(HttpVerb::Post, UnbanJobName, - basePath % "/rooms/" % roomId % "/unban") +UnbanJob::UnbanJob(const QString& roomId, const QString& userId, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/unban")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h index 237bd2a0..e4c60ce3 100644 --- a/lib/csapi/banning.h +++ b/lib/csapi/banning.h @@ -6,47 +6,62 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Ban a user in the room. + * + * Ban a user in the room. If the user is currently in the room, also kick them. + * + * When a user is banned from a room, they may not join it or be invited to it + * until they are unbanned. + * + * The caller must have the required power level in order to perform this + * operation. + */ +class QUOTIENT_API BanJob : public BaseJob { +public: + /*! \brief Ban a user in the room. + * + * \param roomId + * The room identifier (not alias) from which the user should be banned. + * + * \param userId + * The fully qualified user ID of the user being banned. + * + * \param reason + * The reason the user has been banned. This will be supplied as the + * `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 = {}); +}; - /// Ban a user in the room. - /// - /// Ban a user in the room. If the user is currently in the room, also kick them. - /// - /// When a user is banned from a room, they may not join it or be invited to it until they are unbanned. - /// - /// The caller must have the required power level in order to perform this operation. - class BanJob : public BaseJob - { - public: - /*! Ban a user in the room. - * \param roomId - * The room identifier (not alias) from which the user should be banned. - * \param userId - * The fully qualified user ID of the user being banned. - * \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. - */ - explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {}); - }; +/*! \brief Unban a user from the room. + * + * Unban a user from the room. This allows them to be invited to the room, + * and join if they would otherwise be allowed to join according to its join + * rules. + * + * The caller must have the required power level in order to perform this + * operation. + */ +class QUOTIENT_API UnbanJob : public BaseJob { +public: + /*! \brief Unban a user from the room. + * + * \param roomId + * The room identifier (not alias) from which the user should be unbanned. + * + * \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, + const QString& reason = {}); +}; - /// Unban a user from the room. - /// - /// Unban a user from the room. This allows them to be invited to the room, - /// and join if they would otherwise be allowed to join according to its join rules. - /// - /// The caller must have the required power level in order to perform this operation. - class UnbanJob : public BaseJob - { - public: - /*! Unban a user from the room. - * \param roomId - * The room identifier (not alias) from which the user should be unbanned. - * \param userId - * The fully qualified user ID of the user being unbanned. - */ - explicit UnbanJob(const QString& roomId, const QString& userId); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp new file mode 100644 index 00000000..ca2a543f --- /dev/null +++ b/lib/csapi/capabilities.cpp @@ -0,0 +1,20 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "capabilities.h" + +using namespace Quotient; + +QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/v3", "/capabilities")); +} + +GetCapabilitiesJob::GetCapabilitiesJob() + : BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"), + makePath("/_matrix/client/v3", "/capabilities")) +{ + addExpectedKey("capabilities"); +} diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h new file mode 100644 index 00000000..81b47cd4 --- /dev/null +++ b/lib/csapi/capabilities.h @@ -0,0 +1,98 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Gets information about the server's capabilities. + * + * Gets information about the server's supported feature set + * and other relevant capabilities. + */ +class QUOTIENT_API GetCapabilitiesJob : public BaseJob { +public: + // Inner data structures + + /// Capability to indicate if the user can change their password. + struct ChangePasswordCapability { + /// True if the user can change their password, false otherwise. + bool enabled; + }; + + /// The room versions the server supports. + struct RoomVersionsCapability { + /// The default room version the server is using for new rooms. + QString defaultVersion; + /// A detailed description of the room versions the server supports. + QHash<QString, QString> available; + }; + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + struct Capabilities { + /// Capability to indicate if the user can change their password. + Omittable<ChangePasswordCapability> changePassword; + /// The room versions the server supports. + Omittable<RoomVersionsCapability> roomVersions; + /// The custom capabilities the server supports, using the + /// Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; + }; + + // Construction/destruction + + /// Gets information about the server's capabilities. + explicit GetCapabilitiesJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetCapabilitiesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + Capabilities capabilities() const + { + return loadFromJson<Capabilities>("capabilities"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetCapabilitiesJob::ChangePasswordCapability> { + static void fillFrom(const QJsonObject& jo, + GetCapabilitiesJob::ChangePasswordCapability& result) + { + fromJson(jo.value("enabled"_ls), result.enabled); + } +}; + +template <> +struct JsonObjectConverter<GetCapabilitiesJob::RoomVersionsCapability> { + static void fillFrom(const QJsonObject& jo, + GetCapabilitiesJob::RoomVersionsCapability& result) + { + fromJson(jo.value("default"_ls), result.defaultVersion); + fromJson(jo.value("available"_ls), result.available); + } +}; + +template <> +struct JsonObjectConverter<GetCapabilitiesJob::Capabilities> { + static void fillFrom(QJsonObject jo, + GetCapabilitiesJob::Capabilities& result) + { + fromJson(jo.take("m.change_password"_ls), result.changePassword); + fromJson(jo.take("m.room_versions"_ls), result.roomVersions); + fromJson(jo, result.additionalProperties); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 9b590e42..6f6738af 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -4,186 +4,88 @@ #include "content-repo.h" -#include "converters.h" +using namespace Quotient; -#include <QtNetwork/QNetworkReply> -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/media/r0"); - -class UploadContentJob::Private +auto queryToUploadContent(const QString& filename) { - public: - QString contentUri; -}; - -BaseJob::Query queryToUploadContent(const QString& filename) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("filename"), filename); return _q; } -static const auto UploadContentJobName = QStringLiteral("UploadContentJob"); - -UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) - : BaseJob(HttpVerb::Post, UploadContentJobName, - basePath % "/upload", - queryToUploadContent(filename)) - , d(new Private) +UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, + const QString& contentType) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"), + makePath("/_matrix/media/v3", "/upload"), + queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); - - setRequestData(Data(content)); -} - -UploadContentJob::~UploadContentJob() = default; - -const QString& UploadContentJob::contentUri() const -{ - return d->contentUri; + setRequestData({ content }); + addExpectedKey("content_uri"); } -BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) +auto queryToGetContent(bool allowRemote) { - auto json = data.object(); - if (!json.contains("content_uri"_ls)) - return { JsonParseError, - "The key 'content_uri' not found in the response" }; - d->contentUri = fromJson<QString>(json.value("content_uri"_ls)); - return Success; -} - -class GetContentJob::Private -{ - public: - QString contentType; - QString contentDisposition; - QIODevice* data; -}; - -BaseJob::Query queryToGetContent(bool allowRemote) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } -QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote) +QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/download/" % serverName % "/" % mediaId, - queryToGetContent(allowRemote)); + makePath("/_matrix/media/v3", "/download/", + serverName, "/", mediaId), + queryToGetContent(allowRemote)); } -static const auto GetContentJobName = QStringLiteral("GetContentJob"); - -GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote) - : BaseJob(HttpVerb::Get, GetContentJobName, - basePath % "/download/" % serverName % "/" % mediaId, - queryToGetContent(allowRemote), - {}, false) - , d(new Private) +GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, + bool allowRemote) + : BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"), + makePath("/_matrix/media/v3", "/download/", serverName, "/", + mediaId), + queryToGetContent(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); } -GetContentJob::~GetContentJob() = default; - -const QString& GetContentJob::contentType() const -{ - return d->contentType; -} - -const QString& GetContentJob::contentDisposition() const -{ - return d->contentDisposition; -} - -QIODevice* GetContentJob::data() const -{ - return d->data; -} - -BaseJob::Status GetContentJob::parseReply(QNetworkReply* reply) +auto queryToGetContentOverrideName(bool allowRemote) { - d->contentType = reply->rawHeader("Content-Type"); - d->contentDisposition = reply->rawHeader("Content-Disposition"); - d->data = reply; - return Success; -} - -class GetContentOverrideNameJob::Private -{ - public: - QString contentType; - QString contentDisposition; - QIODevice* data; -}; - -BaseJob::Query queryToGetContentOverrideName(bool allowRemote) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } -QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote) +QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, + const QString& serverName, + const QString& mediaId, + const QString& fileName, + bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, - queryToGetContentOverrideName(allowRemote)); + makePath("/_matrix/media/v3", "/download/", + serverName, "/", mediaId, "/", + fileName), + queryToGetContentOverrideName(allowRemote)); } -static const auto GetContentOverrideNameJobName = QStringLiteral("GetContentOverrideNameJob"); - -GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote) - : BaseJob(HttpVerb::Get, GetContentOverrideNameJobName, - basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, - queryToGetContentOverrideName(allowRemote), - {}, false) - , d(new Private) +GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, + const QString& mediaId, + const QString& fileName, + bool allowRemote) + : BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"), + makePath("/_matrix/media/v3", "/download/", serverName, "/", + mediaId, "/", fileName), + queryToGetContentOverrideName(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); } -GetContentOverrideNameJob::~GetContentOverrideNameJob() = default; - -const QString& GetContentOverrideNameJob::contentType() const -{ - return d->contentType; -} - -const QString& GetContentOverrideNameJob::contentDisposition() const -{ - return d->contentDisposition; -} - -QIODevice* GetContentOverrideNameJob::data() const -{ - return d->data; -} - -BaseJob::Status GetContentOverrideNameJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->contentDisposition = reply->rawHeader("Content-Disposition"); - d->data = reply; - return Success; -} - -class GetContentThumbnailJob::Private -{ - public: - QString contentType; - QIODevice* data; -}; - -BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& method, bool allowRemote) +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); @@ -191,128 +93,62 @@ BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& return _q; } -QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method, bool allowRemote) +QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, + const QString& serverName, + const QString& mediaId, int width, + int height, const QString& method, + bool allowRemote) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thumbnail/" % serverName % "/" % mediaId, - queryToGetContentThumbnail(width, height, method, allowRemote)); + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/", mediaId), + queryToGetContentThumbnail(width, height, method, allowRemote)); } -static const auto GetContentThumbnailJobName = QStringLiteral("GetContentThumbnailJob"); - -GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method, bool allowRemote) - : BaseJob(HttpVerb::Get, GetContentThumbnailJobName, - basePath % "/thumbnail/" % serverName % "/" % mediaId, - queryToGetContentThumbnail(width, height, method, allowRemote), - {}, false) - , d(new Private) +GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, + const QString& mediaId, + int width, int height, + const QString& method, + bool allowRemote) + : BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"), + makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/", + mediaId), + queryToGetContentThumbnail(width, height, method, allowRemote), + {}, false) { setExpectedContentTypes({ "image/jpeg", "image/png" }); } -GetContentThumbnailJob::~GetContentThumbnailJob() = default; - -const QString& GetContentThumbnailJob::contentType() const -{ - return d->contentType; -} - -QIODevice* GetContentThumbnailJob::data() const -{ - return d->data; -} - -BaseJob::Status GetContentThumbnailJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->data = reply; - return Success; -} - -class GetUrlPreviewJob::Private +auto queryToGetUrlPreview(const QUrl& url, Omittable<qint64> ts) { - public: - Omittable<qint64> matrixImageSize; - QString ogImage; -}; - -BaseJob::Query queryToGetUrlPreview(const QString& 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, Omittable<qint64> ts) +QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url, + Omittable<qint64> ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/preview_url", - queryToGetUrlPreview(url, ts)); -} - -static const auto GetUrlPreviewJobName = QStringLiteral("GetUrlPreviewJob"); - -GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, Omittable<qint64> ts) - : BaseJob(HttpVerb::Get, GetUrlPreviewJobName, - basePath % "/preview_url", - queryToGetUrlPreview(url, ts)) - , d(new Private) -{ + makePath("/_matrix/media/v3", + "/preview_url"), + queryToGetUrlPreview(url, ts)); } -GetUrlPreviewJob::~GetUrlPreviewJob() = default; - -Omittable<qint64> GetUrlPreviewJob::matrixImageSize() const -{ - return d->matrixImageSize; -} - -const QString& GetUrlPreviewJob::ogImage() const -{ - return d->ogImage; -} - -BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->matrixImageSize = fromJson<qint64>(json.value("matrix:image:size"_ls)); - d->ogImage = fromJson<QString>(json.value("og:image"_ls)); - return Success; -} - -class GetConfigJob::Private -{ - public: - Omittable<qint64> uploadSize; -}; +GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts) + : BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"), + makePath("/_matrix/media/v3", "/preview_url"), + queryToGetUrlPreview(url, ts)) +{} QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/config"); + makePath("/_matrix/media/v3", "/config")); } -static const auto GetConfigJobName = QStringLiteral("GetConfigJob"); - GetConfigJob::GetConfigJob() - : BaseJob(HttpVerb::Get, GetConfigJobName, - basePath % "/config") - , d(new Private) -{ -} - -GetConfigJob::~GetConfigJob() = default; - -Omittable<qint64> GetConfigJob::uploadSize() const -{ - return d->uploadSize; -} - -BaseJob::Status GetConfigJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->uploadSize = fromJson<qint64>(json.value("m.upload.size"_ls)); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"), + makePath("/_matrix/media/v3", "/config")) +{} diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index 5ef2e0d6..2ba66a35 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -6,255 +6,274 @@ #include "jobs/basejob.h" -#include "converters.h" #include <QtCore/QIODevice> +#include <QtNetwork/QNetworkReply> -namespace QMatrixClient -{ - // Operations +namespace Quotient { - /// Upload some content to the content repository. - class UploadContentJob : public BaseJob - { - public: - /*! Upload some content to the content repository. - * \param content - * \param filename - * The name of the file being uploaded - * \param contentType - * The content type of the file being uploaded - */ - explicit UploadContentJob(QIODevice* content, const QString& filename = {}, const QString& contentType = {}); - ~UploadContentJob() override; - - // Result properties - - /// The MXC URI to the uploaded content. - const QString& contentUri() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download content from the content repository. - class GetContentJob : public BaseJob - { - public: - /*! Download content from the content repository. - * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) - * \param mediaId - * 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 if it is deemed - * remote. This is to prevent routing loops where the server contacts itself. Defaults to - * true if not provided. - */ - explicit GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote = true); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetContentJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote = true); - - ~GetContentJob() override; - - // Result properties - - /// The content type of the file that was previously uploaded. - const QString& contentType() const; - /// The name of the file that was previously uploaded, if set. - const QString& contentDisposition() const; - /// The content that was previously uploaded. - QIODevice* data() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download content from the content repository as a given filename. - class GetContentOverrideNameJob : public BaseJob +/*! \brief Upload some content to the content repository. + * + */ +class QUOTIENT_API UploadContentJob : public BaseJob { +public: + /*! \brief Upload some content to the content repository. + * + * \param content + * The content to be uploaded. + * + * \param filename + * The name of the file being uploaded + * + * \param contentType + * The content type of the file being uploaded + */ + explicit UploadContentJob(QIODevice* content, const QString& filename = {}, + const QString& contentType = {}); + + // Result properties + + /// 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. + * + */ +class QUOTIENT_API GetContentJob : public BaseJob { +public: + /*! \brief Download content from the content repository. + * + * \param serverName + * The server name from the `mxc://` URI (the authoritory component) + * + * \param mediaId + * 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 + * if it is deemed remote. This is to prevent routing loops where the server + * contacts itself. Defaults to true if not provided. + */ + explicit GetContentJob(const QString& serverName, const QString& mediaId, + bool allowRemote = true); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetContentJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, bool allowRemote = true); + + // Result properties + + /// The content type of the file that was previously uploaded. + QString contentType() const { return reply()->rawHeader("Content-Type"); } + + /// The name of the file that was previously uploaded, if set. + QString contentDisposition() const { - public: - /*! Download content from the content repository as a given filename. - * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) - * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) - * \param fileName - * The filename to give in the Content-Disposition - * \param allowRemote - * Indicates to the server that it should not attempt to fetch the media if it is deemed - * remote. This is to prevent routing loops where the server contacts itself. Defaults to - * true if not provided. - */ - explicit GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote = true); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetContentOverrideNameJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote = true); - - ~GetContentOverrideNameJob() override; - - // Result properties - - /// The content type of the file that was previously uploaded. - const QString& contentType() const; - /// The name of file given in the request - const QString& contentDisposition() const; - /// The content that was previously uploaded. - QIODevice* data() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download a thumbnail of the content from the content repository. - class GetContentThumbnailJob : public BaseJob + return reply()->rawHeader("Content-Disposition"); + } + + /// The content that was previously uploaded. + QIODevice* data() { return reply(); } +}; + +/*! \brief Download content from the content repository overriding the file name + * + * This will download content from the content repository (same as + * the previous endpoint) but replace the target file name with the one + * provided by the caller. + */ +class QUOTIENT_API GetContentOverrideNameJob : public BaseJob { +public: + /*! \brief Download content from the content repository overriding the file + * name + * + * \param serverName + * The server name from the `mxc://` URI (the authoritory component) + * + * \param mediaId + * The media ID from the `mxc://` URI (the path component) + * + * \param fileName + * A filename to give in the `Content-Disposition` header. + * + * \param allowRemote + * Indicates to the server that it should not attempt to fetch the media + * if it is deemed remote. This is to prevent routing loops where the server + * contacts itself. Defaults to true if not provided. + */ + explicit GetContentOverrideNameJob(const QString& serverName, + const QString& mediaId, + const QString& fileName, + bool allowRemote = true); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetContentOverrideNameJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, const QString& fileName, + bool allowRemote = true); + + // Result properties + + /// 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 + /// uploaded, if set. + QString contentDisposition() const { - public: - /*! Download a thumbnail of the content from the content repository. - * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) - * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) - * \param width - * The *desired* width of the thumbnail. The actual thumbnail may not - * match the size specified. - * \param height - * The *desired* height of the thumbnail. The actual thumbnail may not - * match the size specified. - * \param method - * The desired resizing method. - * \param allowRemote - * Indicates to the server that it should not attempt to fetch the media if it is deemed - * remote. This is to prevent routing loops where the server contacts itself. Defaults to - * true if not provided. - */ - explicit GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method = {}, bool allowRemote = true); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetContentThumbnailJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method = {}, bool allowRemote = true); - - ~GetContentThumbnailJob() override; - - // Result properties - - /// The content type of the thumbnail. - const QString& contentType() const; - /// A thumbnail of the requested content. - QIODevice* data() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get information about a URL for a client - class GetUrlPreviewJob : public BaseJob + return reply()->rawHeader("Content-Disposition"); + } + + /// The content that was previously uploaded. + QIODevice* data() { return reply(); } +}; + +/*! \brief Download a thumbnail of content from the content repository + * + * Download a thumbnail of content from the content repository. + * See the [Thumbnails](/client-server-api/#thumbnails) section for more + * information. + */ +class QUOTIENT_API 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) + * + * \param mediaId + * The media ID from the `mxc://` URI (the path component) + * + * \param width + * The *desired* width of the thumbnail. The actual thumbnail may be + * larger than the size specified. + * + * \param height + * The *desired* height of the thumbnail. The actual thumbnail may be + * larger than the size specified. + * + * \param method + * 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 + * the media if it is deemed remote. This is to prevent routing loops + * where the server contacts itself. Defaults to true if not provided. + */ + explicit GetContentThumbnailJob(const QString& serverName, + const QString& mediaId, int width, + int height, const QString& method = {}, + bool allowRemote = true); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetContentThumbnailJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, int width, int height, + const QString& method = {}, + bool allowRemote = true); + + // Result properties + + /// The content type of the thumbnail. + QString contentType() const { return reply()->rawHeader("Content-Type"); } + + /// A thumbnail of the requested content. + QIODevice* data() { return reply(); } +}; + +/*! \brief Get information about a URL for a client + * + * 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. + */ +class QUOTIENT_API GetUrlPreviewJob : public BaseJob { +public: + /*! \brief Get information about a URL for a client + * + * \param url + * The URL to get a preview of. + * + * \param ts + * The preferred point in time to return a preview for. The server may + * return a newer version if it does not have the requested version + * available. + */ + 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 QUrl& url, + Omittable<qint64> ts = none); + + // Result properties + + /// The byte-size of the image. Omitted if there is no image attached. + Omittable<qint64> matrixImageSize() const { - public: - /*! Get information about a URL for a client - * \param url - * The URL to get a preview of - * \param ts - * The preferred point in time to return a preview for. The server may - * return a newer version if it does not have the requested version - * available. - */ - explicit GetUrlPreviewJob(const QString& url, Omittable<qint64> ts = none); - - /*! 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, Omittable<qint64> ts = none); - - ~GetUrlPreviewJob() override; - - // Result properties - - /// The byte-size of the image. Omitted if there is no image attached. - Omittable<qint64> matrixImageSize() const; - /// An MXC URI to the image. Omitted if there is no image. - const QString& ogImage() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - + return loadFromJson<Omittable<qint64>>("matrix:image:size"_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. + * + * This endpoint allows clients to retrieve the configuration of the content + * repository, such as upload limitations. + * Clients SHOULD use this as a guide when using content repository endpoints. + * All values are intentionally left optional. Clients SHOULD follow + * the advice given in the field description when the field is not available. + * + * **NOTE:** Both clients and server administrators should be aware that proxies + * between the client and the server may affect the apparent behaviour of + * content repository APIs, for example, proxies may enforce a lower upload size + * limit than is advertised by the server on this endpoint. + */ +class QUOTIENT_API GetConfigJob : public BaseJob { +public: /// Get the configuration for the content repository. - /// - /// This endpoint allows clients to retrieve the configuration of the content - /// repository, such as upload limitations. - /// Clients SHOULD use this as a guide when using content repository endpoints. - /// All values are intentionally left optional. Clients SHOULD follow - /// the advice given in the field description when the field is not available. - /// - /// **NOTE:** Both clients and server administrators should be aware that proxies - /// between the client and the server may affect the apparent behaviour of content - /// repository APIs, for example, proxies may enforce a lower upload size limit - /// than is advertised by the server on this endpoint. - class GetConfigJob : public BaseJob + explicit GetConfigJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetConfigJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// The maximum size an upload can be in bytes. + /// Clients SHOULD use this as a guide when uploading content. + /// If not listed or null, the size limit should be treated as unknown. + Omittable<qint64> uploadSize() const { - public: - explicit GetConfigJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetConfigJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetConfigJob() override; - - // Result properties - - /// The maximum size an upload can be in bytes. - /// Clients SHOULD use this as a guide when uploading content. - /// If not listed or null, the size limit should be treated as unknown. - Omittable<qint64> uploadSize() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<Omittable<qint64>>("m.upload.size"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 36f83727..afae80af 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -4,80 +4,38 @@ #include "create_room.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - QJsonObject toJson(const CreateRoomJob::Invite3pid& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("medium"), pod.medium); - addParam<>(jo, QStringLiteral("address"), pod.address); - return jo; - } - - QJsonObject toJson(const CreateRoomJob::StateEvent& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); - addParam<>(jo, QStringLiteral("content"), pod.content); - return jo; - } -} // namespace QMatrixClient - -class CreateRoomJob::Private +using namespace Quotient; + +CreateRoomJob::CreateRoomJob(const QString& visibility, + const QString& roomAliasName, const QString& name, + const QString& topic, const QStringList& invite, + const QVector<Invite3pid>& invite3pid, + const QString& roomVersion, + const QJsonObject& creationContent, + const QVector<StateEvent>& initialState, + const QString& preset, Omittable<bool> isDirect, + const QJsonObject& powerLevelContentOverride) + : BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"), + makePath("/_matrix/client/v3", "/createRoom")) { - public: - QString roomId; -}; - -static const auto CreateRoomJobName = QStringLiteral("CreateRoomJob"); - -CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, bool isDirect, const QJsonObject& powerLevelContentOverride) - : BaseJob(HttpVerb::Post, CreateRoomJobName, - basePath % "/createRoom") - , d(new Private) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); - addParam<IfNotEmpty>(_data, QStringLiteral("room_alias_name"), roomAliasName); - addParam<IfNotEmpty>(_data, QStringLiteral("name"), name); - addParam<IfNotEmpty>(_data, QStringLiteral("topic"), topic); - addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite); - addParam<IfNotEmpty>(_data, QStringLiteral("invite_3pid"), invite3pid); - addParam<IfNotEmpty>(_data, QStringLiteral("room_version"), roomVersion); - addParam<IfNotEmpty>(_data, QStringLiteral("creation_content"), creationContent); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_state"), initialState); - addParam<IfNotEmpty>(_data, QStringLiteral("preset"), preset); - addParam<IfNotEmpty>(_data, QStringLiteral("is_direct"), isDirect); - addParam<IfNotEmpty>(_data, QStringLiteral("power_level_content_override"), powerLevelContentOverride); - setRequestData(_data); -} - -CreateRoomJob::~CreateRoomJob() = default; - -const QString& CreateRoomJob::roomId() const -{ - return d->roomId; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("visibility"), visibility); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("room_alias_name"), + roomAliasName); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("name"), name); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("topic"), topic); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("invite"), invite); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("invite_3pid"), invite3pid); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("room_version"), roomVersion); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("creation_content"), + creationContent); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("initial_state"), + initialState); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("preset"), preset); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("is_direct"), isDirect); + addParam<IfNotEmpty>(_dataJson, + QStringLiteral("power_level_content_override"), + powerLevelContentOverride); + setRequestData({ _dataJson }); + addExpectedKey("room_id"); } - -BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("room_id"_ls)) - return { JsonParseError, - "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); - return Success; -} - diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index a0a64df0..336b9767 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -6,229 +6,300 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> -#include "converters.h" -#include <QtCore/QVector> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Create a new room + * + * Create a new room with various configuration options. + * + * The server MUST apply the normal state resolution rules when creating + * 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 + * room. + * + * 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 + * (and not other members) permission to send state events. Overridden + * by the `power_level_content_override` parameter. + * + * 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + * + * 5. Events set by the `preset`. Currently these are the `m.room.join_rules`, + * `m.room.history_visibility`, and `m.room.guest_access` state events. + * + * 6. Events listed in `initial_state`, in the order that they are + * listed. + * + * 7. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic` + * state events). + * + * 8. 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` | | + * + * 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`. + */ +class QUOTIENT_API CreateRoomJob : public BaseJob { +public: + // Inner data structures - /// Create a new room - /// /// Create a new room with various configuration options. - /// + /// /// The server MUST apply the normal state resolution rules when creating /// the new room, including checking power levels for each event. It MUST /// apply the events implied by the request in the following order: - /// - /// 0. A default ``m.room.power_levels`` event, giving the room creator + /// + /// 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 + /// needed so the remaining events can be sent. + /// + /// 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. - /// - /// 1. Events set by the ``preset``. Currently these are the ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state events. - /// - /// 2. Events listed in ``initial_state``, in the order that they are + /// by the `power_level_content_override` parameter. + /// + /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + /// + /// 5. Events set by the `preset`. Currently these are the + /// `m.room.join_rules`, + /// `m.room.history_visibility`, and `m.room.guest_access` state events. + /// + /// 6. Events listed in `initial_state`, in the order that they are /// listed. - /// - /// 3. Events implied by ``name`` and ``topic`` (``m.room.name`` and ``m.room.topic`` + /// + /// 7. Events implied by `name` and `topic` (`m.room.name` and + /// `m.room.topic` /// state events). - /// - /// 4. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member`` with - /// ``membership: invite`` and ``m.room.third_party_invite``). - /// + /// + /// 8. 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`` - /// ======================== ============== ====================== ================ ========= - /// - /// The server will create a ``m.room.create`` event in the room with the + /// + /// | 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 /// requesting user as the creator, alongside other keys provided in the - /// ``creation_content``. - class CreateRoomJob : public BaseJob - { - public: - // Inner data structures + /// `creation_content`. + struct Invite3pid { + /// The hostname+port of the identity server which should be used for + /// third party identifier lookups. + 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. + QString idAccessToken; + /// The kind of address being passed in the address field, for example + /// `email`. + QString medium; + /// The invitee's third party identifier. + QString address; + }; - /// Create a new room with various configuration options. - /// - /// The server MUST apply the normal state resolution rules when creating - /// the new room, including checking power levels for each event. It MUST - /// apply the events implied by the request in the following order: - /// - /// 0. 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. - /// - /// 1. Events set by the ``preset``. Currently these are the ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state events. - /// - /// 2. Events listed in ``initial_state``, in the order that they are - /// listed. - /// - /// 3. Events implied by ``name`` and ``topic`` (``m.room.name`` and ``m.room.topic`` - /// state events). - /// - /// 4. 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`` - /// ======================== ============== ====================== ================ ========= - /// - /// 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``. - struct Invite3pid - { - /// The hostname+port of the identity server which should be used for third party identifier lookups. - QString idServer; - /// The kind of address being passed in the address field, for example ``email``. - QString medium; - /// The invitee's third party identifier. - QString address; - }; + /// Create a new room with various configuration options. + /// + /// The server MUST apply the normal state resolution rules when creating + /// 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 + /// room. + /// + /// 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 + /// (and not other members) permission to send state events. Overridden + /// by the `power_level_content_override` parameter. + /// + /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + /// + /// 5. Events set by the `preset`. Currently these are the + /// `m.room.join_rules`, + /// `m.room.history_visibility`, and `m.room.guest_access` state events. + /// + /// 6. Events listed in `initial_state`, in the order that they are + /// listed. + /// + /// 7. Events implied by `name` and `topic` (`m.room.name` and + /// `m.room.topic` + /// state events). + /// + /// 8. 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` | | + /// + /// 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`. + struct StateEvent { + /// The type of event to send. + QString type; + /// The state_key of the state event. Defaults to an empty string. + QString stateKey; + /// The content of the event. + QJsonObject content; + }; - /// Create a new room with various configuration options. - /// - /// The server MUST apply the normal state resolution rules when creating - /// the new room, including checking power levels for each event. It MUST - /// apply the events implied by the request in the following order: - /// - /// 0. 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. - /// - /// 1. Events set by the ``preset``. Currently these are the ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state events. - /// - /// 2. Events listed in ``initial_state``, in the order that they are - /// listed. - /// - /// 3. Events implied by ``name`` and ``topic`` (``m.room.name`` and ``m.room.topic`` - /// state events). - /// - /// 4. 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`` - /// ======================== ============== ====================== ================ ========= - /// - /// 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``. - struct StateEvent - { - /// The type of event to send. - QString type; - /// The state_key of the state event. Defaults to an empty string. - QString stateKey; - /// The content of the event. - QJsonObject content; - }; + // Construction/destruction - // Construction/destruction + /*! \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 + * 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`. + * + * \param roomAliasName + * The desired room alias **local part**. If this is included, a + * room alias will be created and mapped to the newly created + * 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`. + * + * The complete room alias will become the canonical alias for + * the room and an `m.room.canonical_alias` event will be sent + * into the room. + * + * \param name + * 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`. + * + * \param topic + * 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`. + * + * \param invite + * A list of user IDs to invite to the room. This will tell the + * server to invite everyone in the list to the newly created room. + * + * \param invite3pid + * A list of objects representing third party IDs to invite into + * the room. + * + * \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. + * + * \param creationContent + * 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 overwrite the following keys: `creator`, `room_version`. + * Future versions of the specification may allow the server to overwrite + * other keys. + * + * \param initialState + * A list of state events to set in the new room. This allows + * the user to override the default state events set in the new + * 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 + * overridden 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`. + * + * \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](/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`](/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 = {}, + const QString& name = {}, const QString& topic = {}, + const QStringList& invite = {}, + const QVector<Invite3pid>& invite3pid = {}, + const QString& roomVersion = {}, + const QJsonObject& creationContent = {}, + const QVector<StateEvent>& initialState = {}, + const QString& preset = {}, + Omittable<bool> isDirect = none, + const QJsonObject& powerLevelContentOverride = {}); - /*! 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 - * 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``. - * \param roomAliasName - * The desired room alias **local part**. If this is included, a - * room alias will be created and mapped to the newly created - * 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``. - * - * 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 - * into the room to indicate the name of the room. See Room - * Events for more information on ``m.room.name``. - * \param topic - * 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``. - * \param invite - * A list of user IDs to invite to the room. This will tell the - * server to invite everyone in the list to the newly created room. - * \param invite3pid - * A list of objects representing third party IDs to invite into - * the room. - * \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. - * \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. - * \param initialState - * A list of state events to set in the new room. This allows - * the user to override the default state events set in the new - * 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. - * \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``. - * \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. - * \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. - */ - explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, bool isDirect = false, const QJsonObject& powerLevelContentOverride = {}); - ~CreateRoomJob() override; + // Result properties - // Result properties + /// The created room's ID. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } +}; - /// The created room's ID. - const QString& roomId() const; +template <> +struct JsonObjectConverter<CreateRoomJob::Invite3pid> { + static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod) + { + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("id_access_token"), pod.idAccessToken); + addParam<>(jo, QStringLiteral("medium"), pod.medium); + addParam<>(jo, QStringLiteral("address"), pod.address); + } +}; - protected: - Status parseJson(const QJsonDocument& data) override; +template <> +struct JsonObjectConverter<CreateRoomJob::StateEvent> { + static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod) + { + addParam<>(jo, QStringLiteral("type"), pod.type); + addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); + addParam<>(jo, QStringLiteral("content"), pod.content); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp new file mode 100644 index 00000000..83136d71 --- /dev/null +++ b/lib/csapi/cross_signing.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * 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, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), + makePath("/_matrix/client/v3", "/keys/device_signing/upload")) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("master_key"), masterKey); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("self_signing_key"), + selfSigningKey); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("user_signing_key"), + userSigningKey); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); +} + +UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( + const QHash<QString, QHash<QString, QJsonObject>>& signatures) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), + makePath("/_matrix/client/v3", "/keys/signatures/upload")) +{ + setRequestData({ toJson(signatures) }); +} diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h new file mode 100644 index 00000000..6cea73e6 --- /dev/null +++ b/lib/csapi/cross_signing.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "csapi/definitions/auth_data.h" +#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 QUOTIENT_API 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. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit UploadCrossSigningKeysJob( + const Omittable<CrossSigningKey>& masterKey = none, + const Omittable<CrossSigningKey>& selfSigningKey = none, + const Omittable<CrossSigningKey>& userSigningKey = none, + const Omittable<AuthenticationData>& auth = 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 QUOTIENT_API 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/auth_data.cpp b/lib/csapi/definitions/auth_data.cpp deleted file mode 100644 index f8639432..00000000 --- a/lib/csapi/definitions/auth_data.cpp +++ /dev/null @@ -1,28 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "auth_data.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const AuthenticationData& pod) -{ - QJsonObject jo = toJson(pod.authInfo); - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session); - return jo; -} - -AuthenticationData FromJsonObject<AuthenticationData>::operator()(QJsonObject jo) const -{ - AuthenticationData result; - result.type = - fromJson<QString>(jo.take("type"_ls)); - result.session = - fromJson<QString>(jo.take("session"_ls)); - - result.authInfo = fromJson<QHash<QString, QJsonObject>>(jo); - return result; -} - diff --git a/lib/csapi/definitions/auth_data.h b/lib/csapi/definitions/auth_data.h index 661d3e5f..a9972323 100644 --- a/lib/csapi/definitions/auth_data.h +++ b/lib/csapi/definitions/auth_data.h @@ -6,29 +6,37 @@ #include "converters.h" -#include <QtCore/QJsonObject> -#include <QtCore/QHash> - -namespace QMatrixClient -{ - // Data structures - - /// Used by clients to submit authentication information to the interactive-authentication API - struct AuthenticationData +namespace Quotient { +/// Used by clients to submit authentication information to the +/// interactive-authentication API +struct AuthenticationData { + /// The authentication type that the client is attempting to complete. + /// May be omitted if `session` is given, and the client is reissuing a + /// request which it believes has been completed out-of-band (for example, + /// via the [fallback mechanism](#fallback)). + QString type; + + /// The value of the session key given by the homeserver. + QString session; + + /// Keys dependent on the login type + QHash<QString, QJsonObject> authInfo; +}; + +template <> +struct JsonObjectConverter<AuthenticationData> { + static void dumpTo(QJsonObject& jo, const AuthenticationData& pod) { - /// The login type that the client is attempting to complete. - QString type; - /// The value of the session key given by the homeserver. - QString session; - /// Keys dependent on the login type - QHash<QString, QJsonObject> authInfo; - }; - - QJsonObject toJson(const AuthenticationData& pod); - - template <> struct FromJsonObject<AuthenticationData> + fillJson(jo, pod.authInfo); + addParam<IfNotEmpty>(jo, QStringLiteral("type"), pod.type); + addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session); + } + static void fillFrom(QJsonObject jo, AuthenticationData& pod) { - AuthenticationData operator()(QJsonObject jo) const; - }; + fromJson(jo.take("type"_ls), pod.type); + fromJson(jo.take("session"_ls), pod.session); + fromJson(jo, pod.authInfo); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/client_device.cpp b/lib/csapi/definitions/client_device.cpp deleted file mode 100644 index 4a192f85..00000000 --- a/lib/csapi/definitions/client_device.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "client_device.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const Device& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); - addParam<IfNotEmpty>(jo, QStringLiteral("display_name"), pod.displayName); - addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); - addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); - return jo; -} - -Device FromJsonObject<Device>::operator()(const QJsonObject& jo) const -{ - Device result; - result.deviceId = - fromJson<QString>(jo.value("device_id"_ls)); - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.lastSeenIp = - fromJson<QString>(jo.value("last_seen_ip"_ls)); - result.lastSeenTs = - fromJson<qint64>(jo.value("last_seen_ts"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/client_device.h b/lib/csapi/definitions/client_device.h index 9f10888a..a5ab1bfc 100644 --- a/lib/csapi/definitions/client_device.h +++ b/lib/csapi/definitions/client_device.h @@ -6,34 +6,43 @@ #include "converters.h" -#include "converters.h" - -namespace QMatrixClient -{ - // Data structures - - /// A client device - struct Device +namespace Quotient { +/// A client device +struct Device { + /// Identifier of this device. + QString deviceId; + + /// Display name set by the user for this device. Absent if no name has been + /// set. + QString displayName; + + /// The IP address where this device was last seen. (May be a few minutes + /// out of date, for efficiency reasons). + QString lastSeenIp; + + /// The timestamp (in milliseconds since the unix epoch) when this devices + /// was last seen. (May be a few minutes out of date, for efficiency + /// reasons). + Omittable<qint64> lastSeenTs; +}; + +template <> +struct JsonObjectConverter<Device> { + static void dumpTo(QJsonObject& jo, const Device& pod) { - /// Identifier of this device. - QString deviceId; - /// Display name set by the user for this device. Absent if no name has been - /// set. - QString displayName; - /// The IP address where this device was last seen. (May be a few minutes out - /// of date, for efficiency reasons). - QString lastSeenIp; - /// The timestamp (in milliseconds since the unix epoch) when this devices - /// was last seen. (May be a few minutes out of date, for efficiency - /// reasons). - Omittable<qint64> lastSeenTs; - }; - - QJsonObject toJson(const Device& pod); - - template <> struct FromJsonObject<Device> + addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); + addParam<IfNotEmpty>(jo, QStringLiteral("display_name"), + pod.displayName); + addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); + addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); + } + static void fillFrom(const QJsonObject& jo, Device& pod) { - Device operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient + fromJson(jo.value("device_id"_ls), pod.deviceId); + fromJson(jo.value("display_name"_ls), pod.displayName); + fromJson(jo.value("last_seen_ip"_ls), pod.lastSeenIp); + fromJson(jo.value("last_seen_ts"_ls), pod.lastSeenTs); + } +}; + +} // 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.cpp b/lib/csapi/definitions/device_keys.cpp deleted file mode 100644 index a0e0ca42..00000000 --- a/lib/csapi/definitions/device_keys.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "device_keys.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const DeviceKeys& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("user_id"), pod.userId); - addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); - addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); - addParam<>(jo, QStringLiteral("keys"), pod.keys); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; -} - -DeviceKeys FromJsonObject<DeviceKeys>::operator()(const QJsonObject& jo) const -{ - DeviceKeys result; - result.userId = - fromJson<QString>(jo.value("user_id"_ls)); - result.deviceId = - fromJson<QString>(jo.value("device_id"_ls)); - result.algorithms = - fromJson<QStringList>(jo.value("algorithms"_ls)); - result.keys = - fromJson<QHash<QString, QString>>(jo.value("keys"_ls)); - result.signatures = - fromJson<QHash<QString, QHash<QString, QString>>>(jo.value("signatures"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h index 6023e7e8..84ecefae 100644 --- a/lib/csapi/definitions/device_keys.h +++ b/lib/csapi/definitions/device_keys.h @@ -6,40 +6,51 @@ #include "converters.h" -#include <QtCore/QHash> - -namespace QMatrixClient -{ - // Data structures - - /// Device identity keys - struct DeviceKeys +namespace Quotient { +/// Device identity keys +struct DeviceKeys { + /// The ID of the user the device belongs to. Must match the user ID used + /// when logging in. + QString userId; + + /// The ID of the device these keys belong to. Must match the device ID used + /// when logging in. + QString deviceId; + + /// The encryption algorithms supported by this device. + QStringList algorithms; + + /// Public identity keys. The names of the properties should be in the + /// 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. + /// + /// The signature is calculated using the process described at [Signing + /// JSON](/appendices/#signing-json). + QHash<QString, QHash<QString, QString>> signatures; +}; + +template <> +struct JsonObjectConverter<DeviceKeys> { + static void dumpTo(QJsonObject& jo, const DeviceKeys& pod) { - /// The ID of the user the device belongs to. Must match the user ID used - /// when logging in. - QString userId; - /// The ID of the device these keys belong to. Must match the device ID used - /// when logging in. - QString deviceId; - /// The encryption algorithms supported by this device. - QStringList algorithms; - /// Public identity keys. The names of the properties should be in the - /// 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. - /// - /// The signature is calculated using the process described at `Signing - /// JSON`_. - QHash<QString, QHash<QString, QString>> signatures; - }; - - QJsonObject toJson(const DeviceKeys& pod); - - template <> struct FromJsonObject<DeviceKeys> + addParam<>(jo, QStringLiteral("user_id"), pod.userId); + addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); + addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); + addParam<>(jo, QStringLiteral("keys"), pod.keys); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + static void fillFrom(const QJsonObject& jo, DeviceKeys& pod) { - DeviceKeys operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient + fromJson(jo.value("user_id"_ls), pod.userId); + fromJson(jo.value("device_id"_ls), pod.deviceId); + fromJson(jo.value("algorithms"_ls), pod.algorithms); + fromJson(jo.value("keys"_ls), pod.keys); + fromJson(jo.value("signatures"_ls), pod.signatures); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/event_filter.cpp b/lib/csapi/definitions/event_filter.cpp deleted file mode 100644 index cc444db0..00000000 --- a/lib/csapi/definitions/event_filter.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "event_filter.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const EventFilter& pod) -{ - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("limit"), pod.limit); - addParam<IfNotEmpty>(jo, QStringLiteral("not_senders"), pod.notSenders); - addParam<IfNotEmpty>(jo, QStringLiteral("not_types"), pod.notTypes); - addParam<IfNotEmpty>(jo, QStringLiteral("senders"), pod.senders); - addParam<IfNotEmpty>(jo, QStringLiteral("types"), pod.types); - return jo; -} - -EventFilter FromJsonObject<EventFilter>::operator()(const QJsonObject& jo) const -{ - EventFilter result; - result.limit = - fromJson<int>(jo.value("limit"_ls)); - result.notSenders = - fromJson<QStringList>(jo.value("not_senders"_ls)); - result.notTypes = - fromJson<QStringList>(jo.value("not_types"_ls)); - result.senders = - fromJson<QStringList>(jo.value("senders"_ls)); - result.types = - fromJson<QStringList>(jo.value("types"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h index 5c6a5b27..c55d4f92 100644 --- a/lib/csapi/definitions/event_filter.h +++ b/lib/csapi/definitions/event_filter.h @@ -6,31 +6,51 @@ #include "converters.h" -#include "converters.h" - -namespace QMatrixClient -{ - // Data structures - - struct EventFilter +namespace Quotient { + +struct EventFilter { + /// The maximum number of events to return. + Omittable<int> limit; + + /// 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. + 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. + QStringList notTypes; + + /// A list of senders IDs to include. If this list is absent then all + /// senders are included. + 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 + /// sequence of characters. + QStringList types; +}; + +template <> +struct JsonObjectConverter<EventFilter> { + static void dumpTo(QJsonObject& jo, const EventFilter& pod) { - /// The maximum number of events to return. - Omittable<int> limit; - /// 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. - 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. - QStringList notTypes; - /// A list of senders IDs to include. If this list is absent then all senders are included. - 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 sequence of characters. - QStringList types; - }; - - QJsonObject toJson(const EventFilter& pod); - - template <> struct FromJsonObject<EventFilter> + addParam<IfNotEmpty>(jo, QStringLiteral("limit"), pod.limit); + addParam<IfNotEmpty>(jo, QStringLiteral("not_senders"), pod.notSenders); + addParam<IfNotEmpty>(jo, QStringLiteral("not_types"), pod.notTypes); + addParam<IfNotEmpty>(jo, QStringLiteral("senders"), pod.senders); + addParam<IfNotEmpty>(jo, QStringLiteral("types"), pod.types); + } + static void fillFrom(const QJsonObject& jo, EventFilter& pod) { - EventFilter operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient + fromJson(jo.value("limit"_ls), pod.limit); + fromJson(jo.value("not_senders"_ls), pod.notSenders); + fromJson(jo.value("not_types"_ls), pod.notTypes); + fromJson(jo.value("senders"_ls), pod.senders); + fromJson(jo.value("types"_ls), pod.types); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/openid_token.h b/lib/csapi/definitions/openid_token.h new file mode 100644 index 00000000..9b026dea --- /dev/null +++ b/lib/csapi/definitions/openid_token.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { + +struct OpenIdCredentials { + /// 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. + QString accessToken; + + /// The string `Bearer`. + QString tokenType; + + /// The homeserver domain the consumer should use when attempting to + /// verify the user's identity. + QString matrixServerName; + + /// The number of seconds before this token expires and a new one must + /// be generated. + int expiresIn; +}; + +template <> +struct JsonObjectConverter<OpenIdCredentials> { + static void dumpTo(QJsonObject& jo, const OpenIdCredentials& pod) + { + addParam<>(jo, QStringLiteral("access_token"), pod.accessToken); + addParam<>(jo, QStringLiteral("token_type"), pod.tokenType); + addParam<>(jo, QStringLiteral("matrix_server_name"), + pod.matrixServerName); + addParam<>(jo, QStringLiteral("expires_in"), pod.expiresIn); + } + static void fillFrom(const QJsonObject& jo, OpenIdCredentials& pod) + { + fromJson(jo.value("access_token"_ls), pod.accessToken); + fromJson(jo.value("token_type"_ls), pod.tokenType); + fromJson(jo.value("matrix_server_name"_ls), pod.matrixServerName); + fromJson(jo.value("expires_in"_ls), pod.expiresIn); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/public_rooms_response.cpp b/lib/csapi/definitions/public_rooms_response.cpp deleted file mode 100644 index 2f52501d..00000000 --- a/lib/csapi/definitions/public_rooms_response.cpp +++ /dev/null @@ -1,73 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "public_rooms_response.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod) -{ - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases); - addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias); - addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name); - addParam<>(jo, QStringLiteral("num_joined_members"), pod.numJoinedMembers); - addParam<>(jo, QStringLiteral("room_id"), pod.roomId); - addParam<IfNotEmpty>(jo, QStringLiteral("topic"), pod.topic); - addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); - addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); - addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl); - return jo; -} - -PublicRoomsChunk FromJsonObject<PublicRoomsChunk>::operator()(const QJsonObject& jo) const -{ - PublicRoomsChunk result; - result.aliases = - fromJson<QStringList>(jo.value("aliases"_ls)); - result.canonicalAlias = - fromJson<QString>(jo.value("canonical_alias"_ls)); - result.name = - fromJson<QString>(jo.value("name"_ls)); - result.numJoinedMembers = - fromJson<int>(jo.value("num_joined_members"_ls)); - result.roomId = - fromJson<QString>(jo.value("room_id"_ls)); - result.topic = - fromJson<QString>(jo.value("topic"_ls)); - result.worldReadable = - fromJson<bool>(jo.value("world_readable"_ls)); - result.guestCanJoin = - fromJson<bool>(jo.value("guest_can_join"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; -} - -QJsonObject QMatrixClient::toJson(const PublicRoomsResponse& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("chunk"), pod.chunk); - addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch); - addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch); - addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"), pod.totalRoomCountEstimate); - return jo; -} - -PublicRoomsResponse FromJsonObject<PublicRoomsResponse>::operator()(const QJsonObject& jo) const -{ - PublicRoomsResponse result; - result.chunk = - fromJson<QVector<PublicRoomsChunk>>(jo.value("chunk"_ls)); - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - result.prevBatch = - fromJson<QString>(jo.value("prev_batch"_ls)); - result.totalRoomCountEstimate = - fromJson<int>(jo.value("total_room_count_estimate"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 88c805ba..7c7d9cc6 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -6,67 +6,76 @@ #include "converters.h" -#include <QtCore/QVector> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Data structures +struct PublicRoomsChunk { + /// The canonical alias of the room, if any. + QString canonicalAlias; - struct PublicRoomsChunk - { - /// Aliases of the room. May be empty. - QStringList aliases; - /// The canonical alias of the room, if any. - QString canonicalAlias; - /// The name of the room, if any. - QString name; - /// The number of members joined to the room. - int numJoinedMembers; - /// The ID of the room. - QString roomId; - /// The topic of the room, if any. - QString topic; - /// Whether the room may be viewed by guest users without joining. - bool worldReadable; - /// Whether guest users may join the room and participate in it. - /// If they can, they will be subject to ordinary power level - /// rules like any other user. - bool guestCanJoin; - /// The URL for the room's avatar, if one is set. - QString avatarUrl; - }; + /// The name of the room, if any. + QString name; - QJsonObject toJson(const PublicRoomsChunk& pod); + /// The number of members joined to the room. + int numJoinedMembers; - template <> struct FromJsonObject<PublicRoomsChunk> - { - PublicRoomsChunk operator()(const QJsonObject& jo) const; - }; + /// The ID of the room. + QString roomId; - /// A list of the rooms on the server. - struct PublicRoomsResponse - { - /// A paginated chunk of public rooms. - QVector<PublicRoomsChunk> chunk; - /// A pagination token for the response. The absence of this token - /// means there are no more results to fetch and the client should - /// stop paginating. - QString nextBatch; - /// A pagination token that allows fetching previous results. The - /// absence of this token means there are no results before this - /// batch, i.e. this is the first batch. - QString prevBatch; - /// An estimate on the total number of public rooms, if the - /// server has an estimate. - Omittable<int> totalRoomCountEstimate; - }; + /// The topic of the room, if any. + QString topic; + + /// Whether the room may be viewed by guest users without joining. + bool worldReadable; - QJsonObject toJson(const PublicRoomsResponse& pod); + /// Whether guest users may join the room and participate in it. + /// If they can, they will be subject to ordinary power level + /// rules like any other user. + bool guestCanJoin; - template <> struct FromJsonObject<PublicRoomsResponse> + /// The URL for the room's avatar, if one is set. + QUrl avatarUrl; + + /// The `type` of room (from + /// [`m.room.create`](/client-server-api/#mroomcreate)), if any. + QString roomType; + + /// 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 <> +struct JsonObjectConverter<PublicRoomsChunk> { + static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), + pod.canonicalAlias); + addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name); + addParam<>(jo, QStringLiteral("num_joined_members"), + pod.numJoinedMembers); + addParam<>(jo, QStringLiteral("room_id"), pod.roomId); + addParam<IfNotEmpty>(jo, QStringLiteral("topic"), pod.topic); + 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("room_type"), pod.roomType); + addParam<IfNotEmpty>(jo, QStringLiteral("join_rule"), pod.joinRule); + } + static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) { - PublicRoomsResponse operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias); + fromJson(jo.value("name"_ls), pod.name); + fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers); + fromJson(jo.value("room_id"_ls), pod.roomId); + fromJson(jo.value("topic"_ls), pod.topic); + 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("room_type"_ls), pod.roomType); + fromJson(jo.value("join_rule"_ls), pod.joinRule); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/push_condition.cpp b/lib/csapi/definitions/push_condition.cpp deleted file mode 100644 index 045094bc..00000000 --- a/lib/csapi/definitions/push_condition.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "push_condition.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const PushCondition& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("kind"), pod.kind); - addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); - addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); - addParam<IfNotEmpty>(jo, QStringLiteral("is"), pod.is); - return jo; -} - -PushCondition FromJsonObject<PushCondition>::operator()(const QJsonObject& jo) const -{ - PushCondition result; - result.kind = - fromJson<QString>(jo.value("kind"_ls)); - result.key = - fromJson<QString>(jo.value("key"_ls)); - result.pattern = - fromJson<QString>(jo.value("pattern"_ls)); - result.is = - fromJson<QString>(jo.value("is"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index defcebb3..6a048ba8 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -6,34 +6,50 @@ #include "converters.h" - -namespace QMatrixClient -{ - // Data structures - - struct PushCondition +namespace Quotient { + +struct PushCondition { + /// 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 + /// event to match. + /// + /// 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`. + QString key; + + /// Required for `event_match` conditions. The glob-style pattern to + /// match against. + QString pattern; + + /// 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 ==. + QString is; +}; + +template <> +struct JsonObjectConverter<PushCondition> { + static void dumpTo(QJsonObject& jo, const PushCondition& pod) { - QString kind; - /// Required for ``event_match`` conditions. The dot-separated field of the - /// event to match. - QString key; - /// 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 - /// 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 ==. - QString is; - }; - - QJsonObject toJson(const PushCondition& pod); - - template <> struct FromJsonObject<PushCondition> + addParam<>(jo, QStringLiteral("kind"), pod.kind); + addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); + addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); + addParam<IfNotEmpty>(jo, QStringLiteral("is"), pod.is); + } + static void fillFrom(const QJsonObject& jo, PushCondition& pod) { - PushCondition operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient + fromJson(jo.value("kind"_ls), pod.kind); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("pattern"_ls), pod.pattern); + fromJson(jo.value("is"_ls), pod.is); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/push_rule.cpp b/lib/csapi/definitions/push_rule.cpp deleted file mode 100644 index baddd187..00000000 --- a/lib/csapi/definitions/push_rule.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "push_rule.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const PushRule& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("actions"), pod.actions); - addParam<>(jo, QStringLiteral("default"), pod.isDefault); - addParam<>(jo, QStringLiteral("enabled"), pod.enabled); - addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); - addParam<IfNotEmpty>(jo, QStringLiteral("conditions"), pod.conditions); - addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); - return jo; -} - -PushRule FromJsonObject<PushRule>::operator()(const QJsonObject& jo) const -{ - PushRule result; - result.actions = - fromJson<QVector<QVariant>>(jo.value("actions"_ls)); - result.isDefault = - fromJson<bool>(jo.value("default"_ls)); - result.enabled = - fromJson<bool>(jo.value("enabled"_ls)); - result.ruleId = - fromJson<QString>(jo.value("rule_id"_ls)); - result.conditions = - fromJson<QVector<PushCondition>>(jo.value("conditions"_ls)); - result.pattern = - fromJson<QString>(jo.value("pattern"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index 5f52876d..135537c1 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -7,39 +7,52 @@ #include "converters.h" #include "csapi/definitions/push_condition.h" -#include "converters.h" -#include <QtCore/QVector> -#include <QtCore/QVariant> -#include <QtCore/QJsonObject> -namespace QMatrixClient -{ - // Data structures +namespace Quotient { + +struct PushRule { + /// The actions to perform when this rule is matched. + QVector<QVariant> actions; + + /// Whether this is a default rule, or has been set explicitly. + bool isDefault; + + /// Whether the push rule is enabled or not. + bool enabled; - struct PushRule + /// The ID of this rule. + QString ruleId; + + /// 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. + QVector<PushCondition> conditions; + + /// The glob-style pattern to match against. Only applicable to `content` + /// rules. + QString pattern; +}; + +template <> +struct JsonObjectConverter<PushRule> { + static void dumpTo(QJsonObject& jo, const PushRule& pod) { - /// The actions to perform when this rule is matched. - QVector<QVariant> actions; - /// Whether this is a default rule, or has been set explicitly. - bool isDefault; - /// Whether the push rule is enabled or not. - bool enabled; - /// The ID of this rule. - QString ruleId; - /// 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. - QVector<PushCondition> conditions; - /// The glob-style pattern to match against. Only applicable to ``content`` - /// rules. - QString pattern; - }; - - QJsonObject toJson(const PushRule& pod); - - template <> struct FromJsonObject<PushRule> + addParam<>(jo, QStringLiteral("actions"), pod.actions); + addParam<>(jo, QStringLiteral("default"), pod.isDefault); + addParam<>(jo, QStringLiteral("enabled"), pod.enabled); + addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); + addParam<IfNotEmpty>(jo, QStringLiteral("conditions"), pod.conditions); + addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); + } + static void fillFrom(const QJsonObject& jo, PushRule& pod) { - PushRule operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient + fromJson(jo.value("actions"_ls), pod.actions); + fromJson(jo.value("default"_ls), pod.isDefault); + fromJson(jo.value("enabled"_ls), pod.enabled); + fromJson(jo.value("rule_id"_ls), pod.ruleId); + fromJson(jo.value("conditions"_ls), pod.conditions); + fromJson(jo.value("pattern"_ls), pod.pattern); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/push_ruleset.cpp b/lib/csapi/definitions/push_ruleset.cpp deleted file mode 100644 index 14b7a4b6..00000000 --- a/lib/csapi/definitions/push_ruleset.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "push_ruleset.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const PushRuleset& pod) -{ - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("content"), pod.content); - addParam<IfNotEmpty>(jo, QStringLiteral("override"), pod.override); - addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); - addParam<IfNotEmpty>(jo, QStringLiteral("sender"), pod.sender); - addParam<IfNotEmpty>(jo, QStringLiteral("underride"), pod.underride); - return jo; -} - -PushRuleset FromJsonObject<PushRuleset>::operator()(const QJsonObject& jo) const -{ - PushRuleset result; - result.content = - fromJson<QVector<PushRule>>(jo.value("content"_ls)); - result.override = - fromJson<QVector<PushRule>>(jo.value("override"_ls)); - result.room = - fromJson<QVector<PushRule>>(jo.value("room"_ls)); - result.sender = - fromJson<QVector<PushRule>>(jo.value("sender"_ls)); - result.underride = - fromJson<QVector<PushRule>>(jo.value("underride"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/push_ruleset.h b/lib/csapi/definitions/push_ruleset.h index a274b72a..ba780a33 100644 --- a/lib/csapi/definitions/push_ruleset.h +++ b/lib/csapi/definitions/push_ruleset.h @@ -6,28 +6,40 @@ #include "converters.h" -#include <QtCore/QVector> -#include "converters.h" #include "csapi/definitions/push_rule.h" -namespace QMatrixClient -{ - // Data structures +namespace Quotient { - struct PushRuleset - { - QVector<PushRule> content; - QVector<PushRule> override; - QVector<PushRule> room; - QVector<PushRule> sender; - QVector<PushRule> underride; - }; +struct PushRuleset { + QVector<PushRule> content; - QJsonObject toJson(const PushRuleset& pod); + QVector<PushRule> override; - template <> struct FromJsonObject<PushRuleset> - { - PushRuleset operator()(const QJsonObject& jo) const; - }; + QVector<PushRule> room; + + QVector<PushRule> sender; -} // namespace QMatrixClient + QVector<PushRule> underride; +}; + +template <> +struct JsonObjectConverter<PushRuleset> { + static void dumpTo(QJsonObject& jo, const PushRuleset& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("content"), pod.content); + addParam<IfNotEmpty>(jo, QStringLiteral("override"), pod.override); + addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); + addParam<IfNotEmpty>(jo, QStringLiteral("sender"), pod.sender); + addParam<IfNotEmpty>(jo, QStringLiteral("underride"), pod.underride); + } + static void fillFrom(const QJsonObject& jo, PushRuleset& pod) + { + fromJson(jo.value("content"_ls), pod.content); + fromJson(jo.value("override"_ls), pod.override); + fromJson(jo.value("room"_ls), pod.room); + fromJson(jo.value("sender"_ls), pod.sender); + fromJson(jo.value("underride"_ls), pod.underride); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/request_email_validation.h b/lib/csapi/definitions/request_email_validation.h new file mode 100644 index 00000000..b1781e27 --- /dev/null +++ b/lib/csapi/definitions/request_email_validation.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include "csapi/definitions/../../identity/definitions/request_email_validation.h" + +namespace Quotient { + +struct EmailValidationData : RequestEmailValidation { + /// The hostname of the identity server to communicate with. May optionally + /// include a port. This parameter is ignored when the homeserver handles + /// 3PID verification. + /// + /// This parameter is deprecated with a plan to be removed in a future + /// 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. + QString idAccessToken; +}; + +template <> +struct JsonObjectConverter<EmailValidationData> { + static void dumpTo(QJsonObject& jo, const EmailValidationData& pod) + { + fillJson<RequestEmailValidation>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<IfNotEmpty>(jo, QStringLiteral("id_access_token"), + pod.idAccessToken); + } + static void fillFrom(const QJsonObject& jo, EmailValidationData& pod) + { + fillFromJson<RequestEmailValidation>(jo, pod); + fromJson(jo.value("id_server"_ls), pod.idServer); + fromJson(jo.value("id_access_token"_ls), pod.idAccessToken); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/request_msisdn_validation.h b/lib/csapi/definitions/request_msisdn_validation.h new file mode 100644 index 00000000..4600b48c --- /dev/null +++ b/lib/csapi/definitions/request_msisdn_validation.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include "csapi/definitions/../../identity/definitions/request_msisdn_validation.h" + +namespace Quotient { + +struct MsisdnValidationData : RequestMsisdnValidation { + /// The hostname of the identity server to communicate with. May optionally + /// include a port. This parameter is ignored when the homeserver handles + /// 3PID verification. + /// + /// This parameter is deprecated with a plan to be removed in a future + /// 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. + QString idAccessToken; +}; + +template <> +struct JsonObjectConverter<MsisdnValidationData> { + static void dumpTo(QJsonObject& jo, const MsisdnValidationData& pod) + { + fillJson<RequestMsisdnValidation>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<IfNotEmpty>(jo, QStringLiteral("id_access_token"), + pod.idAccessToken); + } + static void fillFrom(const QJsonObject& jo, MsisdnValidationData& pod) + { + fillFromJson<RequestMsisdnValidation>(jo, pod); + fromJson(jo.value("id_server"_ls), pod.idServer); + fromJson(jo.value("id_access_token"_ls), pod.idAccessToken); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h new file mode 100644 index 00000000..d5fbbadb --- /dev/null +++ b/lib/csapi/definitions/request_token_response.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +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 + /// 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 + /// 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 + /// (ie: r0.5.0). + QUrl submitUrl; +}; + +template <> +struct JsonObjectConverter<RequestTokenResponse> { + static void dumpTo(QJsonObject& jo, const RequestTokenResponse& pod) + { + addParam<>(jo, QStringLiteral("sid"), pod.sid); + addParam<IfNotEmpty>(jo, QStringLiteral("submit_url"), pod.submitUrl); + } + static void fillFrom(const QJsonObject& jo, RequestTokenResponse& pod) + { + fromJson(jo.value("sid"_ls), pod.sid); + fromJson(jo.value("submit_url"_ls), pod.submitUrl); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp deleted file mode 100644 index f6f1e5cb..00000000 --- a/lib/csapi/definitions/room_event_filter.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "room_event_filter.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const RoomEventFilter& pod) -{ - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); - addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); - addParam<IfNotEmpty>(jo, QStringLiteral("contains_url"), pod.containsUrl); - return jo; -} - -RoomEventFilter FromJsonObject<RoomEventFilter>::operator()(const QJsonObject& jo) const -{ - RoomEventFilter result; - result.notRooms = - fromJson<QStringList>(jo.value("not_rooms"_ls)); - result.rooms = - fromJson<QStringList>(jo.value("rooms"_ls)); - result.containsUrl = - fromJson<bool>(jo.value("contains_url"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 697fe661..293e5492 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -7,27 +7,71 @@ #include "converters.h" #include "csapi/definitions/event_filter.h" -#include "converters.h" -namespace QMatrixClient -{ - // Data structures +namespace Quotient { - struct RoomEventFilter : EventFilter - { - /// 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. - QStringList notRooms; - /// A list of room IDs to include. If this list is absent then all rooms are included. - QStringList rooms; - /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. Defaults to ``false``. - bool containsUrl; - }; +struct RoomEventFilter : EventFilter { + /// If `true`, enables per-[thread](/client-server-api/#threading) + /// notification counts. Only applies to the `/sync` endpoint. Defaults to + /// `false`. + Omittable<bool> unreadThreadNotifications; + + /// 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](/client-server-api/#lazy-loading-room-members) for more + /// information. Defaults to `false`. + Omittable<bool> includeRedundantMembers; - QJsonObject toJson(const RoomEventFilter& pod); + /// 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. + QStringList notRooms; - template <> struct FromJsonObject<RoomEventFilter> + /// 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. + Omittable<bool> containsUrl; +}; + +template <> +struct JsonObjectConverter<RoomEventFilter> { + static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod) + { + fillJson<EventFilter>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("unread_thread_notifications"), + pod.unreadThreadNotifications); + addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"), + pod.lazyLoadMembers); + addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"), + pod.includeRedundantMembers); + addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); + addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); + addParam<IfNotEmpty>(jo, QStringLiteral("contains_url"), + pod.containsUrl); + } + static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod) { - RoomEventFilter operator()(const QJsonObject& jo) const; - }; + fillFromJson<EventFilter>(jo, pod); + fromJson(jo.value("unread_thread_notifications"_ls), + pod.unreadThreadNotifications); + fromJson(jo.value("lazy_load_members"_ls), pod.lazyLoadMembers); + fromJson(jo.value("include_redundant_members"_ls), + pod.includeRedundantMembers); + fromJson(jo.value("not_rooms"_ls), pod.notRooms); + fromJson(jo.value("rooms"_ls), pod.rooms); + fromJson(jo.value("contains_url"_ls), pod.containsUrl); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/sync_filter.cpp b/lib/csapi/definitions/sync_filter.cpp deleted file mode 100644 index bd87804c..00000000 --- a/lib/csapi/definitions/sync_filter.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "sync_filter.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const RoomFilter& pod) -{ - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); - addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); - addParam<IfNotEmpty>(jo, QStringLiteral("ephemeral"), pod.ephemeral); - addParam<IfNotEmpty>(jo, QStringLiteral("include_leave"), pod.includeLeave); - addParam<IfNotEmpty>(jo, QStringLiteral("state"), pod.state); - addParam<IfNotEmpty>(jo, QStringLiteral("timeline"), pod.timeline); - addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); - return jo; -} - -RoomFilter FromJsonObject<RoomFilter>::operator()(const QJsonObject& jo) const -{ - RoomFilter result; - result.notRooms = - fromJson<QStringList>(jo.value("not_rooms"_ls)); - result.rooms = - fromJson<QStringList>(jo.value("rooms"_ls)); - result.ephemeral = - fromJson<RoomEventFilter>(jo.value("ephemeral"_ls)); - result.includeLeave = - fromJson<bool>(jo.value("include_leave"_ls)); - result.state = - fromJson<RoomEventFilter>(jo.value("state"_ls)); - result.timeline = - fromJson<RoomEventFilter>(jo.value("timeline"_ls)); - result.accountData = - fromJson<RoomEventFilter>(jo.value("account_data"_ls)); - - return result; -} - -QJsonObject QMatrixClient::toJson(const Filter& pod) -{ - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("event_fields"), pod.eventFields); - addParam<IfNotEmpty>(jo, QStringLiteral("event_format"), pod.eventFormat); - addParam<IfNotEmpty>(jo, QStringLiteral("presence"), pod.presence); - addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); - addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); - return jo; -} - -Filter FromJsonObject<Filter>::operator()(const QJsonObject& jo) const -{ - Filter result; - result.eventFields = - fromJson<QStringList>(jo.value("event_fields"_ls)); - result.eventFormat = - fromJson<QString>(jo.value("event_format"_ls)); - result.presence = - fromJson<EventFilter>(jo.value("presence"_ls)); - result.accountData = - fromJson<EventFilter>(jo.value("account_data"_ls)); - result.room = - fromJson<RoomFilter>(jo.value("room"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index ca275a9a..62e17962 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -6,59 +6,110 @@ #include "converters.h" -#include "csapi/definitions/room_event_filter.h" -#include "converters.h" #include "csapi/definitions/event_filter.h" +#include "csapi/definitions/room_event_filter.h" -namespace QMatrixClient -{ - // Data structures +namespace Quotient { +/// Filters to be applied to room data. +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` + QStringList notRooms; - /// Filters to be applied to room data. - struct RoomFilter + /// 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` + QStringList rooms; + + /// The events that aren't recorded in the room history, e.g. typing and + /// receipts, to include for rooms. + RoomEventFilter ephemeral; + + /// Include rooms that the user has left in the sync, default false + Omittable<bool> includeLeave; + + /// The state events to include for rooms. + RoomEventFilter state; + + /// The message and state update events to include for rooms. + RoomEventFilter timeline; + + /// The per user account data to include for rooms. + RoomEventFilter accountData; +}; + +template <> +struct JsonObjectConverter<RoomFilter> { + static void dumpTo(QJsonObject& jo, const RoomFilter& pod) { - /// 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`` - 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`` - QStringList rooms; - /// The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. - Omittable<RoomEventFilter> ephemeral; - /// Include rooms that the user has left in the sync, default false - bool includeLeave; - /// The state events to include for rooms. - Omittable<RoomEventFilter> state; - /// The message and state update events to include for rooms. - Omittable<RoomEventFilter> timeline; - /// The per user account data to include for rooms. - Omittable<RoomEventFilter> accountData; - }; - - QJsonObject toJson(const RoomFilter& pod); - - template <> struct FromJsonObject<RoomFilter> + addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); + addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); + addParam<IfNotEmpty>(jo, QStringLiteral("ephemeral"), pod.ephemeral); + addParam<IfNotEmpty>(jo, QStringLiteral("include_leave"), + pod.includeLeave); + addParam<IfNotEmpty>(jo, QStringLiteral("state"), pod.state); + addParam<IfNotEmpty>(jo, QStringLiteral("timeline"), pod.timeline); + addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), + pod.accountData); + } + static void fillFrom(const QJsonObject& jo, RoomFilter& pod) { - RoomFilter operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("not_rooms"_ls), pod.notRooms); + fromJson(jo.value("rooms"_ls), pod.rooms); + fromJson(jo.value("ephemeral"_ls), pod.ephemeral); + fromJson(jo.value("include_leave"_ls), pod.includeLeave); + fromJson(jo.value("state"_ls), pod.state); + fromJson(jo.value("timeline"_ls), pod.timeline); + fromJson(jo.value("account_data"_ls), pod.accountData); + } +}; + +struct Filter { + /// List of event fields to include. If this list is absent then all fields + /// are included. The entries may include '.' characters to indicate + /// sub-fields. So ['content.body'] will include the 'body' field of the + /// 'content' object. A literal '.' character in a field name may be escaped + /// using a '\\'. A server may include more fields than were requested. + QStringList eventFields; + + /// The format to use for events. 'client' will return the events in a + /// format suitable for clients. 'federation' will return the raw event as + /// received over federation. The default is 'client'. + QString eventFormat; + + /// The presence updates to include. + EventFilter presence; + + /// The user account data that isn't associated with rooms to include. + EventFilter accountData; + + /// Filters to be applied to room data. + RoomFilter room; +}; - struct Filter +template <> +struct JsonObjectConverter<Filter> { + static void dumpTo(QJsonObject& jo, const Filter& pod) { - /// List of event fields to include. If this list is absent then all fields are included. The entries may include '.' charaters to indicate sub-fields. So ['content.body'] will include the 'body' field of the 'content' object. A literal '.' character in a field name may be escaped using a '\\'. A server may include more fields than were requested. - QStringList eventFields; - /// The format to use for events. 'client' will return the events in a format suitable for clients. 'federation' will return the raw event as receieved over federation. The default is 'client'. - QString eventFormat; - /// The presence updates to include. - Omittable<EventFilter> presence; - /// The user account data that isn't associated with rooms to include. - Omittable<EventFilter> accountData; - /// Filters to be applied to room data. - Omittable<RoomFilter> room; - }; - - QJsonObject toJson(const Filter& pod); - - template <> struct FromJsonObject<Filter> + addParam<IfNotEmpty>(jo, QStringLiteral("event_fields"), + pod.eventFields); + addParam<IfNotEmpty>(jo, QStringLiteral("event_format"), + pod.eventFormat); + addParam<IfNotEmpty>(jo, QStringLiteral("presence"), pod.presence); + addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), + pod.accountData); + addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); + } + static void fillFrom(const QJsonObject& jo, Filter& pod) { - Filter operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("event_fields"_ls), pod.eventFields); + fromJson(jo.value("event_format"_ls), pod.eventFormat); + fromJson(jo.value("presence"_ls), pod.presence); + fromJson(jo.value("account_data"_ls), pod.accountData); + fromJson(jo.value("room"_ls), pod.room); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/third_party_signed.h b/lib/csapi/definitions/third_party_signed.h new file mode 100644 index 00000000..7097bda4 --- /dev/null +++ b/lib/csapi/definitions/third_party_signed.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { +/// 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. + QString sender; + + /// The Matrix ID of the invitee. + QString mxid; + + /// The state key of the m.third_party_invite event. + QString token; + + /// A signatures object containing a signature of the entire signed object. + QHash<QString, QHash<QString, QString>> signatures; +}; + +template <> +struct JsonObjectConverter<ThirdPartySigned> { + static void dumpTo(QJsonObject& jo, const ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + static void fillFrom(const QJsonObject& jo, ThirdPartySigned& pod) + { + fromJson(jo.value("sender"_ls), pod.sender); + fromJson(jo.value("mxid"_ls), pod.mxid); + fromJson(jo.value("token"_ls), pod.token); + fromJson(jo.value("signatures"_ls), pod.signatures); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/user_identifier.cpp b/lib/csapi/definitions/user_identifier.cpp deleted file mode 100644 index 80a6d450..00000000 --- a/lib/csapi/definitions/user_identifier.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "user_identifier.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const UserIdentifier& pod) -{ - QJsonObject jo = toJson(pod.additionalProperties); - addParam<>(jo, QStringLiteral("type"), pod.type); - return jo; -} - -UserIdentifier FromJsonObject<UserIdentifier>::operator()(QJsonObject jo) const -{ - UserIdentifier result; - result.type = - fromJson<QString>(jo.take("type"_ls)); - - result.additionalProperties = fromJson<QVariantHash>(jo); - return result; -} - diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h index 42614436..cb585a6a 100644 --- a/lib/csapi/definitions/user_identifier.h +++ b/lib/csapi/definitions/user_identifier.h @@ -6,26 +6,30 @@ #include "converters.h" -#include <QtCore/QVariant> - -namespace QMatrixClient -{ - // Data structures +namespace Quotient { +/// Identification information for a user +struct UserIdentifier { + /// 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 - struct UserIdentifier - { - /// The type of identification. See `Identifier types`_ for supported values and additional property descriptions. - QString type; - /// Identification information for a user - QVariantHash additionalProperties; - }; + QVariantHash additionalProperties; +}; - QJsonObject toJson(const UserIdentifier& pod); - - template <> struct FromJsonObject<UserIdentifier> +template <> +struct JsonObjectConverter<UserIdentifier> { + static void dumpTo(QJsonObject& jo, const UserIdentifier& pod) + { + fillJson(jo, pod.additionalProperties); + addParam<>(jo, QStringLiteral("type"), pod.type); + } + static void fillFrom(QJsonObject jo, UserIdentifier& pod) { - UserIdentifier operator()(QJsonObject jo) const; - }; + fromJson(jo.take("type"_ls), pod.type); + fromJson(jo, pod.additionalProperties); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/wellknown/full.h b/lib/csapi/definitions/wellknown/full.h new file mode 100644 index 00000000..a0ef2076 --- /dev/null +++ b/lib/csapi/definitions/wellknown/full.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include "csapi/definitions/wellknown/homeserver.h" +#include "csapi/definitions/wellknown/identity_server.h" + +namespace Quotient { +/// Used by clients to determine the homeserver, identity server, and other +/// optional components they should be interacting with. +struct DiscoveryInformation { + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + HomeserverInformation homeserver; + + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + Omittable<IdentityServerInformation> identityServer; + + /// Application-dependent keys using Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; +}; + +template <> +struct JsonObjectConverter<DiscoveryInformation> { + static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod) + { + fillJson(jo, pod.additionalProperties); + addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver); + addParam<IfNotEmpty>(jo, QStringLiteral("m.identity_server"), + pod.identityServer); + } + static void fillFrom(QJsonObject jo, DiscoveryInformation& pod) + { + fromJson(jo.take("m.homeserver"_ls), pod.homeserver); + fromJson(jo.take("m.identity_server"_ls), pod.identityServer); + fromJson(jo, pod.additionalProperties); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/wellknown/homeserver.cpp b/lib/csapi/definitions/wellknown/homeserver.cpp deleted file mode 100644 index f1482ee4..00000000 --- a/lib/csapi/definitions/wellknown/homeserver.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "homeserver.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const HomeserverInformation& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; -} - -HomeserverInformation FromJsonObject<HomeserverInformation>::operator()(const QJsonObject& jo) const -{ - HomeserverInformation result; - result.baseUrl = - fromJson<QString>(jo.value("base_url"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h index 09d6ba63..b7db4182 100644 --- a/lib/csapi/definitions/wellknown/homeserver.h +++ b/lib/csapi/definitions/wellknown/homeserver.h @@ -6,23 +6,23 @@ #include "converters.h" - -namespace QMatrixClient -{ - // Data structures - - /// Used by clients to discover homeserver information. - struct HomeserverInformation +namespace Quotient { +/// Used by clients to discover homeserver information. +struct HomeserverInformation { + /// The base URL for the homeserver for client-server connections. + QUrl baseUrl; +}; + +template <> +struct JsonObjectConverter<HomeserverInformation> { + static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod) { - /// The base URL for the homeserver for client-server connections. - QString baseUrl; - }; - - QJsonObject toJson(const HomeserverInformation& pod); - - template <> struct FromJsonObject<HomeserverInformation> + addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); + } + static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod) { - HomeserverInformation operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("base_url"_ls), pod.baseUrl); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/wellknown/identity_server.cpp b/lib/csapi/definitions/wellknown/identity_server.cpp deleted file mode 100644 index f9d7bc37..00000000 --- a/lib/csapi/definitions/wellknown/identity_server.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "identity_server.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const IdentityServerInformation& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; -} - -IdentityServerInformation FromJsonObject<IdentityServerInformation>::operator()(const QJsonObject& jo) const -{ - IdentityServerInformation result; - result.baseUrl = - fromJson<QString>(jo.value("base_url"_ls)); - - return result; -} - diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h index cb8ffcee..885e3d34 100644 --- a/lib/csapi/definitions/wellknown/identity_server.h +++ b/lib/csapi/definitions/wellknown/identity_server.h @@ -6,23 +6,23 @@ #include "converters.h" - -namespace QMatrixClient -{ - // Data structures - - /// Used by clients to discover identity server information. - struct IdentityServerInformation +namespace Quotient { +/// Used by clients to discover identity server information. +struct IdentityServerInformation { + /// The base URL for the identity server for client-server connections. + QUrl baseUrl; +}; + +template <> +struct JsonObjectConverter<IdentityServerInformation> { + static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod) { - /// The base URL for the identity server for client-server connections. - QString baseUrl; - }; - - QJsonObject toJson(const IdentityServerInformation& pod); - - template <> struct FromJsonObject<IdentityServerInformation> + addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); + } + static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod) { - IdentityServerInformation operator()(const QJsonObject& jo) const; - }; + fromJson(jo.value("base_url"_ls), pod.baseUrl); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index 861e1994..6f2badee 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -4,114 +4,58 @@ #include "device_management.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetDevicesJob::Private -{ - public: - QVector<Device> devices; -}; +using namespace Quotient; QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/devices"); + makePath("/_matrix/client/v3", "/devices")); } -static const auto GetDevicesJobName = QStringLiteral("GetDevicesJob"); - GetDevicesJob::GetDevicesJob() - : BaseJob(HttpVerb::Get, GetDevicesJobName, - basePath % "/devices") - , d(new Private) -{ -} - -GetDevicesJob::~GetDevicesJob() = default; - -const QVector<Device>& GetDevicesJob::devices() const -{ - return d->devices; -} - -BaseJob::Status GetDevicesJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->devices = fromJson<QVector<Device>>(json.value("devices"_ls)); - return Success; -} - -class GetDeviceJob::Private -{ - public: - Device data; -}; + : BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"), + makePath("/_matrix/client/v3", "/devices")) +{} QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/devices/" % deviceId); + makePath("/_matrix/client/v3", "/devices/", + deviceId)); } -static const auto GetDeviceJobName = QStringLiteral("GetDeviceJob"); - GetDeviceJob::GetDeviceJob(const QString& deviceId) - : BaseJob(HttpVerb::Get, GetDeviceJobName, - basePath % "/devices/" % deviceId) - , d(new Private) -{ -} + : BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"), + makePath("/_matrix/client/v3", "/devices/", deviceId)) +{} -GetDeviceJob::~GetDeviceJob() = default; - -const Device& GetDeviceJob::data() const +UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, + const QString& displayName) + : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), + makePath("/_matrix/client/v3", "/devices/", deviceId)) { - return d->data; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("display_name"), displayName); + setRequestData({ _dataJson }); } -BaseJob::Status GetDeviceJob::parseJson(const QJsonDocument& data) +DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), + makePath("/_matrix/client/v3", "/devices/", deviceId)) { - d->data = fromJson<Device>(data); - return Success; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } -static const auto UpdateDeviceJobName = QStringLiteral("UpdateDeviceJob"); - -UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) - : BaseJob(HttpVerb::Put, UpdateDeviceJobName, - basePath % "/devices/" % deviceId) +DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"), + makePath("/_matrix/client/v3", "/delete_devices")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("display_name"), displayName); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("devices"), devices); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } - -static const auto DeleteDeviceJobName = QStringLiteral("DeleteDeviceJob"); - -DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Delete, DeleteDeviceJobName, - basePath % "/devices/" % deviceId) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); -} - -static const auto DeleteDevicesJobName = QStringLiteral("DeleteDevicesJob"); - -DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Post, DeleteDevicesJobName, - basePath % "/delete_devices") -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("devices"), devices); - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); -} - diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index f41efdbc..c10389b3 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -4,132 +4,127 @@ #pragma once -#include "jobs/basejob.h" - #include "csapi/definitions/auth_data.h" -#include <QtCore/QVector> -#include "converters.h" #include "csapi/definitions/client_device.h" -namespace QMatrixClient -{ - // Operations - - /// List registered devices for the current user - /// - /// Gets information about all devices for the current user. - class GetDevicesJob : public BaseJob - { - public: - explicit GetDevicesJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetDevicesJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetDevicesJob() override; +#include "jobs/basejob.h" - // Result properties +namespace Quotient { - /// A list of all registered devices for this user. - const QVector<Device>& devices() const; +/*! \brief List registered devices for the current user + * + * Gets information about all devices for the current user. + */ +class QUOTIENT_API GetDevicesJob : public BaseJob { +public: + /// List registered devices for the current user + explicit GetDevicesJob(); - protected: - Status parseJson(const QJsonDocument& data) override; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetDevicesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - private: - class Private; - QScopedPointer<Private> d; - }; + // Result properties - /// Get a single device - /// - /// Gets information on a single device, by device id. - class GetDeviceJob : public BaseJob - { - public: - /*! Get a single device - * \param deviceId - * The device to retrieve. - */ - explicit GetDeviceJob(const QString& deviceId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetDeviceJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& deviceId); - - ~GetDeviceJob() override; - - // Result properties - - /// Device information - const Device& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Update a device - /// - /// Updates the metadata on the given device. - class UpdateDeviceJob : public BaseJob - { - public: - /*! Update a device - * \param deviceId - * The device to update. - * \param displayName - * The new display name for this device. If not given, the - * display name is unchanged. - */ - explicit UpdateDeviceJob(const QString& deviceId, const QString& displayName = {}); - }; - - /// Delete a device - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// Deletes the given device, and invalidates any access token associated with it. - class DeleteDeviceJob : public BaseJob + /// A list of all registered devices for this user. + QVector<Device> devices() const { - public: - /*! Delete a device - * \param deviceId - * The device to delete. - * \param auth - * Additional authentication information for the - * user-interactive authentication API. - */ - explicit DeleteDeviceJob(const QString& deviceId, const Omittable<AuthenticationData>& auth = none); - }; - - /// Bulk deletion of devices - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// Deletes the given devices, and invalidates any access token associated with them. - class DeleteDevicesJob : public BaseJob - { - public: - /*! Bulk deletion of devices - * \param devices - * The list of device IDs to delete. - * \param auth - * Additional authentication information for the - * user-interactive authentication API. - */ - explicit DeleteDevicesJob(const QStringList& devices, const Omittable<AuthenticationData>& auth = none); - }; -} // namespace QMatrixClient + return loadFromJson<QVector<Device>>("devices"_ls); + } +}; + +/*! \brief Get a single device + * + * Gets information on a single device, by device id. + */ +class QUOTIENT_API GetDeviceJob : public BaseJob { +public: + /*! \brief Get a single device + * + * \param deviceId + * The device to retrieve. + */ + explicit GetDeviceJob(const QString& deviceId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetDeviceJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& deviceId); + + // Result properties + + /// Device information + Device device() const { return fromJson<Device>(jsonData()); } +}; + +/*! \brief Update a device + * + * Updates the metadata on the given device. + */ +class QUOTIENT_API UpdateDeviceJob : public BaseJob { +public: + /*! \brief Update a device + * + * \param deviceId + * The device to update. + * + * \param displayName + * The new display name for this device. If not given, the + * display name is unchanged. + */ + explicit UpdateDeviceJob(const QString& deviceId, + const QString& displayName = {}); +}; + +/*! \brief Delete a device + * + * 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. + */ +class QUOTIENT_API DeleteDeviceJob : public BaseJob { +public: + /*! \brief Delete a device + * + * \param deviceId + * The device to delete. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit DeleteDeviceJob(const QString& deviceId, + const Omittable<AuthenticationData>& auth = none); +}; + +/*! \brief Bulk deletion of devices + * + * 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. + */ +class QUOTIENT_API DeleteDevicesJob : public BaseJob { +public: + /*! \brief Bulk deletion of devices + * + * \param devices + * The list of device IDs to delete. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit DeleteDevicesJob(const QStringList& devices, + const Omittable<AuthenticationData>& auth = none); +}; + +} // namespace Quotient diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 5353f3bc..c1255bb1 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -4,78 +4,52 @@ #include "directory.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0/directory"); - -static const auto SetRoomAliasJobName = QStringLiteral("SetRoomAliasJob"); +using namespace Quotient; SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) - : BaseJob(HttpVerb::Put, SetRoomAliasJobName, - basePath % "/room/" % roomAlias) + : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"), + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("room_id"), roomId); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("room_id"), roomId); + setRequestData({ _dataJson }); } -class GetRoomIdByAliasJob::Private -{ - public: - QString roomId; - QStringList servers; -}; - QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/room/" % roomAlias); + makePath("/_matrix/client/v3", + "/directory/room/", roomAlias)); } -static const auto GetRoomIdByAliasJobName = QStringLiteral("GetRoomIdByAliasJob"); - GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) - : BaseJob(HttpVerb::Get, GetRoomIdByAliasJobName, - basePath % "/room/" % roomAlias, false) - , d(new Private) -{ -} - -GetRoomIdByAliasJob::~GetRoomIdByAliasJob() = default; - -const QString& GetRoomIdByAliasJob::roomId() const -{ - return d->roomId; -} + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"), + makePath("/_matrix/client/v3", "/directory/room/", roomAlias), + false) +{} -const QStringList& GetRoomIdByAliasJob::servers() const +QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { - return d->servers; + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v3", + "/directory/room/", roomAlias)); } -BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->roomId = fromJson<QString>(json.value("room_id"_ls)); - d->servers = fromJson<QStringList>(json.value("servers"_ls)); - return Success; -} +DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"), + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)) +{} -QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) +QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/room/" % roomAlias); + makePath("/_matrix/client/v3", "/rooms/", + roomId, "/aliases")); } -static const auto DeleteRoomAliasJobName = QStringLiteral("DeleteRoomAliasJob"); - -DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) - : BaseJob(HttpVerb::Delete, DeleteRoomAliasJobName, - basePath % "/room/" % roomAlias) +GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases")) { + addExpectedKey("aliases"); } - diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 39e86635..0bd13a76 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -6,86 +6,135 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Create a new mapping from room alias to room ID. + * + */ +class QUOTIENT_API SetRoomAliasJob : public BaseJob { +public: + /*! \brief Create a new mapping from room alias to room ID. + * + * \param roomAlias + * The room alias to set. + * + * \param roomId + * The room ID to set. + */ + explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId); +}; - /// Create a new mapping from room alias to room ID. - class SetRoomAliasJob : public BaseJob - { - public: - /*! Create a new mapping from room alias to room ID. - * \param roomAlias - * The room alias to set. - * \param roomId - * The room ID to set. - */ - explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId); - }; - - /// Get the room ID corresponding to this room alias. - /// - /// Requests that the server resolve a room alias to a room ID. - /// - /// The server will use the federation API to resolve the alias if the - /// domain part of the alias does not correspond to the server's own - /// domain. - class GetRoomIdByAliasJob : public BaseJob +/*! \brief Get the room ID corresponding to this room alias. + * + * Requests that the server resolve a room alias to a room ID. + * + * The server will use the federation API to resolve the alias if the + * domain part of the alias does not correspond to the server's own + * domain. + */ +class QUOTIENT_API GetRoomIdByAliasJob : public BaseJob { +public: + /*! \brief Get the room ID corresponding to this room alias. + * + * \param roomAlias + * The room alias. + */ + explicit GetRoomIdByAliasJob(const QString& roomAlias); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomIdByAliasJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); + + // Result properties + + /// The room ID for this room alias. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } + + /// A list of servers that are aware of this room alias. + QStringList servers() const { - public: - /*! Get the room ID corresponding to this room alias. - * \param roomAlias - * The room alias. - */ - explicit GetRoomIdByAliasJob(const QString& roomAlias); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomIdByAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); - - ~GetRoomIdByAliasJob() override; - - // Result properties - - /// The room ID for this room alias. - const QString& roomId() const; - /// A list of servers that are aware of this room alias. - const QStringList& servers() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Remove a mapping of room alias to room ID. - /// - /// Remove a mapping of room alias to room ID. - /// - /// Servers may choose to implement additional access control checks here, for instance that room aliases can only be deleted by their creator or a server administrator. - class DeleteRoomAliasJob : public BaseJob + return loadFromJson<QStringList>("servers"_ls); + } +}; + +/*! \brief Remove a mapping of room alias to room ID. + * + * Remove a mapping of room alias to room ID. + * + * Servers may choose to implement additional access control checks here, for + * 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. + * 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. + */ +class QUOTIENT_API DeleteRoomAliasJob : public BaseJob { +public: + /*! \brief Remove a mapping of room alias to room ID. + * + * \param roomAlias + * The room alias to remove. + */ + explicit DeleteRoomAliasJob(const QString& roomAlias); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for DeleteRoomAliasJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); +}; + +/*! \brief Get a list of local aliases on a given room. + * + * Get a list of aliases maintained by the local server for the + * 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 + * 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. + */ +class QUOTIENT_API GetLocalAliasesJob : public BaseJob { +public: + /*! \brief Get a list of local aliases on a given room. + * + * \param roomId + * The room ID to find local aliases of. + */ + explicit GetLocalAliasesJob(const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetLocalAliasesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + // Result properties + + /// The server's local aliases on the room. Can be empty. + QStringList aliases() const { - public: - /*! Remove a mapping of room alias to room ID. - * \param roomAlias - * The room alias to remove. - */ - explicit DeleteRoomAliasJob(const QString& roomAlias); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * DeleteRoomAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); - - }; -} // namespace QMatrixClient + return loadFromJson<QStringList>("aliases"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index 806c1613..4ebbbf98 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -4,90 +4,33 @@ #include "event_context.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetEventContextJob::Private -{ - public: - QString begin; - QString end; - RoomEvents eventsBefore; - RoomEventPtr event; - RoomEvents eventsAfter; - StateEvents state; -}; - -BaseJob::Query queryToGetEventContext(Omittable<int> limit) +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; } -QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, Omittable<int> limit) +QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + Omittable<int> limit, + const QString& filter) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/context/" % eventId, - queryToGetEventContext(limit)); -} - -static const auto GetEventContextJobName = QStringLiteral("GetEventContextJob"); - -GetEventContextJob::GetEventContextJob(const QString& roomId, const QString& eventId, Omittable<int> limit) - : BaseJob(HttpVerb::Get, GetEventContextJobName, - basePath % "/rooms/" % roomId % "/context/" % eventId, - queryToGetEventContext(limit)) - , d(new Private) -{ -} - -GetEventContextJob::~GetEventContextJob() = default; - -const QString& GetEventContextJob::begin() const -{ - return d->begin; -} - -const QString& GetEventContextJob::end() const -{ - return d->end; -} - -RoomEvents&& GetEventContextJob::eventsBefore() -{ - return std::move(d->eventsBefore); -} - -RoomEventPtr&& GetEventContextJob::event() -{ - return std::move(d->event); -} - -RoomEvents&& GetEventContextJob::eventsAfter() -{ - return std::move(d->eventsAfter); -} - -StateEvents&& GetEventContextJob::state() -{ - return std::move(d->state); -} - -BaseJob::Status GetEventContextJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->eventsBefore = fromJson<RoomEvents>(json.value("events_before"_ls)); - d->event = fromJson<RoomEventPtr>(json.value("event"_ls)); - d->eventsAfter = fromJson<RoomEvents>(json.value("events_after"_ls)); - d->state = fromJson<StateEvents>(json.value("state"_ls)); - return Success; -} - + makePath("/_matrix/client/v3", "/rooms/", + roomId, "/context/", eventId), + queryToGetEventContext(limit, filter)); +} + +GetEventContextJob::GetEventContextJob(const QString& roomId, + const QString& eventId, + Omittable<int> limit, + const QString& filter) + : BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/context/", + eventId), + queryToGetEventContext(limit, filter)) +{} diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index a5fda7ea..1614c7ed 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -4,65 +4,85 @@ #pragma once +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Get events and state around the specified event. + * + * This API returns a number of events that happened just before and + * after the specified event. This allows clients to get the context + * surrounding an event. + * + * *Note*: This endpoint supports lazy-loading of room member events. See + * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) + * for more information. + */ +class QUOTIENT_API GetEventContextJob : public BaseJob { +public: + /*! \brief Get events and state around the specified event. + * + * \param roomId + * The room to get events from. + * + * \param eventId + * The event to get context around. + * + * \param limit + * 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 + * homeserver prefers. + * + * See [Filtering](/client-server-api/#filtering) for more information. + */ + explicit GetEventContextJob(const QString& roomId, const QString& eventId, + Omittable<int> limit = none, + const QString& filter = {}); - /// Get events and state around the specified event. - /// - /// This API returns a number of events that happened just before and - /// after the specified event. This allows clients to get the context - /// surrounding an event. - class GetEventContextJob : public BaseJob - { - public: - /*! Get events and state around the specified event. - * \param roomId - * The room to get events from. - * \param eventId - * The event to get context around. - * \param limit - * The maximum number of events to return. Default: 10. - */ - explicit GetEventContextJob(const QString& roomId, const QString& eventId, Omittable<int> limit = none); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetEventContextJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + Omittable<int> limit = none, + const QString& filter = {}); + + // Result properties - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetEventContextJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, Omittable<int> limit = none); + /// A token that can be used to paginate backwards with. + QString begin() const { return loadFromJson<QString>("start"_ls); } - ~GetEventContextJob() override; + /// A token that can be used to paginate forwards with. + QString end() const { return loadFromJson<QString>("end"_ls); } - // Result properties + /// A list of room events that happened just before the + /// requested event, in reverse-chronological order. + RoomEvents eventsBefore() + { + return takeFromJson<RoomEvents>("events_before"_ls); + } + + /// Details of the requested event. + RoomEventPtr event() { return takeFromJson<RoomEventPtr>("event"_ls); } - /// A token that can be used to paginate backwards with. - const QString& begin() const; - /// A token that can be used to paginate forwards with. - const QString& end() const; - /// A list of room events that happened just before the - /// requested event, in reverse-chronological order. - RoomEvents&& eventsBefore(); - /// Details of the requested event. - RoomEventPtr&& event(); - /// A list of room events that happened just after the - /// requested event, in chronological order. - RoomEvents&& eventsAfter(); - /// The state of the room at the last event returned. - StateEvents&& state(); + /// A list of room events that happened just after the + /// requested event, in chronological order. + RoomEvents eventsAfter() + { + return takeFromJson<RoomEvents>("events_after"_ls); + } - protected: - Status parseJson(const QJsonDocument& data) override; + /// The state of the room at the last event returned. + StateEvents state() { return takeFromJson<StateEvents>("state"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 77dc9b92..2469fbd1 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -4,78 +4,26 @@ #include "filter.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class DefineFilterJob::Private -{ - public: - QString filterId; -}; - -static const auto DefineFilterJobName = QStringLiteral("DefineFilterJob"); +using namespace Quotient; DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) - : BaseJob(HttpVerb::Post, DefineFilterJobName, - basePath % "/user/" % userId % "/filter") - , d(new Private) -{ - setRequestData(Data(toJson(filter))); -} - -DefineFilterJob::~DefineFilterJob() = default; - -const QString& DefineFilterJob::filterId() const -{ - return d->filterId; -} - -BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data) + : BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"), + makePath("/_matrix/client/v3", "/user/", userId, "/filter")) { - auto json = data.object(); - if (!json.contains("filter_id"_ls)) - return { JsonParseError, - "The key 'filter_id' not found in the response" }; - d->filterId = fromJson<QString>(json.value("filter_id"_ls)); - return Success; + setRequestData({ toJson(filter) }); + addExpectedKey("filter_id"); } -class GetFilterJob::Private -{ - public: - Filter data; -}; - -QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId) +QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& filterId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/filter/" % filterId); + makePath("/_matrix/client/v3", "/user/", + userId, "/filter/", filterId)); } -static const auto GetFilterJobName = QStringLiteral("GetFilterJob"); - GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId) - : BaseJob(HttpVerb::Get, GetFilterJobName, - basePath % "/user/" % userId % "/filter/" % filterId) - , d(new Private) -{ -} - -GetFilterJob::~GetFilterJob() = default; - -const Filter& GetFilterJob::data() const -{ - return d->data; -} - -BaseJob::Status GetFilterJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Filter>(data); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"), + makePath("/_matrix/client/v3", "/user/", userId, "/filter/", + filterId)) +{} diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index 0ca7e953..9518a461 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -4,82 +4,67 @@ #pragma once -#include "jobs/basejob.h" - -#include "converters.h" #include "csapi/definitions/sync_filter.h" -namespace QMatrixClient -{ - // Operations - - /// Upload a new filter. - /// - /// Uploads a new filter definition to the homeserver. - /// Returns a filter ID that may be used in future requests to - /// restrict which events are returned to the client. - class DefineFilterJob : public BaseJob - { - public: - /*! Upload a new filter. - * \param userId - * The id of the user uploading the filter. The access token must be authorized to make requests for this user id. - * \param filter - * Uploads a new filter definition to the homeserver. - * Returns a filter ID that may be used in future requests to - * restrict which events are returned to the client. - */ - explicit DefineFilterJob(const QString& userId, const Filter& filter); - ~DefineFilterJob() override; - - // Result properties - - /// The ID of the filter that was created. Cannot start - /// 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. - const QString& filterId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download a filter - class GetFilterJob : public BaseJob - { - public: - /*! Download a filter - * \param userId - * The user ID to download a filter for. - * \param filterId - * The filter ID to download. - */ - explicit GetFilterJob(const QString& userId, const QString& filterId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetFilterJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId); - - ~GetFilterJob() override; - - // Result properties - - /// "The filter defintion" - const Filter& data() const; +#include "jobs/basejob.h" - protected: - Status parseJson(const QJsonDocument& data) override; +namespace Quotient { - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +/*! \brief Upload a new filter. + * + * Uploads a new filter definition to the homeserver. + * Returns a filter ID that may be used in future requests to + * restrict which events are returned to the client. + */ +class QUOTIENT_API DefineFilterJob : public BaseJob { +public: + /*! \brief Upload a new filter. + * + * \param userId + * The id of the user uploading the filter. The access token must be + * authorized to make requests for this user id. + * + * \param filter + * The filter to upload. + */ + explicit DefineFilterJob(const QString& userId, const Filter& filter); + + // Result properties + + /// The ID of the filter that was created. Cannot start + /// 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); } +}; + +/*! \brief Download a filter + * + */ +class QUOTIENT_API GetFilterJob : public BaseJob { +public: + /*! \brief Download a filter + * + * \param userId + * The user ID to download a filter for. + * + * \param filterId + * The filter ID to download. + */ + explicit GetFilterJob(const QString& userId, const QString& filterId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetFilterJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& filterId); + + // Result properties + + /// The filter definition. + Filter filter() const { return fromJson<Filter>(jsonData()); } +}; + +} // namespace Quotient diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml deleted file mode 100644 index cb5e553c..00000000 --- a/lib/csapi/gtad.yaml +++ /dev/null @@ -1,145 +0,0 @@ -analyzer: - subst: - "%CLIENT_RELEASE_LABEL%": r0 - "%CLIENT_MAJOR_VERSION%": r0 - identifiers: - signed: signedData - unsigned: unsignedData - default: isDefault - origin_server_ts: originServerTimestamp # Instead of originServerTs - start: begin # Because start() is a method in BaseJob - m.upload.size: uploadSize - m.homeserver: homeserver - m.identity_server: identityServer - AuthenticationData/additionalProperties: authInfo - - # Structure inside `types`: - # - swaggerType: <targetTypeSpec> - # OR - # - swaggerType: - # - swaggerFormat: <targetTypeSpec> - # - /swaggerFormatRegEx/: <targetTypeSpec> - # - //: <targetTypeSpec> # default, if the format doesn't mach anything above - # WHERE - # targetTypeSpec = targetType OR - # { type: targetType, imports: <filename OR [ filenames... ]>, <other attributes...> } - # swaggerType can be +set/+on pair; attributes from the map under +set - # are added to each type from the sequence under +on. - types: - - +set: &UseOmittable - useOmittable: - imports: [ '"converters.h"' ] - omittedValue: 'none' # See `none` in converters.h - +on: - - integer: - - int64: qint64 - - int32: qint32 - - //: int - - number: - - float: float - - //: double - - boolean: { type: bool, omittedValue: 'false' } - - string: - - byte: &ByteStream - type: QIODevice* - imports: <QtCore/QIODevice> - - binary: *ByteStream - - +set: { avoidCopy: } - +on: - - date: - type: QDate - initializer: QDate::fromString("{{defaultValue}}") - imports: <QtCore/QDate> - - dateTime: - type: QDateTime - initializer: QDateTime::fromString("{{defaultValue}}") - imports: <QtCore/QDateTime> - - //: &QString - type: QString - initializer: QStringLiteral("{{defaultValue}}") - isString: - - file: *ByteStream - - +set: { avoidCopy: } - +on: - - object: &QJsonObject { type: QJsonObject, imports: <QtCore/QJsonObject> } - - $ref: - - +set: { moveOnly: } - +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$/: pass # This $ref is only used in an array, see below - - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types - - schema: # Properties of inline structure definitions - - TurnServerCredentials: *QJsonObject # Because it's used as is - - //: *UseOmittable - - array: - - string: QStringList - - +set: { moveOnly: } - +on: - - /^Notification|Result$/: - type: "std::vector<{{1}}>" - imports: '"events/eventloader.h"' - - /m\.room\.member$/: - type: "EventsArray<RoomMemberEvent>" - imports: '"events/roommemberevent.h"' - - /state_event.yaml$/: - type: StateEvents - - /room_event.yaml$/: - type: RoomEvents - - /event.yaml$/: - type: Events - - //: { type: "QVector<{{1}}>", imports: <QtCore/QVector> } - - map: # `additionalProperties` in OpenAPI - - RoomState: - type: "std::unordered_map<QString, {{1}}>" - moveOnly: - imports: <unordered_map> - - /.+/: - type: "QHash<QString, {{1}}>" - imports: <QtCore/QHash> - - //: - type: QVariantHash - imports: <QtCore/QVariant> - - variant: # A sequence `type` (multitype) in OpenAPI - - /^string,null|null,string$/: *QString - - //: { type: QVariant, imports: <QtCore/QVariant> } - - #operations: - -mustache: - constants: - # Syntax elements used by GTAD -# _quote: '"' # Common quote for left and right -# _leftQuote: '"' -# _rightQuote: '"' -# _joinChar: ',' # The character used by {{_join}} - not working yet - _comment: '//' - partials: - _typeRenderer: "{{#scope}}{{scopeCamelCase}}Job::{{/scope}}{{>name}}" - omittedValue: '{}' # default value to initialize omitted parameters with - initializer: '{{defaultValue}}' - cjoin: '{{#hasMore}}, {{/hasMore}}' - openOmittable: "{{^required?}}{{#useOmittable}}{{^defaultValue}}Omittable<{{/defaultValue}}{{/useOmittable}}{{/required?}}" - closeOmittable: "{{^required?}}{{#useOmittable}}{{^defaultValue}}>{{/defaultValue}}{{/useOmittable}}{{/required?}}" - maybeOmittableType: "{{>openOmittable}}{{dataType.name}}{{>closeOmittable}}" - qualifiedMaybeOmittableType: "{{>openOmittable}}{{dataType.qualifiedName}}{{>closeOmittable}}" - maybeCrefType: "{{#avoidCopy}}const {{/avoidCopy}}{{>maybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}{{#moveOnly}}&&{{/moveOnly}}" - qualifiedMaybeCrefType: - "{{#avoidCopy}}const {{/avoidCopy}}{{>qualifiedMaybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}{{#moveOnly}}&&{{/moveOnly}}" - initializeDefaultValue: "{{#defaultValue}}{{>initializer}}{{/defaultValue}}{{^defaultValue}}{{>omittedValue}}{{/defaultValue}}" - joinedParamDecl: '{{>maybeCrefType}} {{paramName}}{{^required?}} = {{>initializeDefaultValue}}{{/required?}}{{>cjoin}}' - joinedParamDef: '{{>maybeCrefType}} {{paramName}}{{>cjoin}}' - passQueryParams: '{{#queryParams}}{{paramName}}{{>cjoin}}{{/queryParams}}' - copyrightName: Kitsune Ral - copyrightEmail: <kitsune-ral@users.sf.net> - - templates: - - "{{base}}.h.mustache" - - "{{base}}.cpp.mustache" - - #outFilesList: apifiles.txt - diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 7dc33b18..41a8b5be 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -4,22 +4,15 @@ #include "inviting.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto InviteUserJobName = QStringLiteral("InviteUserJob"); - -InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId) - : BaseJob(HttpVerb::Post, InviteUserJobName, - basePath % "/rooms/" % roomId % "/invite") +InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index 6d5d2e99..cb9d052b 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -6,40 +6,42 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Invite a user to participate in a particular room. + * + * *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](/client-server-api/#post_matrixclientv3roomsroomidinvite-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 + * room. + * + * Only users currently in a particular room can invite other users to + * join that room. + * + * If the user was invited to the room, the homeserver will append a + * `m.room.member` event to the room. + */ +class QUOTIENT_API InviteUserJob : public BaseJob { +public: + /*! \brief Invite a user to participate in a particular room. + * + * \param roomId + * The room identifier (not alias) to which to invite the user. + * + * \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, + const QString& reason = {}); +}; - /// 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`_. - /// - /// This API invites a user to participate in a particular room. - /// They do not start participating in the room until they actually join the - /// room. - /// - /// Only users currently in a particular room can invite other users to - /// 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`_ - class InviteUserJob : public BaseJob - { - public: - /*! Invite a user to participate in a particular room. - * \param roomId - * The room identifier (not alias) to which to invite the user. - * \param userId - * The fully qualified user ID of the invitee. - */ - explicit InviteUserJob(const QString& roomId, const QString& userId); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 71781154..cdba95e9 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -4,126 +4,41 @@ #include "joining.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - QJsonObject toJson(const JoinRoomByIdJob::ThirdPartySigned& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } -} // namespace QMatrixClient - -class JoinRoomByIdJob::Private -{ - public: - QString roomId; -}; - -static const auto JoinRoomByIdJobName = QStringLiteral("JoinRoomByIdJob"); - -JoinRoomByIdJob::JoinRoomByIdJob(const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned) - : BaseJob(HttpVerb::Post, JoinRoomByIdJobName, - basePath % "/rooms/" % roomId % "/join") - , d(new Private) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), thirdPartySigned); - setRequestData(_data); -} - -JoinRoomByIdJob::~JoinRoomByIdJob() = default; - -const QString& JoinRoomByIdJob::roomId() const -{ - return d->roomId; -} - -BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("room_id"_ls)) - return { JsonParseError, - "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); - return Success; +using namespace Quotient; + +JoinRoomByIdJob::JoinRoomByIdJob( + const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/join")) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_signed"), + thirdPartySigned); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); + addExpectedKey("room_id"); } -namespace QMatrixClient -{ - // Converters - - QJsonObject toJson(const JoinRoomJob::Signed& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } - - QJsonObject toJson(const JoinRoomJob::ThirdPartySigned& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("signed"), pod.signedData); - return jo; - } -} // namespace QMatrixClient - -class JoinRoomJob::Private +auto queryToJoinRoom(const QStringList& serverName) { - public: - QString roomId; -}; - -BaseJob::Query queryToJoinRoom(const QStringList& serverName) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName); return _q; } -static const auto JoinRoomJobName = QStringLiteral("JoinRoomJob"); - -JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, const Omittable<ThirdPartySigned>& thirdPartySigned) - : BaseJob(HttpVerb::Post, JoinRoomJobName, - basePath % "/join/" % roomIdOrAlias, - queryToJoinRoom(serverName)) - , d(new Private) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), thirdPartySigned); - setRequestData(_data); +JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, + const QStringList& serverName, + const Omittable<ThirdPartySigned>& thirdPartySigned, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"), + makePath("/_matrix/client/v3", "/join/", roomIdOrAlias), + queryToJoinRoom(serverName)) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_signed"), + thirdPartySigned); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); + addExpectedKey("room_id"); } - -JoinRoomJob::~JoinRoomJob() = default; - -const QString& JoinRoomJob::roomId() const -{ - return d->roomId; -} - -BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("room_id"_ls)) - return { JsonParseError, - "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); - return Success; -} - diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 137afbfc..c86baa90 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -4,148 +4,98 @@ #pragma once -#include "jobs/basejob.h" - -#include "converters.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations - - /// 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.* - /// - /// 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 - /// allowed to see all current state events in the room, and all subsequent - /// 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. - /// - /// 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. - class JoinRoomByIdJob : public BaseJob - { - public: - // Inner data structures - - /// 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. - QString sender; - /// The Matrix ID of the invitee. - QString mxid; - /// The state key of the m.third_party_invite event. - QString token; - /// A signatures object containing a signature of the entire signed object. - QJsonObject signatures; - }; - - // Construction/destruction - - /*! Start the requesting user participating in a particular room. - * \param roomId - * The room identifier (not alias) to join. - * \param thirdPartySigned - * 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. - */ - explicit JoinRoomByIdJob(const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned = none); - ~JoinRoomByIdJob() override; - - // Result properties - - /// The joined room id - const QString& roomId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; +#include "csapi/definitions/third_party_signed.h" - /// Start the requesting user participating in a particular room. - /// - /// *Note that this API takes either a room ID or alias, unlike* ``/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 - /// allowed to see all current state events in the room, and all subsequent - /// 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. - /// - /// 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. - class JoinRoomJob : public BaseJob - { - public: - // Inner data structures - - /// *Note that this API takes either a room ID or alias, unlike* ``/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 - /// allowed to see all current state events in the room, and all subsequent - /// 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. - /// - /// 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. - struct Signed - { - /// The Matrix ID of the user who issued the invite. - QString sender; - /// The Matrix ID of the invitee. - QString mxid; - /// The state key of the m.third_party_invite event. - QString token; - /// A signatures object containing a signature of the entire signed object. - QJsonObject signatures; - }; - - /// 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 - { - /// 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. - Signed signedData; - }; - - // Construction/destruction - - /*! Start the requesting user participating in a particular room. - * \param roomIdOrAlias - * The room identifier or alias to join. - * \param serverName - * The servers to attempt to join the room through. One of the servers - * must be participating in the room. - * \param thirdPartySigned - * 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. - */ - explicit JoinRoomJob(const QString& roomIdOrAlias, const QStringList& serverName = {}, const Omittable<ThirdPartySigned>& thirdPartySigned = none); - ~JoinRoomJob() override; - - // Result properties - - /// The joined room id - const QString& roomId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; +#include "jobs/basejob.h" - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +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.* + * + * 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 + * allowed to see all current state events in the room, and all subsequent + * 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`](/client-server-api/#get_matrixclientv3initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs. + */ +class QUOTIENT_API JoinRoomByIdJob : public BaseJob { +public: + /*! \brief Start the requesting user participating in a particular room. + * + * \param roomId + * The room identifier (not alias) to join. + * + * \param thirdPartySigned + * If 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 JoinRoomByIdJob( + const QString& roomId, + const Omittable<ThirdPartySigned>& thirdPartySigned = none, + const QString& reason = {}); + + // Result properties + + /// The joined room ID. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } +}; + +/*! \brief Start the requesting user participating in a particular room. + * + * *Note that this API takes either a room ID or alias, unlike* + * `/rooms/{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 + * allowed to see all current state events in the room, and all subsequent + * 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`](/client-server-api/#get_matrixclientv3initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs. + */ +class QUOTIENT_API JoinRoomJob : public BaseJob { +public: + /*! \brief Start the requesting user participating in a particular room. + * + * \param roomIdOrAlias + * The room identifier or alias to join. + * + * \param serverName + * The servers to attempt to join the room through. One of the servers + * 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 + * 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 QString& reason = {}); + + // Result properties + + /// The joined room ID. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index c7492411..2e4978f2 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -4,209 +4,68 @@ #include "keys.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class UploadKeysJob::Private -{ - public: - QHash<QString, int> oneTimeKeyCounts; -}; - -static const auto UploadKeysJobName = QStringLiteral("UploadKeysJob"); - -UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys, const QHash<QString, QVariant>& oneTimeKeys) - : BaseJob(HttpVerb::Post, UploadKeysJobName, - basePath % "/keys/upload") - , d(new Private) +UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys, + const OneTimeKeys& oneTimeKeys, + const OneTimeKeys& fallbackKeys) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"), + makePath("/_matrix/client/v3", "/keys/upload")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("device_keys"), deviceKeys); - addParam<IfNotEmpty>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - setRequestData(_data); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_keys"), deviceKeys); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("one_time_keys"), + oneTimeKeys); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("fallback_keys"), + fallbackKeys); + setRequestData({ _dataJson }); + addExpectedKey("one_time_key_counts"); } -UploadKeysJob::~UploadKeysJob() = default; - -const QHash<QString, int>& UploadKeysJob::oneTimeKeyCounts() const +QueryKeysJob::QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, + Omittable<int> timeout, const QString& token) + : BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"), + makePath("/_matrix/client/v3", "/keys/query")) { - return d->oneTimeKeyCounts; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout); + addParam<>(_dataJson, QStringLiteral("device_keys"), deviceKeys); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("token"), token); + setRequestData({ _dataJson }); } -BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data) +ClaimKeysJob::ClaimKeysJob( + const QHash<QString, QHash<QString, QString>>& oneTimeKeys, + Omittable<int> timeout) + : BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"), + makePath("/_matrix/client/v3", "/keys/claim")) { - auto json = data.object(); - if (!json.contains("one_time_key_counts"_ls)) - return { JsonParseError, - "The key 'one_time_key_counts' not found in the response" }; - d->oneTimeKeyCounts = fromJson<QHash<QString, int>>(json.value("one_time_key_counts"_ls)); - return Success; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout); + addParam<>(_dataJson, QStringLiteral("one_time_keys"), oneTimeKeys); + setRequestData({ _dataJson }); + addExpectedKey("one_time_keys"); } -namespace QMatrixClient -{ - // Converters - - template <> struct FromJsonObject<QueryKeysJob::UnsignedDeviceInfo> - { - QueryKeysJob::UnsignedDeviceInfo operator()(const QJsonObject& jo) const - { - QueryKeysJob::UnsignedDeviceInfo result; - result.deviceDisplayName = - fromJson<QString>(jo.value("device_display_name"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<QueryKeysJob::DeviceInformation> - { - QueryKeysJob::DeviceInformation operator()(const QJsonObject& jo) const - { - QueryKeysJob::DeviceInformation result; - result.unsignedData = - fromJson<QueryKeysJob::UnsignedDeviceInfo>(jo.value("unsigned"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class QueryKeysJob::Private -{ - public: - QHash<QString, QJsonObject> failures; - QHash<QString, QHash<QString, DeviceInformation>> deviceKeys; -}; - -static const auto QueryKeysJobName = QStringLiteral("QueryKeysJob"); - -QueryKeysJob::QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, Omittable<int> timeout, const QString& token) - : BaseJob(HttpVerb::Post, QueryKeysJobName, - basePath % "/keys/query") - , d(new Private) +auto queryToGetKeysChanges(const QString& from, const QString& to) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); - addParam<>(_data, QStringLiteral("device_keys"), deviceKeys); - addParam<IfNotEmpty>(_data, QStringLiteral("token"), token); - setRequestData(_data); -} - -QueryKeysJob::~QueryKeysJob() = default; - -const QHash<QString, QJsonObject>& QueryKeysJob::failures() const -{ - return d->failures; -} - -const QHash<QString, QHash<QString, QueryKeysJob::DeviceInformation>>& QueryKeysJob::deviceKeys() const -{ - return d->deviceKeys; -} - -BaseJob::Status QueryKeysJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls)); - d->deviceKeys = fromJson<QHash<QString, QHash<QString, DeviceInformation>>>(json.value("device_keys"_ls)); - return Success; -} - -class ClaimKeysJob::Private -{ - public: - QHash<QString, QJsonObject> failures; - QHash<QString, QHash<QString, QVariant>> oneTimeKeys; -}; - -static const auto ClaimKeysJobName = QStringLiteral("ClaimKeysJob"); - -ClaimKeysJob::ClaimKeysJob(const QHash<QString, QHash<QString, QString>>& oneTimeKeys, Omittable<int> timeout) - : BaseJob(HttpVerb::Post, ClaimKeysJobName, - basePath % "/keys/claim") - , d(new Private) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); - addParam<>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - setRequestData(_data); -} - -ClaimKeysJob::~ClaimKeysJob() = default; - -const QHash<QString, QJsonObject>& ClaimKeysJob::failures() const -{ - return d->failures; -} - -const QHash<QString, QHash<QString, QVariant>>& ClaimKeysJob::oneTimeKeys() const -{ - return d->oneTimeKeys; -} - -BaseJob::Status ClaimKeysJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls)); - d->oneTimeKeys = fromJson<QHash<QString, QHash<QString, QVariant>>>(json.value("one_time_keys"_ls)); - return Success; -} - -class GetKeysChangesJob::Private -{ - public: - QStringList changed; - QStringList left; -}; - -BaseJob::Query queryToGetKeysChanges(const QString& from, const QString& to) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("from"), from); addParam<>(_q, QStringLiteral("to"), to); return _q; } -QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to) +QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, + const QString& to) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/keys/changes", - queryToGetKeysChanges(from, to)); + makePath("/_matrix/client/v3", + "/keys/changes"), + queryToGetKeysChanges(from, to)); } -static const auto GetKeysChangesJobName = QStringLiteral("GetKeysChangesJob"); - GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to) - : BaseJob(HttpVerb::Get, GetKeysChangesJobName, - basePath % "/keys/changes", - queryToGetKeysChanges(from, to)) - , d(new Private) -{ -} - -GetKeysChangesJob::~GetKeysChangesJob() = default; - -const QStringList& GetKeysChangesJob::changed() const -{ - return d->changed; -} - -const QStringList& GetKeysChangesJob::left() const -{ - return d->left; -} - -BaseJob::Status GetKeysChangesJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->changed = fromJson<QStringList>(json.value("changed"_ls)); - d->left = fromJson<QStringList>(json.value("left"_ls)); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"), + makePath("/_matrix/client/v3", "/keys/changes"), + queryToGetKeysChanges(from, to)) +{} diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index e59b1dae..b28de305 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,216 +4,293 @@ #pragma once +#include "csapi/definitions/cross_signing_key.h" +#include "csapi/definitions/device_keys.h" + +#include "e2ee/e2ee.h" + #include "jobs/basejob.h" -#include "csapi/definitions/device_keys.h" -#include <QtCore/QHash> -#include "converters.h" -#include <QtCore/QVariant> -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Upload end-to-end encryption keys. + * + * Publishes end-to-end encryption keys for the device. + */ +class QUOTIENT_API UploadKeysJob : public BaseJob { +public: + /*! \brief Upload end-to-end encryption keys. + * + * \param deviceKeys + * Identity keys for the device. May be absent if no new + * identity keys are required. + * + * \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](/client-server-api/#key-algorithms). + * + * May be absent if no new one-time keys are required. + * + * \param fallbackKeys + * The public key which should be used if the device's one-time keys + * are exhausted. The fallback key is not deleted once used, but should + * be replaced when additional one-time keys are being uploaded. The + * server will notify the client of the fallback key being used through + * `/sync`. + * + * There can only be at most one key per algorithm uploaded, and the + * server will only persist one key per algorithm. + * + * When uploading a signed key, an additional `fallback: true` key should + * be included to denote that the key is a fallback key. + * + * May be absent if a new fallback key is not required. + */ + explicit UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys = none, + const OneTimeKeys& oneTimeKeys = {}, + const OneTimeKeys& fallbackKeys = {}); - /// Upload end-to-end encryption keys. - /// - /// Publishes end-to-end encryption keys for the device. - class UploadKeysJob : public BaseJob + // Result properties + + /// For each key algorithm, the number of unclaimed one-time keys + /// of that type currently held on the server for this device. + /// If an algorithm is not listed, the count for that algorithm + /// is to be assumed zero. + QHash<QString, int> oneTimeKeyCounts() const { - public: - /*! Upload end-to-end encryption keys. - * \param deviceKeys - * Identity keys for the device. May be absent if no new - * identity keys are required. - * \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. - * - * May be absent if no new one-time keys are required. - */ - explicit UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys = none, const QHash<QString, QVariant>& oneTimeKeys = {}); - ~UploadKeysJob() override; - - // Result properties - - /// For each key algorithm, the number of unclaimed one-time keys - /// of that type currently held on the server for this device. - const QHash<QString, int>& oneTimeKeyCounts() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + return loadFromJson<QHash<QString, int>>("one_time_key_counts"_ls); + } +}; + +/*! \brief Download device identity keys. + * + * Returns the current devices and identity keys for the given users. + */ +class QUOTIENT_API QueryKeysJob : public BaseJob { +public: + // Inner data structures + + /// Additional data added to the device key information + /// by intermediate servers, and not covered by the + /// signatures. + struct UnsignedDeviceInfo { + /// The display name which the user set on the device. + QString deviceDisplayName; }; - /// Download device identity keys. - /// /// Returns the current devices and identity keys for the given users. - class QueryKeysJob : public BaseJob - { - public: - // Inner data structures - - /// Additional data added to the device key information - /// by intermediate servers, and not covered by the - /// signatures. - struct UnsignedDeviceInfo - { - /// The display name which the user set on the device. - QString deviceDisplayName; - }; - - /// Returns the current devices and identity keys for the given users. - struct DeviceInformation : DeviceKeys - { - /// Additional data added to the device key information - /// by intermediate servers, and not covered by the - /// signatures. - Omittable<UnsignedDeviceInfo> unsignedData; - }; - - // Construction/destruction - - /*! Download device identity keys. - * \param deviceKeys - * The keys to be downloaded. A map from user ID, to a list of - * device IDs, or to an empty list to indicate all devices for the - * corresponding user. - * \param timeout - * The time (in milliseconds) to wait when downloading keys from - * remote servers. 10 seconds is the recommended default. - * \param token - * If the client is fetching keys as a result of a device update received - * in a sync request, this should be the 'since' token of that sync request, - * or any later sync token. This allows the server to ensure its response - * contains the keys advertised by the notification in that sync. - */ - explicit QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, Omittable<int> timeout = none, const QString& token = {}); - ~QueryKeysJob() override; - - // Result properties - - /// If any remote homeservers could not be reached, they are - /// recorded here. The names of the properties are the names of - /// the unreachable servers. - /// - /// 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. - const QHash<QString, QJsonObject>& failures() const; - /// 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`` - /// property. - const QHash<QString, QHash<QString, DeviceInformation>>& deviceKeys() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct DeviceInformation : DeviceKeys { + /// Additional data added to the device key information + /// by intermediate servers, and not covered by the + /// signatures. + Omittable<UnsignedDeviceInfo> unsignedData; }; - /// Claim one-time encryption keys. + // Construction/destruction + + /*! \brief Download device identity keys. + * + * \param deviceKeys + * The keys to be downloaded. A map from user ID, to a list of + * device IDs, or to an empty list to indicate all devices for the + * corresponding user. + * + * \param timeout + * The time (in milliseconds) to wait when downloading keys from + * remote servers. 10 seconds is the recommended default. + * + * \param token + * If the client is fetching keys as a result of a device update received + * in a sync request, this should be the 'since' token of that sync + * request, or any later sync token. This allows the server to ensure its + * response contains the keys advertised by the notification in that sync. + */ + explicit QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, + Omittable<int> timeout = none, + const QString& token = {}); + + // Result properties + + /// If any remote homeservers could not be reached, they are + /// recorded here. The names of the properties are the names of + /// the unreachable servers. /// - /// Claims one-time keys for use in pre-key messages. - class ClaimKeysJob : public BaseJob + /// 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. + QHash<QString, QJsonObject> failures() const { - public: - /*! Claim one-time encryption keys. - * \param oneTimeKeys - * The keys to be claimed. A map from user ID, to a map from - * device ID to algorithm name. - * \param timeout - * The time (in milliseconds) to wait when downloading keys from - * remote servers. 10 seconds is the recommended default. - */ - explicit ClaimKeysJob(const QHash<QString, QHash<QString, QString>>& oneTimeKeys, Omittable<int> timeout = none); - ~ClaimKeysJob() override; - - // Result properties - - /// If any remote homeservers could not be reached, they are - /// recorded here. The names of the properties are the names of - /// the unreachable servers. - /// - /// 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. - const QHash<QString, QJsonObject>& failures() const; - /// 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. - const QHash<QString, QHash<QString, QVariant>>& oneTimeKeys() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls); + } + + /// 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` + /// 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); + } - /// Query users with recent device key updates. + /// 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 <> +struct JsonObjectConverter<QueryKeysJob::UnsignedDeviceInfo> { + static void fillFrom(const QJsonObject& jo, + QueryKeysJob::UnsignedDeviceInfo& result) + { + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); + } +}; + +template <> +struct JsonObjectConverter<QueryKeysJob::DeviceInformation> { + static void fillFrom(const QJsonObject& jo, + QueryKeysJob::DeviceInformation& result) + { + fillFromJson<DeviceKeys>(jo, result); + fromJson(jo.value("unsigned"_ls), result.unsignedData); + } +}; + +/*! \brief Claim one-time encryption keys. + * + * Claims one-time keys for use in pre-key messages. + */ +class QUOTIENT_API ClaimKeysJob : public BaseJob { +public: + /*! \brief Claim one-time encryption keys. + * + * \param oneTimeKeys + * The keys to be claimed. A map from user ID, to a map from + * device ID to algorithm name. + * + * \param timeout + * The time (in milliseconds) to wait when downloading keys from + * remote servers. 10 seconds is the recommended default. + */ + explicit ClaimKeysJob( + const QHash<QString, QHash<QString, QString>>& oneTimeKeys, + Omittable<int> timeout = none); + + // Result properties + + /// If any remote homeservers could not be reached, they are + /// recorded here. The names of the properties are the names of + /// the unreachable servers. /// - /// Gets a list of users who have updated their device identity keys since a - /// previous sync token. - /// - /// 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* - /// * added new device identity keys or removed an existing device with - /// identity keys, between ``from`` and ``to``. - class GetKeysChangesJob : public BaseJob + /// 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. + QHash<QString, QJsonObject> failures() const { - public: - /*! 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 - * 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. - */ - explicit GetKeysChangesJob(const QString& from, const QString& to); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetKeysChangesJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to); - - ~GetKeysChangesJob() override; - - // Result properties - - /// The Matrix User IDs of all users who updated their device - /// identity keys. - const QStringList& changed() const; - /// The Matrix User IDs of all users who may have left all - /// the end-to-end encrypted rooms they previously shared - /// with the user. - const QStringList& left() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + 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. + /// + /// See the [key algorithms](/client-server-api/#key-algorithms) section for + /// information on the Key Object format. + /// + /// If necessary, the claimed key might be a fallback key. Fallback + /// keys are re-used by the server until replaced by the device. + QHash<QString, QHash<QString, OneTimeKeys>> oneTimeKeys() const + { + return loadFromJson<QHash<QString, QHash<QString, OneTimeKeys>>>( + "one_time_keys"_ls); + } +}; + +/*! \brief Query users with recent device key updates. + * + * Gets a list of users who have updated their device identity keys since a + * previous sync token. + * + * 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* + * * added new device identity keys or removed an existing device with + * identity keys, between `from` and `to`. + */ +class QUOTIENT_API 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`](/client-server-api/#get_matrixclientv3sync). 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`](/client-server-api/#get_matrixclientv3sync) - 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); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetKeysChangesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& from, + const QString& to); + + // Result properties + + /// The Matrix User IDs of all users who updated their device + /// identity keys. + QStringList changed() const + { + return loadFromJson<QStringList>("changed"_ls); + } + + /// The Matrix User IDs of all users who may have left all + /// the end-to-end encrypted rooms they previously shared + /// with the user. + QStringList left() const { return loadFromJson<QStringList>("left"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp index 1d6d5543..4ca39c4c 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -4,23 +4,15 @@ #include "kicking.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto KickJobName = QStringLiteral("KickJob"); - -KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) - : BaseJob(HttpVerb::Post, KickJobName, - basePath % "/rooms/" % roomId % "/kick") +KickJob::KickJob(const QString& roomId, const QString& userId, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("KickJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/kick")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 5968187e..6ac106e2 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -6,32 +6,37 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Kick a user from the room. + * + * Kick a user from the room. + * + * 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 + * directly adjust the target member's state by making a request to + * `/rooms/<room id>/state/m.room.member/<user id>`. + */ +class QUOTIENT_API KickJob : public BaseJob { +public: + /*! \brief Kick a user from the room. + * + * \param roomId + * The room identifier (not alias) from which the user should be kicked. + * + * \param userId + * The fully qualified user ID of the user being kicked. + * + * \param reason + * The reason the user has been kicked. This will be supplied as the + * `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 = {}); +}; - /// Kick a user from the room. - /// - /// Kick a user from the room. - /// - /// 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 directly adjust - /// the target member's state by making a request to ``/rooms/<room id>/state/m.room.member/<user id>``. - class KickJob : public BaseJob - { - public: - /*! Kick a user from the room. - * \param roomId - * The room identifier (not alias) from which the user should be kicked. - * \param userId - * The fully qualified user ID of the user being kicked. - * \param reason - * The reason the user has been kicked. This will be supplied as the - * ``reason`` on the target's updated `m.room.member`_ event. - */ - explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp new file mode 100644 index 00000000..b9da4b9b --- /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/v3", "/knock/", roomIdOrAlias), + queryToKnockRoom(serverName)) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); + addExpectedKey("room_id"); +} diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h new file mode 100644 index 00000000..f43033a8 --- /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_matrixclientv3sync) API. + */ +class QUOTIENT_API 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 09e5f83b..ba91f26a 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -4,39 +4,25 @@ #include "leaving.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/leave"); -} - -static const auto LeaveRoomJobName = QStringLiteral("LeaveRoomJob"); - -LeaveRoomJob::LeaveRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Post, LeaveRoomJobName, - basePath % "/rooms/" % roomId % "/leave") +LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/leave")) { + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/forget"); + makePath("/_matrix/client/v3", "/rooms/", + roomId, "/forget")); } -static const auto ForgetRoomJobName = QStringLiteral("ForgetRoomJob"); - ForgetRoomJob::ForgetRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Post, ForgetRoomJobName, - basePath % "/rooms/" % roomId % "/forget") -{ -} - + : BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget")) +{} diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h index 3a340034..19cac3f0 100644 --- a/lib/csapi/leaving.h +++ b/lib/csapi/leaving.h @@ -6,70 +6,63 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Stop the requesting user participating in a particular room. - /// - /// This API stops a user participating in a particular room. - /// - /// If the user was already in the room, they will no longer be able to see - /// new events in the room. If the room requires an invite to join, they - /// will need to be re-invited before they can re-join. - /// - /// If the user was invited to the room, but had not joined, this call - /// serves to reject the invite. - /// - /// The user will still be allowed to retrieve history from the room which - /// they were previously allowed to see. - class LeaveRoomJob : public BaseJob - { - public: - /*! Stop the requesting user participating in a particular room. - * \param roomId - * The room identifier to leave. - */ - explicit LeaveRoomJob(const QString& roomId); - - /*! 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. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - }; +/*! \brief Stop the requesting user participating in a particular room. + * + * This API stops a user participating in a particular room. + * + * If the user was already in the room, they will no longer be able to see + * new events in the room. If the room requires an invite to join, they + * will need to be re-invited before they can re-join. + * + * If the user was invited to the room, but had not joined, this call + * serves to reject the invite. + * + * The user will still be allowed to retrieve history from the room which + * they were previously allowed to see. + */ +class QUOTIENT_API LeaveRoomJob : public BaseJob { +public: + /*! \brief Stop the requesting user participating in a particular room. + * + * \param roomId + * The room identifier to leave. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. + */ + explicit LeaveRoomJob(const QString& roomId, const QString& reason = {}); +}; - /// Stop the requesting user remembering about a particular room. - /// - /// This API stops a user remembering about a particular room. - /// - /// In general, history is a first class citizen in Matrix. After this API - /// is called, however, a user will no longer be able to retrieve history - /// for this room. If all users on a homeserver forget a room, the room is - /// eligible for deletion from that homeserver. - /// - /// If the user is currently joined to the room, they must leave the room - /// before calling this API. - class ForgetRoomJob : public BaseJob - { - public: - /*! Stop the requesting user remembering about a particular room. - * \param roomId - * The room identifier to forget. - */ - explicit ForgetRoomJob(const QString& roomId); +/*! \brief Stop the requesting user remembering about a particular room. + * + * This API stops a user remembering about a particular room. + * + * In general, history is a first class citizen in Matrix. After this API + * is called, however, a user will no longer be able to retrieve history + * for this room. If all users on a homeserver forget a room, the room is + * eligible for deletion from that homeserver. + * + * If the user is currently joined to the room, they must leave the room + * before calling this API. + */ +class QUOTIENT_API ForgetRoomJob : public BaseJob { +public: + /*! \brief Stop the requesting user remembering about a particular room. + * + * \param roomId + * The room identifier to forget. + */ + explicit ForgetRoomJob(const QString& roomId); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * ForgetRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for ForgetRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); +}; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index a745dba1..cdcf3eb2 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -4,49 +4,17 @@ #include "list_joined_rooms.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetJoinedRoomsJob::Private -{ - public: - QStringList joinedRooms; -}; +using namespace Quotient; QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/joined_rooms"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/v3", "/joined_rooms")); } -static const auto GetJoinedRoomsJobName = QStringLiteral("GetJoinedRoomsJob"); - GetJoinedRoomsJob::GetJoinedRoomsJob() - : BaseJob(HttpVerb::Get, GetJoinedRoomsJobName, - basePath % "/joined_rooms") - , d(new Private) -{ -} - -GetJoinedRoomsJob::~GetJoinedRoomsJob() = default; - -const QStringList& GetJoinedRoomsJob::joinedRooms() const + : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"), + makePath("/_matrix/client/v3", "/joined_rooms")) { - return d->joinedRooms; + addExpectedKey("joined_rooms"); } - -BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("joined_rooms"_ls)) - return { JsonParseError, - "The key 'joined_rooms' not found in the response" }; - d->joinedRooms = fromJson<QStringList>(json.value("joined_rooms"_ls)); - return Success; -} - diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h index 881a97b4..aea68afd 100644 --- a/lib/csapi/list_joined_rooms.h +++ b/lib/csapi/list_joined_rooms.h @@ -6,39 +6,31 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - +/*! \brief Lists the user's current rooms. + * + * This API returns a list of the user's current rooms. + */ +class QUOTIENT_API GetJoinedRoomsJob : public BaseJob { +public: /// Lists the user's current rooms. - /// - /// This API returns a list of the user's current rooms. - class GetJoinedRoomsJob : public BaseJob - { - public: - explicit GetJoinedRoomsJob(); + explicit GetJoinedRoomsJob(); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetJoinedRoomsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetJoinedRoomsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - ~GetJoinedRoomsJob() override; + // Result properties - // Result properties - - /// The ID of each room in which the user has ``joined`` membership. - const QStringList& joinedRooms() const; - - protected: - Status parseJson(const QJsonDocument& data) override; + /// The ID of each room in which the user has `joined` membership. + QStringList joinedRooms() const + { + return loadFromJson<QStringList>("joined_rooms"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 2fdb2005..4deecfc2 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -4,158 +4,87 @@ #include "list_public_rooms.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetRoomVisibilityOnDirectoryJob::Private -{ - public: - QString visibility; -}; - -QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, + const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/directory/list/room/" % roomId); -} - -static const auto GetRoomVisibilityOnDirectoryJobName = QStringLiteral("GetRoomVisibilityOnDirectoryJob"); - -GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob(const QString& roomId) - : BaseJob(HttpVerb::Get, GetRoomVisibilityOnDirectoryJobName, - basePath % "/directory/list/room/" % roomId, false) - , d(new Private) -{ -} - -GetRoomVisibilityOnDirectoryJob::~GetRoomVisibilityOnDirectoryJob() = default; - -const QString& GetRoomVisibilityOnDirectoryJob::visibility() const -{ - return d->visibility; + makePath("/_matrix/client/v3", + "/directory/list/room/", roomId)); } -BaseJob::Status GetRoomVisibilityOnDirectoryJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->visibility = fromJson<QString>(json.value("visibility"_ls)); - return Success; +GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob( + const QString& roomId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"), + makePath("/_matrix/client/v3", "/directory/list/room/", roomId), + false) +{} + +SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( + const QString& roomId, const QString& visibility) + : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"), + makePath("/_matrix/client/v3", "/directory/list/room/", roomId)) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("visibility"), visibility); + setRequestData({ _dataJson }); } -static const auto SetRoomVisibilityOnDirectoryJobName = QStringLiteral("SetRoomVisibilityOnDirectoryJob"); - -SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob(const QString& roomId, const QString& visibility) - : BaseJob(HttpVerb::Put, SetRoomVisibilityOnDirectoryJobName, - basePath % "/directory/list/room/" % roomId) +auto queryToGetPublicRooms(Omittable<int> limit, const QString& since, + const QString& server) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); - setRequestData(_data); -} - -class GetPublicRoomsJob::Private -{ - public: - PublicRoomsResponse data; -}; - -BaseJob::Query 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); return _q; } -QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit, const QString& since, const QString& server) +QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit, + const QString& since, + const QString& server) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/publicRooms", - queryToGetPublicRooms(limit, since, server)); + makePath("/_matrix/client/v3", + "/publicRooms"), + queryToGetPublicRooms(limit, since, server)); } -static const auto GetPublicRoomsJobName = QStringLiteral("GetPublicRoomsJob"); - -GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since, const QString& server) - : BaseJob(HttpVerb::Get, GetPublicRoomsJobName, - basePath % "/publicRooms", - queryToGetPublicRooms(limit, since, server), - {}, false) - , d(new Private) +GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since, + const QString& server) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"), + makePath("/_matrix/client/v3", "/publicRooms"), + queryToGetPublicRooms(limit, since, server), {}, false) { + addExpectedKey("chunk"); } -GetPublicRoomsJob::~GetPublicRoomsJob() = default; - -const PublicRoomsResponse& GetPublicRoomsJob::data() const +auto queryToQueryPublicRooms(const QString& server) { - return d->data; -} - -BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<PublicRoomsResponse>(data); - return Success; -} - -namespace QMatrixClient -{ - // Converters - - QJsonObject toJson(const QueryPublicRoomsJob::Filter& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); - return jo; - } -} // namespace QMatrixClient - -class QueryPublicRoomsJob::Private -{ - public: - PublicRoomsResponse data; -}; - -BaseJob::Query queryToQueryPublicRooms(const QString& server) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("server"), server); return _q; } -static const auto QueryPublicRoomsJobName = QStringLiteral("QueryPublicRoomsJob"); - -QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, bool includeAllNetworks, const QString& thirdPartyInstanceId) - : BaseJob(HttpVerb::Post, QueryPublicRoomsJobName, - basePath % "/publicRooms", - queryToQueryPublicRooms(server)) - , d(new Private) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit); - addParam<IfNotEmpty>(_data, QStringLiteral("since"), since); - addParam<IfNotEmpty>(_data, QStringLiteral("filter"), filter); - addParam<IfNotEmpty>(_data, QStringLiteral("include_all_networks"), includeAllNetworks); - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_instance_id"), thirdPartyInstanceId); - setRequestData(_data); -} - -QueryPublicRoomsJob::~QueryPublicRoomsJob() = default; - -const PublicRoomsResponse& QueryPublicRoomsJob::data() const -{ - return d->data; +QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, + Omittable<int> limit, + const QString& since, + const Omittable<Filter>& filter, + Omittable<bool> includeAllNetworks, + const QString& thirdPartyInstanceId) + : BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"), + makePath("/_matrix/client/v3", "/publicRooms"), + queryToQueryPublicRooms(server)) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("since"), since); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("filter"), filter); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("include_all_networks"), + includeAllNetworks); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_instance_id"), + thirdPartyInstanceId); + setRequestData({ _dataJson }); + addExpectedKey("chunk"); } - -BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<PublicRoomsResponse>(data); - return Success; -} - diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 8401c134..3b6b91b9 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -4,171 +4,220 @@ #pragma once -#include "jobs/basejob.h" - #include "csapi/definitions/public_rooms_response.h" -#include "converters.h" -namespace QMatrixClient -{ - // Operations +#include "jobs/basejob.h" - /// Gets the visibility of a room in the directory - /// - /// Gets the visibility of a given room on the server's public room directory. - class GetRoomVisibilityOnDirectoryJob : public BaseJob - { - public: - /*! Gets the visibility of a room in the directory - * \param roomId - * The room ID. - */ - explicit GetRoomVisibilityOnDirectoryJob(const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomVisibilityOnDirectoryJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - ~GetRoomVisibilityOnDirectoryJob() override; - - // Result properties - - /// The visibility of the room in the directory. - const QString& visibility() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; +namespace Quotient { - /// Sets the visibility of a room in the room directory - /// - /// Sets the visibility of a given room in the server's public room - /// directory. - /// - /// Servers may choose to implement additional access control checks - /// here, for instance that room visibility can only be changed by - /// the room creator or a server administrator. - class SetRoomVisibilityOnDirectoryJob : public BaseJob +/*! \brief Gets the visibility of a room in the directory + * + * Gets the visibility of a given room on the server's public room directory. + */ +class QUOTIENT_API GetRoomVisibilityOnDirectoryJob : public BaseJob { +public: + /*! \brief Gets the visibility of a room in the directory + * + * \param roomId + * The room ID. + */ + explicit GetRoomVisibilityOnDirectoryJob(const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomVisibilityOnDirectoryJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + // Result properties + + /// The visibility of the room in the directory. + QString visibility() const { - public: - /*! Sets the visibility of a room in the room directory - * \param roomId - * The room ID. - * \param visibility - * The new visibility setting for the room. - * Defaults to 'public'. - */ - explicit SetRoomVisibilityOnDirectoryJob(const QString& roomId, const QString& visibility = {}); + return loadFromJson<QString>("visibility"_ls); + } +}; + +/*! \brief Sets the visibility of a room in the room directory + * + * Sets the visibility of a given room in the server's public room + * directory. + * + * Servers may choose to implement additional access control checks + * here, for instance that room visibility can only be changed by + * the room creator or a server administrator. + */ +class QUOTIENT_API SetRoomVisibilityOnDirectoryJob : public BaseJob { +public: + /*! \brief Sets the visibility of a room in the room directory + * + * \param roomId + * The room ID. + * + * \param visibility + * The new visibility setting for the room. + * Defaults to 'public'. + */ + explicit SetRoomVisibilityOnDirectoryJob(const QString& roomId, + const QString& visibility = {}); +}; + +/*! \brief Lists the public rooms on the server. + * + * Lists the public rooms on the server. + * + * This API returns paginated responses. The rooms are ordered by the number + * of joined members, with the largest rooms first. + */ +class QUOTIENT_API GetPublicRoomsJob : public BaseJob { +public: + /*! \brief Lists the public rooms on the server. + * + * \param limit + * Limit the number of results returned. + * + * \param since + * A pagination token from a previous request, allowing clients to + * get the next (or previous) batch of rooms. + * The direction of pagination is specified solely by which token + * is supplied, rather than via an explicit flag. + * + * \param server + * The server to fetch the public room lists from. Defaults to the + * local server. + */ + explicit GetPublicRoomsJob(Omittable<int> limit = none, + const QString& since = {}, + const QString& server = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPublicRoomsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, Omittable<int> limit = none, + const QString& since = {}, + const QString& server = {}); + + // Result properties + + /// A paginated chunk of public rooms. + QVector<PublicRoomsChunk> chunk() const + { + return loadFromJson<QVector<PublicRoomsChunk>>("chunk"_ls); + } + + /// A pagination token for the response. The absence of this token + /// means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// A pagination token that allows fetching previous results. The + /// absence of this token means there are no results before this + /// batch, i.e. this is the first batch. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } + + /// An estimate on the total number of public rooms, if the + /// server has an estimate. + Omittable<int> totalRoomCountEstimate() const + { + return loadFromJson<Omittable<int>>("total_room_count_estimate"_ls); + } +}; + +/*! \brief Lists the public rooms on the server with optional filter. + * + * Lists the public rooms on the server, with optional filter. + * + * This API returns paginated responses. The rooms are ordered by the number + * of joined members, with the largest rooms first. + */ +class QUOTIENT_API QueryPublicRoomsJob : public BaseJob { +public: + // Inner data structures + + /// Filter to apply to the results. + struct Filter { + /// An optional string to search for in the room metadata, e.g. name, + /// topic, canonical alias, etc. + QString genericSearchTerm; + /// An optional list of [room types](/client-server-api/#types) to + /// search for. To include rooms without a room type, specify `null` + /// within this list. When not specified, all applicable rooms + /// (regardless of type) are returned. + QStringList roomTypes; }; - /// Lists the public rooms on the server. - /// - /// Lists the public rooms on the server. - /// - /// This API returns paginated responses. The rooms are ordered by the number - /// of joined members, with the largest rooms first. - class GetPublicRoomsJob : public BaseJob + // Construction/destruction + + /*! \brief Lists the public rooms on the server with optional filter. + * + * \param server + * The server to fetch the public room lists from. Defaults to the + * local server. + * + * \param limit + * Limit the number of results returned. + * + * \param since + * A pagination token from a previous request, allowing clients + * to get the next (or previous) batch of rooms. The direction + * of pagination is specified solely by which token is supplied, + * rather than via an explicit flag. + * + * \param filter + * Filter to apply to the results. + * + * \param includeAllNetworks + * Whether or not to include all known networks/protocols from + * application services on the homeserver. Defaults to false. + * + * \param thirdPartyInstanceId + * The specific third party network/protocol to request from the + * homeserver. Can only be used if `include_all_networks` is false. + */ + explicit QueryPublicRoomsJob(const QString& server = {}, + Omittable<int> limit = none, + const QString& since = {}, + const Omittable<Filter>& filter = none, + Omittable<bool> includeAllNetworks = none, + const QString& thirdPartyInstanceId = {}); + + // Result properties + + /// A paginated chunk of public rooms. + QVector<PublicRoomsChunk> chunk() const { - public: - /*! Lists the public rooms on the server. - * \param limit - * Limit the number of results returned. - * \param since - * A pagination token from a previous request, allowing clients to - * get the next (or previous) batch of rooms. - * The direction of pagination is specified solely by which token - * is supplied, rather than via an explicit flag. - * \param server - * The server to fetch the public room lists from. Defaults to the - * local server. - */ - explicit GetPublicRoomsJob(Omittable<int> limit = none, const QString& since = {}, const QString& server = {}); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPublicRoomsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, Omittable<int> limit = none, const QString& since = {}, const QString& server = {}); - - ~GetPublicRoomsJob() override; - - // Result properties - - /// A list of the rooms on the server. - const PublicRoomsResponse& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + return loadFromJson<QVector<PublicRoomsChunk>>("chunk"_ls); + } + + /// A pagination token for the response. The absence of this token + /// means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// A pagination token that allows fetching previous results. The + /// absence of this token means there are no results before this + /// batch, i.e. this is the first batch. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } + + /// An estimate on the total number of public rooms, if the + /// server has an estimate. + Omittable<int> totalRoomCountEstimate() const + { + return loadFromJson<Omittable<int>>("total_room_count_estimate"_ls); + } +}; - /// Lists the public rooms on the server with optional filter. - /// - /// Lists the public rooms on the server, with optional filter. - /// - /// This API returns paginated responses. The rooms are ordered by the number - /// of joined members, with the largest rooms first. - class QueryPublicRoomsJob : public BaseJob +template <> +struct JsonObjectConverter<QueryPublicRoomsJob::Filter> { + static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod) { - public: - // Inner data structures - - /// Filter to apply to the results. - struct Filter - { - /// A string to search for in the room metadata, e.g. name, - /// topic, canonical alias etc. (Optional). - QString genericSearchTerm; - }; - - // Construction/destruction - - /*! Lists the public rooms on the server with optional filter. - * \param server - * The server to fetch the public room lists from. Defaults to the - * local server. - * \param limit - * Limit the number of results returned. - * \param since - * A pagination token from a previous request, allowing clients - * to get the next (or previous) batch of rooms. The direction - * of pagination is specified solely by which token is supplied, - * rather than via an explicit flag. - * \param filter - * Filter to apply to the results. - * \param includeAllNetworks - * Whether or not to include all known networks/protocols from - * application services on the homeserver. Defaults to false. - * \param thirdPartyInstanceId - * The specific third party network/protocol to request from the - * homeserver. Can only be used if ``include_all_networks`` is false. - */ - explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, bool includeAllNetworks = false, const QString& thirdPartyInstanceId = {}); - ~QueryPublicRoomsJob() override; - - // Result properties - - /// A list of the rooms on the server. - const PublicRoomsResponse& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), + pod.genericSearchTerm); + addParam<IfNotEmpty>(jo, QStringLiteral("room_types"), pod.roomTypes); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 4d15a30b..7bb74e29 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -4,124 +4,41 @@ #include "login.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct FromJsonObject<GetLoginFlowsJob::LoginFlow> - { - GetLoginFlowsJob::LoginFlow operator()(const QJsonObject& jo) const - { - GetLoginFlowsJob::LoginFlow result; - result.type = - fromJson<QString>(jo.value("type"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class GetLoginFlowsJob::Private -{ - public: - QVector<LoginFlow> flows; -}; +using namespace Quotient; QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/login"); + makePath("/_matrix/client/v3", "/login")); } -static const auto GetLoginFlowsJobName = QStringLiteral("GetLoginFlowsJob"); - GetLoginFlowsJob::GetLoginFlowsJob() - : BaseJob(HttpVerb::Get, GetLoginFlowsJobName, - basePath % "/login", false) - , d(new Private) -{ + : BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"), + makePath("/_matrix/client/v3", "/login"), false) +{} + +LoginJob::LoginJob(const QString& type, + const Omittable<UserIdentifier>& identifier, + const QString& password, const QString& token, + const QString& deviceId, + const QString& initialDeviceDisplayName, + Omittable<bool> refreshToken) + : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), + makePath("/_matrix/client/v3", "/login"), false) +{ + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("type"), type); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("identifier"), identifier); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("password"), password); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("token"), token); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_id"), deviceId); + addParam<IfNotEmpty>(_dataJson, + QStringLiteral("initial_device_display_name"), + initialDeviceDisplayName); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); + addExpectedKey("user_id"); + addExpectedKey("access_token"); + addExpectedKey("device_id"); } - -GetLoginFlowsJob::~GetLoginFlowsJob() = default; - -const QVector<GetLoginFlowsJob::LoginFlow>& GetLoginFlowsJob::flows() const -{ - return d->flows; -} - -BaseJob::Status GetLoginFlowsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->flows = fromJson<QVector<LoginFlow>>(json.value("flows"_ls)); - return Success; -} - -class LoginJob::Private -{ - public: - QString userId; - QString accessToken; - QString homeServer; - QString deviceId; -}; - -static const auto LoginJobName = QStringLiteral("LoginJob"); - -LoginJob::LoginJob(const QString& type, const Omittable<UserIdentifier>& identifier, const QString& password, const QString& token, const QString& deviceId, const QString& initialDeviceDisplayName, const QString& user, const QString& medium, const QString& address) - : BaseJob(HttpVerb::Post, LoginJobName, - basePath % "/login", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("type"), type); - addParam<IfNotEmpty>(_data, QStringLiteral("identifier"), identifier); - addParam<IfNotEmpty>(_data, QStringLiteral("password"), password); - addParam<IfNotEmpty>(_data, QStringLiteral("token"), token); - addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); - addParam<IfNotEmpty>(_data, QStringLiteral("user"), user); - addParam<IfNotEmpty>(_data, QStringLiteral("medium"), medium); - addParam<IfNotEmpty>(_data, QStringLiteral("address"), address); - setRequestData(_data); -} - -LoginJob::~LoginJob() = default; - -const QString& LoginJob::userId() const -{ - return d->userId; -} - -const QString& LoginJob::accessToken() const -{ - return d->accessToken; -} - -const QString& LoginJob::homeServer() const -{ - return d->homeServer; -} - -const QString& LoginJob::deviceId() const -{ - return d->deviceId; -} - -BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); - d->homeServer = fromJson<QString>(json.value("home_server"_ls)); - d->deviceId = fromJson<QString>(json.value("device_id"_ls)); - return Success; -} - diff --git a/lib/csapi/login.h b/lib/csapi/login.h index 957d8881..b9f14266 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -4,126 +4,167 @@ #pragma once -#include "jobs/basejob.h" - -#include <QtCore/QVector> #include "csapi/definitions/user_identifier.h" -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - /// 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. - 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 logging in. - struct LoginFlow - { - /// The login type. This is supplied as the ``type`` when - /// logging in. - QString type; - }; +#include "csapi/definitions/wellknown/full.h" - // Construction/destruction +#include "jobs/basejob.h" - explicit GetLoginFlowsJob(); +namespace Quotient { - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetLoginFlowsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); +/*! \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. + */ +class QUOTIENT_API 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 + /// logging in. + struct LoginFlow { + /// The login type. This is supplied as the `type` when + /// logging in. + QString type; + }; - ~GetLoginFlowsJob() override; + // Construction/destruction - // Result properties + /// Get the supported login types to authenticate users + explicit GetLoginFlowsJob(); - /// The homeserver's supported login types - const QVector<LoginFlow>& flows() const; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetLoginFlowsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - protected: - Status parseJson(const QJsonDocument& data) override; + // Result properties - private: - class Private; - QScopedPointer<Private> d; - }; + /// The homeserver's supported login types + QVector<LoginFlow> flows() const + { + return loadFromJson<QVector<LoginFlow>>("flows"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> { + static void fillFrom(const QJsonObject& jo, + GetLoginFlowsJob::LoginFlow& result) + { + fromJson(jo.value("type"_ls), result.type); + } +}; + +/*! \brief Authenticates the user. + * + * 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 + * auto-generate one. + * + * 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](/client-server-api/#relationship-between-access-tokens-and-devices). + */ +class QUOTIENT_API LoginJob : public BaseJob { +public: + /*! \brief Authenticates the user. + * + * \param type + * The login type being used. + * + * \param identifier + * 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 + * auto-generate one. + * + * 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](/client-server-api/#relationship-between-access-tokens-and-devices). + * + * \param password + * 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. + * + * \param deviceId + * ID of the client device. If this does not correspond to a + * 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. + * + * \param refreshToken + * If true, the client supports refresh tokens. + */ + explicit LoginJob(const QString& type, + const Omittable<UserIdentifier>& identifier = none, + const QString& password = {}, const QString& token = {}, + const QString& deviceId = {}, + const QString& initialDeviceDisplayName = {}, + Omittable<bool> refreshToken = none); + + // Result properties + + /// The fully-qualified Matrix ID for the account. + 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. + QString accessToken() const + { + return loadFromJson<QString>("access_token"_ls); + } - /// Authenticates the user. - /// - /// 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 - /// auto-generate one. - /// - /// 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`_. - class LoginJob : public BaseJob + /// A refresh token for the account. This token can be used to + /// obtain a new access token when it expires by calling the + /// `/refresh` endpoint. + QString refreshToken() const { - public: - /*! Authenticates the user. - * \param type - * The login type being used. - * \param identifier - * Identification information for the user. - * \param password - * 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. - * \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. - * \param initialDeviceDisplayName - * A display name to assign to the newly-created device. Ignored - * if ``device_id`` corresponds to a known device. - * \param user - * The fully qualified user ID or just local part of the user ID, to log in. Deprecated in favour of ``identifier``. - * \param medium - * When logging in using a third party identifier, the medium of the identifier. Must be 'email'. Deprecated in favour of ``identifier``. - * \param address - * Third party identifier for the user. Deprecated in favour of ``identifier``. - */ - explicit LoginJob(const QString& type, const Omittable<UserIdentifier>& identifier = none, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, const QString& user = {}, const QString& medium = {}, const QString& address = {}); - ~LoginJob() override; - - // Result properties - - /// The fully-qualified Matrix ID that has been registered. - const QString& userId() const; - /// An access token for the account. - /// This access token can then be used to authorize other requests. - const QString& accessToken() const; - /// The server_name of the homeserver on which the account has - /// 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. - const QString& homeServer() const; - /// ID of the logged-in device. Will be the same as the - /// corresponding parameter in the request, if one was specified. - const QString& deviceId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<QString>("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. Once + /// the access token has expired a new access token can be + /// obtained by using the provided refresh token. If no + /// refresh token is provided, the client will need to re-log in + /// to obtain a new access token. If not given, the client can + /// assume that the access token will not expire. + Omittable<int> expiresInMs() const + { + return loadFromJson<Omittable<int>>("expires_in_ms"_ls); + } + + /// ID of the logged-in device. Will be the same as the + /// corresponding parameter in the request, if one was specified. + QString deviceId() const { return loadFromJson<QString>("device_id"_ls); } + + /// Optional client configuration provided by the server. If present, + /// clients SHOULD use the provided object to reconfigure themselves, + /// optionally validating the URLs within. This object takes the same + /// form as the one returned from .well-known autodiscovery. + Omittable<DiscoveryInformation> wellKnown() const + { + return loadFromJson<Omittable<DiscoveryInformation>>("well_known"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp index 6e209e07..9ec54c71 100644 --- a/lib/csapi/logout.cpp +++ b/lib/csapi/logout.cpp @@ -4,39 +4,26 @@ #include "logout.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/logout"); + makePath("/_matrix/client/v3", "/logout")); } -static const auto LogoutJobName = QStringLiteral("LogoutJob"); - LogoutJob::LogoutJob() - : BaseJob(HttpVerb::Post, LogoutJobName, - basePath % "/logout") -{ -} + : BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"), + makePath("/_matrix/client/v3", "/logout")) +{} QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/logout/all"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/v3", "/logout/all")); } -static const auto LogoutAllJobName = QStringLiteral("LogoutAllJob"); - LogoutAllJob::LogoutAllJob() - : BaseJob(HttpVerb::Post, LogoutAllJobName, - basePath % "/logout/all") -{ -} - + : BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"), + makePath("/_matrix/client/v3", "/logout/all")) +{} diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h index 3ef3c656..3f1ac7fa 100644 --- a/lib/csapi/logout.h +++ b/lib/csapi/logout.h @@ -6,52 +6,55 @@ #include "jobs/basejob.h" - -namespace QMatrixClient -{ - // Operations - +namespace Quotient { + +/*! \brief Invalidates a user access token + * + * 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](/client-server-api/#device-keys) for the device are deleted + * alongside the device. + */ +class QUOTIENT_API LogoutJob : public BaseJob { +public: /// Invalidates a user access token - /// - /// Invalidates an existing access token, so that it can no longer be used for - /// authorization. - class LogoutJob : public BaseJob - { - public: - explicit LogoutJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * LogoutJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - }; - + explicit LogoutJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for LogoutJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); +}; + +/*! \brief Invalidates all access tokens for a user + * + * 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](/client-server-api/#device-keys) for the device are deleted alongside + * the device. + * + * 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 + * used in the request, and therefore the attacker is unable to take over the + * account in this way. + */ +class QUOTIENT_API LogoutAllJob : public BaseJob { +public: /// Invalidates all access tokens for a user - /// - /// 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. - /// - /// This endpoint does not require UI authorization because UI authorization 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 used in the request, and therefore the attacker is - /// unable to take over the account in this way. - class LogoutAllJob : public BaseJob - { - public: - explicit LogoutAllJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * LogoutAllJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - }; -} // namespace QMatrixClient + explicit LogoutAllJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for LogoutAllJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); +}; + +} // namespace Quotient diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index c59a51ab..0b2c99ce 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -4,26 +4,14 @@ #include "message_pagination.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetRoomEventsJob::Private -{ - public: - QString begin; - QString end; - RoomEvents chunk; -}; - -BaseJob::Query queryToGetRoomEvents(const QString& from, const QString& to, const QString& dir, Omittable<int> limit, const QString& filter) +auto queryToGetRoomEvents(const QString& from, const QString& to, + const QString& dir, Omittable<int> limit, + const QString& filter) { - BaseJob::Query _q; - addParam<>(_q, QStringLiteral("from"), from); + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); addParam<>(_q, QStringLiteral("dir"), dir); addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); @@ -31,46 +19,24 @@ BaseJob::Query queryToGetRoomEvents(const QString& from, const QString& to, cons return _q; } -QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& from, const QString& dir, const QString& to, Omittable<int> limit, const QString& filter) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/messages", - queryToGetRoomEvents(from, to, dir, limit, filter)); -} - -static const auto GetRoomEventsJobName = QStringLiteral("GetRoomEventsJob"); - -GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from, const QString& dir, const QString& to, Omittable<int> limit, const QString& filter) - : BaseJob(HttpVerb::Get, GetRoomEventsJobName, - basePath % "/rooms/" % roomId % "/messages", - queryToGetRoomEvents(from, to, dir, limit, filter)) - , d(new Private) -{ -} - -GetRoomEventsJob::~GetRoomEventsJob() = default; - -const QString& GetRoomEventsJob::begin() const -{ - return d->begin; -} - -const QString& GetRoomEventsJob::end() const +QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& dir, const QString& from, + const QString& to, Omittable<int> limit, + const QString& filter) { - return d->end; + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"), + queryToGetRoomEvents(from, to, dir, limit, filter)); } -RoomEvents&& GetRoomEventsJob::chunk() +GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& dir, + const QString& from, const QString& to, + Omittable<int> limit, const QString& filter) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"), + queryToGetRoomEvents(from, to, dir, limit, filter)) { - return std::move(d->chunk); + addExpectedKey("start"); + addExpectedKey("chunk"); } - -BaseJob::Status GetRoomEventsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls)); - return Success; -} - diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 12544f0c..b4f3a38a 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -4,70 +4,108 @@ #pragma once +#include "events/roomevent.h" #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Get a list of events for this room + * + * This API returns a list of message and state events for a room. It uses + * pagination query parameters to paginate history in the room. + * + * *Note*: This endpoint supports lazy-loading of room member events. See + * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) + * for more information. + */ +class QUOTIENT_API GetRoomEventsJob : public BaseJob { +public: + /*! \brief Get a list of events for this room + * + * \param roomId + * The room to get events from. + * + * \param dir + * The direction to return events from. If this is set to `f`, events + * will be returned in chronological order starting at `from`. If it + * is set to `b`, events will be returned in *reverse* chronological + * order, again starting at `from`. + * + * \param from + * The token to start returning events from. This token can be obtained + * from a `prev_batch` or `next_batch` token returned by the `/sync` + * endpoint, or from an `end` token returned by a previous request to this + * endpoint. + * + * This endpoint can also accept a value returned as a `start` token + * by a previous request to this endpoint, though servers are not + * required to support this. Clients should not rely on the behaviour. + * + * If it is not provided, the homeserver shall return a list of messages + * from the first or last (per the value of the `dir` parameter) visible + * event in the room history for the requesting user. + * + * \param to + * The token to stop returning events at. This token can be obtained from + * a `prev_batch` or `next_batch` token returned by the `/sync` endpoint, + * or from an `end` token returned by a previous request to this endpoint. + * + * \param limit + * The maximum number of events to return. Default: 10. + * + * \param filter + * A JSON RoomEventFilter to filter returned events with. + */ + explicit GetRoomEventsJob(const QString& roomId, const QString& dir, + const QString& from = {}, const QString& to = {}, + Omittable<int> limit = none, + const QString& filter = {}); - /// Get a list of events for this room - /// - /// This API returns a list of message and state events for a room. It uses - /// pagination query parameters to paginate history in the room. - class GetRoomEventsJob : public BaseJob - { - public: - /*! Get a list of events for this room - * \param roomId - * The room to get events from. - * \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 - * to this endpoint. - * \param dir - * The direction to return events from. - * \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 - * this endpoint. - * \param limit - * The maximum number of events to return. Default: 10. - * \param filter - * A JSON RoomEventFilter to filter returned events with. - */ - explicit GetRoomEventsJob(const QString& roomId, const QString& from, const QString& dir, const QString& to = {}, Omittable<int> limit = none, const QString& filter = {}); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomEventsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& dir, const QString& from = {}, + const QString& to = {}, + Omittable<int> limit = none, + const QString& filter = {}); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomEventsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& from, const QString& dir, const QString& to = {}, Omittable<int> limit = none, const QString& filter = {}); + // Result properties - ~GetRoomEventsJob() override; + /// A token corresponding to the start of `chunk`. This will be the same as + /// the value given in `from`. + QString begin() const { return loadFromJson<QString>("start"_ls); } - // Result properties + /// A token corresponding to the end of `chunk`. This token can be passed + /// back to this endpoint to request further events. + /// + /// If no further events are available (either because we have + /// reached the start of the timeline, or because the user does + /// not have permission to see any more events), this property + /// is omitted from the response. + QString end() const { return loadFromJson<QString>("end"_ls); } - /// The token the pagination starts from. If ``dir=b`` this will be - /// the token supplied in ``from``. - const QString& begin() const; - /// The token the pagination ends at. If ``dir=b`` this token should - /// be used again to request even earlier events. - const QString& end() const; - /// A list of room events. - RoomEvents&& chunk(); + /// 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. (The exact definition of + /// `chronological` is dependent on the server implementation.) + /// + /// Note that an empty `chunk` does not *necessarily* imply that no more + /// events are available. Clients should continue to paginate until no `end` + /// property is returned. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } - protected: - Status parseJson(const QJsonDocument& data) override; + /// 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 + /// 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. + RoomEvents state() { return takeFromJson<RoomEvents>("state"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index 785a0a8a..38aed174 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -4,94 +4,34 @@ #include "notifications.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient +auto queryToGetNotifications(const QString& from, Omittable<int> limit, + const QString& only) { - // Converters - - template <> struct FromJsonObject<GetNotificationsJob::Notification> - { - GetNotificationsJob::Notification operator()(const QJsonObject& jo) const - { - GetNotificationsJob::Notification result; - result.actions = - fromJson<QVector<QVariant>>(jo.value("actions"_ls)); - result.event = - fromJson<EventPtr>(jo.value("event"_ls)); - result.profileTag = - fromJson<QString>(jo.value("profile_tag"_ls)); - result.read = - fromJson<bool>(jo.value("read"_ls)); - result.roomId = - fromJson<QString>(jo.value("room_id"_ls)); - result.ts = - fromJson<int>(jo.value("ts"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class GetNotificationsJob::Private -{ - public: - QString nextToken; - std::vector<Notification> notifications; -}; - -BaseJob::Query 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); return _q; } -QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable<int> limit, const QString& only) +QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, + Omittable<int> limit, + const QString& only) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/notifications", - queryToGetNotifications(from, limit, only)); + makePath("/_matrix/client/v3", + "/notifications"), + queryToGetNotifications(from, limit, only)); } -static const auto GetNotificationsJobName = QStringLiteral("GetNotificationsJob"); - -GetNotificationsJob::GetNotificationsJob(const QString& from, Omittable<int> limit, const QString& only) - : BaseJob(HttpVerb::Get, GetNotificationsJobName, - basePath % "/notifications", - queryToGetNotifications(from, limit, only)) - , d(new Private) +GetNotificationsJob::GetNotificationsJob(const QString& from, + Omittable<int> limit, + const QString& only) + : BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"), + makePath("/_matrix/client/v3", "/notifications"), + queryToGetNotifications(from, limit, only)) { + addExpectedKey("notifications"); } - -GetNotificationsJob::~GetNotificationsJob() = default; - -const QString& GetNotificationsJob::nextToken() const -{ - return d->nextToken; -} - -std::vector<GetNotificationsJob::Notification>&& GetNotificationsJob::notifications() -{ - return std::move(d->notifications); -} - -BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->nextToken = fromJson<QString>(json.value("next_token"_ls)); - if (!json.contains("notifications"_ls)) - return { JsonParseError, - "The key 'notifications' not found in the response" }; - d->notifications = fromJson<std::vector<Notification>>(json.value("notifications"_ls)); - return Success; -} - diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 898b5154..ff8aa47f 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -4,86 +4,95 @@ #pragma once +#include "events/event.h" #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" -#include <QtCore/QVector> -#include <QtCore/QVariant> -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Gets a list of events that the user has been notified about + * + * This API is used to paginate through the list of events that the + * user has been, or would have been notified about. + */ +class QUOTIENT_API GetNotificationsJob : public BaseJob { +public: + // Inner data structures - /// Gets a list of events that the user has been notified about - /// /// This API is used to paginate through the list of events that the /// user has been, or would have been notified about. - class GetNotificationsJob : public BaseJob - { - public: - // Inner data structures - - /// This API is used to paginate through the list of events that the - /// 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`_. - QVector<QVariant> actions; - /// The Event object for the event that triggered the notification. - EventPtr event; - /// The profile tag of the rule that matched this event. - QString profileTag; - /// Indicates whether the user has sent a read receipt indicating - /// that they have read this message. - bool read; - /// The ID of the room in which the event was posted. - QString roomId; - /// The unix timestamp at which the event notification was sent, - /// in milliseconds. - int ts; - }; + struct Notification { + /// The action(s) to perform when the conditions for this rule are met. + /// 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; + /// The profile tag of the rule that matched this event. + QString profileTag; + /// Indicates whether the user has sent a read receipt indicating + /// that they have read this message. + bool read; + /// The ID of the room in which the event was posted. + QString roomId; + /// The unix timestamp at which the event notification was sent, + /// in milliseconds. + qint64 ts; + }; - // Construction/destruction + // Construction/destruction - /*! Gets a list of events that the user has been notified about - * \param from - * Pagination token given to retrieve the next set of events. - * \param limit - * Limit on the number of events to return in this request. - * \param only - * Allows basic filtering of events returned. Supply ``highlight`` - * to return only events where the notification had the highlight - * tweak set. - */ - explicit GetNotificationsJob(const QString& from = {}, Omittable<int> limit = none, const QString& only = {}); + /*! \brief Gets a list of events that the user has been notified about + * + * \param from + * Pagination token to continue from. This should be the `next_token` + * returned from an earlier call to this endpoint. + * + * \param limit + * Limit on the number of events to return in this request. + * + * \param only + * Allows basic filtering of events returned. Supply `highlight` + * to return only events where the notification had the highlight + * tweak set. + */ + explicit GetNotificationsJob(const QString& from = {}, + Omittable<int> limit = none, + const QString& only = {}); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetNotificationsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, Omittable<int> limit = none, const QString& only = {}); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetNotificationsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, + Omittable<int> limit = none, + const QString& only = {}); - ~GetNotificationsJob() override; + // Result properties - // Result properties + /// 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); } - /// 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. - const QString& nextToken() const; - /// The list of events that triggered notifications. - std::vector<Notification>&& notifications(); + /// The list of events that triggered notifications. + std::vector<Notification> notifications() + { + return takeFromJson<std::vector<Notification>>("notifications"_ls); + } +}; - protected: - Status parseJson(const QJsonDocument& data) override; +template <> +struct JsonObjectConverter<GetNotificationsJob::Notification> { + static void fillFrom(const QJsonObject& jo, + GetNotificationsJob::Notification& result) + { + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("event"_ls), result.event); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("read"_ls), result.read); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("ts"_ls), result.ts); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 2547f0c8..7e89b8a6 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -4,74 +4,13 @@ #include "openid.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class RequestOpenIdTokenJob::Private -{ - public: - QString accessToken; - QString tokenType; - QString matrixServerName; - int expiresIn; -}; - -static const auto RequestOpenIdTokenJobName = QStringLiteral("RequestOpenIdTokenJob"); - -RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body) - : BaseJob(HttpVerb::Post, RequestOpenIdTokenJobName, - basePath % "/user/" % userId % "/openid/request_token") - , d(new Private) +RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, + const QJsonObject& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"), + makePath("/_matrix/client/v3", "/user/", userId, + "/openid/request_token")) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); } - -RequestOpenIdTokenJob::~RequestOpenIdTokenJob() = default; - -const QString& RequestOpenIdTokenJob::accessToken() const -{ - return d->accessToken; -} - -const QString& RequestOpenIdTokenJob::tokenType() const -{ - return d->tokenType; -} - -const QString& RequestOpenIdTokenJob::matrixServerName() const -{ - return d->matrixServerName; -} - -int RequestOpenIdTokenJob::expiresIn() const -{ - return d->expiresIn; -} - -BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("access_token"_ls)) - return { JsonParseError, - "The key 'access_token' not found in the response" }; - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); - if (!json.contains("token_type"_ls)) - return { JsonParseError, - "The key 'token_type' not found in the response" }; - d->tokenType = fromJson<QString>(json.value("token_type"_ls)); - if (!json.contains("matrix_server_name"_ls)) - return { JsonParseError, - "The key 'matrix_server_name' not found in the response" }; - d->matrixServerName = fromJson<QString>(json.value("matrix_server_name"_ls)); - if (!json.contains("expires_in"_ls)) - return { JsonParseError, - "The key 'expires_in' not found in the response" }; - d->expiresIn = fromJson<int>(json.value("expires_in"_ls)); - return Success; -} - diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 807801fb..b3f72a25 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -4,58 +4,49 @@ #pragma once +#include "csapi/definitions/openid_token.h" + #include "jobs/basejob.h" -#include "converters.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations - - /// Get an OpenID token object to verify the requester's identity. - /// - /// Gets an OpenID token object that the requester may supply to another - /// service to verify their identity in Matrix. The generated token is only - /// valid for exchanging for user information from the federation API for - /// 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 - /// example. - class RequestOpenIdTokenJob : public BaseJob +namespace Quotient { + +/*! \brief Get an OpenID token object to verify the requester's identity. + * + * Gets an OpenID token object that the requester may supply to another + * service to verify their identity in Matrix. The generated token is only + * valid for exchanging for user information from the federation API for + * 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 + * example. + */ +class QUOTIENT_API RequestOpenIdTokenJob : public BaseJob { +public: + /*! \brief Get an OpenID token object to verify the requester's identity. + * + * \param userId + * The user to request and OpenID token for. Should be the user who + * is authenticated for the request. + * + * \param body + * An empty object. Reserved for future expansion. + */ + explicit RequestOpenIdTokenJob(const QString& userId, + const QJsonObject& body = {}); + + // 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, + /// the Matrix homeserver's name is provided. + OpenIdCredentials tokenData() const { - public: - /*! Get an OpenID token object to verify the requester's identity. - * \param userId - * The user to request and OpenID token for. Should be the user who - * is authenticated for the request. - * \param body - * An empty object. Reserved for future expansion. - */ - explicit RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body = {}); - ~RequestOpenIdTokenJob() override; - - // Result properties - - /// 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``. - const QString& accessToken() const; - /// The string ``Bearer``. - const QString& tokenType() const; - /// The homeserver domain the consumer should use when attempting to - /// verify the user's identity. - const QString& matrixServerName() const; - /// The number of seconds before this token expires and a new one must - /// be generated. - int expiresIn() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return fromJson<OpenIdCredentials>(jsonData()); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index e046a62e..9dd1445e 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -4,71 +4,29 @@ #include "peeking_events.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class PeekEventsJob::Private +auto queryToPeekEvents(const QString& from, Omittable<int> timeout, + const QString& roomId) { - public: - QString begin; - QString end; - RoomEvents chunk; -}; - -BaseJob::Query 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); return _q; } -QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable<int> timeout, const QString& roomId) +QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, + Omittable<int> timeout, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/events", - queryToPeekEvents(from, timeout, roomId)); -} - -static const auto PeekEventsJobName = QStringLiteral("PeekEventsJob"); - -PeekEventsJob::PeekEventsJob(const QString& from, Omittable<int> timeout, const QString& roomId) - : BaseJob(HttpVerb::Get, PeekEventsJobName, - basePath % "/events", - queryToPeekEvents(from, timeout, roomId)) - , d(new Private) -{ -} - -PeekEventsJob::~PeekEventsJob() = default; - -const QString& PeekEventsJob::begin() const -{ - return d->begin; -} - -const QString& PeekEventsJob::end() const -{ - return d->end; -} - -RoomEvents&& PeekEventsJob::chunk() -{ - return std::move(d->chunk); -} - -BaseJob::Status PeekEventsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls)); - return Success; + makePath("/_matrix/client/v3", "/events"), + queryToPeekEvents(from, timeout, roomId)); } +PeekEventsJob::PeekEventsJob(const QString& from, Omittable<int> timeout, + const QString& roomId) + : BaseJob(HttpVerb::Get, QStringLiteral("PeekEventsJob"), + makePath("/_matrix/client/v3", "/events"), + queryToPeekEvents(from, timeout, roomId)) +{} diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index 5a6e513c..a67d2e4a 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -4,67 +4,63 @@ #pragma once +#include "events/roomevent.h" #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Listen on the event stream. - /// - /// 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. - /// - /// 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 - /// API will also be deprecated at some point, but its replacement is not - /// yet known. - class PeekEventsJob : public BaseJob - { - public: - /*! Listen on the event stream. - * \param from - * The token to stream from. This token is either from a previous - * request to this API or from the initial sync API. - * \param timeout - * The maximum time in milliseconds to wait for an event. - * \param roomId - * The room ID for which events should be returned. - */ - explicit PeekEventsJob(const QString& from = {}, Omittable<int> timeout = none, const QString& roomId = {}); +/*! \brief Listen on the event stream of a particular room. + * + * 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. + * + * 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 + * API will also be deprecated at some point, but its replacement is not + * yet known. + */ +class QUOTIENT_API PeekEventsJob : public BaseJob { +public: + /*! \brief Listen on the event stream of a particular room. + * + * \param from + * The token to stream from. This token is either from a previous + * request to this API or from the initial sync API. + * + * \param timeout + * The maximum time in milliseconds to wait for an event. + * + * \param roomId + * The room ID for which events should be returned. + */ + explicit PeekEventsJob(const QString& from = {}, + Omittable<int> timeout = none, + const QString& roomId = {}); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * PeekEventsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, Omittable<int> timeout = none, const QString& roomId = {}); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for PeekEventsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, + Omittable<int> timeout = none, + const QString& roomId = {}); - ~PeekEventsJob() override; + // Result properties - // Result properties + /// 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 first value in ``chunk``. This - /// is usually the same token supplied to ``from=``. - const QString& begin() const; - /// A token which correlates to the last value in ``chunk``. This - /// token should be used in the next request to ``/events``. - const QString& end() const; - /// An array of events. - RoomEvents&& chunk(); + /// 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); } - protected: - Status parseJson(const QJsonDocument& data) override; + /// An array of events. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 7aba8b61..828ccfb7 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -4,128 +4,29 @@ #include "presence.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetPresenceJobName = QStringLiteral("SetPresenceJob"); - -SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg) - : BaseJob(HttpVerb::Put, SetPresenceJobName, - basePath % "/presence/" % userId % "/status") +SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, + const QString& statusMsg) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"), + makePath("/_matrix/client/v3", "/presence/", userId, "/status")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("presence"), presence); - addParam<IfNotEmpty>(_data, QStringLiteral("status_msg"), statusMsg); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("presence"), presence); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("status_msg"), statusMsg); + setRequestData({ _dataJson }); } -class GetPresenceJob::Private -{ - public: - QString presence; - Omittable<int> lastActiveAgo; - QString statusMsg; - bool currentlyActive; -}; - QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/presence/" % userId % "/status"); + makePath("/_matrix/client/v3", "/presence/", + userId, "/status")); } -static const auto GetPresenceJobName = QStringLiteral("GetPresenceJob"); - GetPresenceJob::GetPresenceJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetPresenceJobName, - basePath % "/presence/" % userId % "/status") - , d(new Private) -{ -} - -GetPresenceJob::~GetPresenceJob() = default; - -const QString& GetPresenceJob::presence() const -{ - return d->presence; -} - -Omittable<int> GetPresenceJob::lastActiveAgo() const -{ - return d->lastActiveAgo; -} - -const QString& GetPresenceJob::statusMsg() const -{ - return d->statusMsg; -} - -bool GetPresenceJob::currentlyActive() const -{ - return d->currentlyActive; -} - -BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"), + makePath("/_matrix/client/v3", "/presence/", userId, "/status")) { - auto json = data.object(); - if (!json.contains("presence"_ls)) - return { JsonParseError, - "The key 'presence' not found in the response" }; - d->presence = fromJson<QString>(json.value("presence"_ls)); - d->lastActiveAgo = fromJson<int>(json.value("last_active_ago"_ls)); - d->statusMsg = fromJson<QString>(json.value("status_msg"_ls)); - d->currentlyActive = fromJson<bool>(json.value("currently_active"_ls)); - return Success; + addExpectedKey("presence"); } - -static const auto ModifyPresenceListJobName = QStringLiteral("ModifyPresenceListJob"); - -ModifyPresenceListJob::ModifyPresenceListJob(const QString& userId, const QStringList& invite, const QStringList& drop) - : BaseJob(HttpVerb::Post, ModifyPresenceListJobName, - basePath % "/presence/list/" % userId) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite); - addParam<IfNotEmpty>(_data, QStringLiteral("drop"), drop); - setRequestData(_data); -} - -class GetPresenceForListJob::Private -{ - public: - Events data; -}; - -QUrl GetPresenceForListJob::makeRequestUrl(QUrl baseUrl, const QString& userId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/presence/list/" % userId); -} - -static const auto GetPresenceForListJobName = QStringLiteral("GetPresenceForListJob"); - -GetPresenceForListJob::GetPresenceForListJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetPresenceForListJobName, - basePath % "/presence/list/" % userId, false) - , d(new Private) -{ -} - -GetPresenceForListJob::~GetPresenceForListJob() = default; - -Events&& GetPresenceForListJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetPresenceForListJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Events>(data); - return Success; -} - diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 86b9d395..52445205 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -6,124 +6,72 @@ #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - /// Update this user's presence state. - /// - /// 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 - /// presence state of another user. - class SetPresenceJob : public BaseJob - { - public: - /*! Update this user's presence state. - * \param userId - * The user whose presence state to update. - * \param presence - * The new presence state. - * \param statusMsg - * The status message to attach to this state. - */ - explicit SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg = {}); - }; - - /// Get this user's presence state. - /// - /// Get the given user's presence state. - class GetPresenceJob : public BaseJob +namespace Quotient { + +/*! \brief Update this user's presence state. + * + * 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 + * presence state of another user. + */ +class QUOTIENT_API SetPresenceJob : public BaseJob { +public: + /*! \brief Update this user's presence state. + * + * \param userId + * The user whose presence state to update. + * + * \param presence + * The new presence state. + * + * \param statusMsg + * The status message to attach to this state. + */ + explicit SetPresenceJob(const QString& userId, const QString& presence, + const QString& statusMsg = {}); +}; + +/*! \brief Get this user's presence state. + * + * Get the given user's presence state. + */ +class QUOTIENT_API GetPresenceJob : public BaseJob { +public: + /*! \brief Get this user's presence state. + * + * \param userId + * The user whose presence state to get. + */ + explicit GetPresenceJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPresenceJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// This user's presence. + QString presence() const { return loadFromJson<QString>("presence"_ls); } + + /// The length of time in milliseconds since an action was performed + /// by this user. + Omittable<int> lastActiveAgo() const { - public: - /*! Get this user's presence state. - * \param userId - * The user whose presence state to get. - */ - explicit GetPresenceJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPresenceJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetPresenceJob() override; - - // Result properties + return loadFromJson<Omittable<int>>("last_active_ago"_ls); + } - /// This user's presence. - const QString& presence() const; - /// The length of time in milliseconds since an action was performed - /// by this user. - Omittable<int> lastActiveAgo() const; - /// The state message for this user if one was set. - const QString& statusMsg() const; - /// Whether the user is currently active - bool currentlyActive() const; + /// The state message for this user if one was set. + QString statusMsg() const { return loadFromJson<QString>("status_msg"_ls); } - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Add or remove users from this presence list. - /// - /// Adds or removes users from this presence list. - class ModifyPresenceListJob : public BaseJob + /// Whether the user is currently active + Omittable<bool> currentlyActive() const { - public: - /*! Add or remove users from this presence list. - * \param userId - * The user whose presence list is being modified. - * \param invite - * A list of user IDs to add to the list. - * \param drop - * A list of user IDs to remove from the list. - */ - explicit ModifyPresenceListJob(const QString& userId, const QStringList& invite = {}, const QStringList& drop = {}); - }; - - /// Get presence events for this presence list. - /// - /// Retrieve a list of presence events for every user on this list. - class GetPresenceForListJob : public BaseJob - { - public: - /*! Get presence events for this presence list. - * \param userId - * The user whose presence list should be retrieved. - */ - explicit GetPresenceForListJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPresenceForListJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetPresenceForListJob() override; - - // Result properties - - /// A list of presence events for this list. - Events&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; + return loadFromJson<Omittable<bool>>("currently_active"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index bb053062..f024ed82 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -4,145 +4,63 @@ #include "profile.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetDisplayNameJobName = QStringLiteral("SetDisplayNameJob"); - -SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) - : BaseJob(HttpVerb::Put, SetDisplayNameJobName, - basePath % "/profile/" % userId % "/displayname") +SetDisplayNameJob::SetDisplayNameJob(const QString& userId, + const QString& displayname) + : BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"), + makePath("/_matrix/client/v3", "/profile/", userId, + "/displayname")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("displayname"), displayname); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("displayname"), displayname); + setRequestData({ _dataJson }); } -class GetDisplayNameJob::Private -{ - public: - QString displayname; -}; - QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/profile/" % userId % "/displayname"); + makePath("/_matrix/client/v3", "/profile/", + userId, "/displayname")); } -static const auto GetDisplayNameJobName = QStringLiteral("GetDisplayNameJob"); - GetDisplayNameJob::GetDisplayNameJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetDisplayNameJobName, - basePath % "/profile/" % userId % "/displayname", false) - , d(new Private) -{ -} - -GetDisplayNameJob::~GetDisplayNameJob() = default; - -const QString& GetDisplayNameJob::displayname() const -{ - return d->displayname; + : BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"), + makePath("/_matrix/client/v3", "/profile/", userId, + "/displayname"), + false) +{} + +SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) + : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), + makePath("/_matrix/client/v3", "/profile/", userId, "/avatar_url")) +{ + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("avatar_url"), avatarUrl); + setRequestData({ _dataJson }); } -BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->displayname = fromJson<QString>(json.value("displayname"_ls)); - return Success; -} - -static const auto SetAvatarUrlJobName = QStringLiteral("SetAvatarUrlJob"); - -SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl) - : BaseJob(HttpVerb::Put, SetAvatarUrlJobName, - basePath % "/profile/" % userId % "/avatar_url") -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("avatar_url"), avatarUrl); - setRequestData(_data); -} - -class GetAvatarUrlJob::Private -{ - public: - QString avatarUrl; -}; - QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/profile/" % userId % "/avatar_url"); + makePath("/_matrix/client/v3", "/profile/", + userId, "/avatar_url")); } -static const auto GetAvatarUrlJobName = QStringLiteral("GetAvatarUrlJob"); - GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetAvatarUrlJobName, - basePath % "/profile/" % userId % "/avatar_url", false) - , d(new Private) -{ -} - -GetAvatarUrlJob::~GetAvatarUrlJob() = default; - -const QString& GetAvatarUrlJob::avatarUrl() const -{ - return d->avatarUrl; -} - -BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls)); - return Success; -} - -class GetUserProfileJob::Private -{ - public: - QString avatarUrl; - QString displayname; -}; + : BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"), + makePath("/_matrix/client/v3", "/profile/", userId, "/avatar_url"), + false) +{} QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/profile/" % userId); + makePath("/_matrix/client/v3", "/profile/", + userId)); } -static const auto GetUserProfileJobName = QStringLiteral("GetUserProfileJob"); - GetUserProfileJob::GetUserProfileJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetUserProfileJobName, - basePath % "/profile/" % userId, false) - , d(new Private) -{ -} - -GetUserProfileJob::~GetUserProfileJob() = default; - -const QString& GetUserProfileJob::avatarUrl() const -{ - return d->avatarUrl; -} - -const QString& GetUserProfileJob::displayname() const -{ - return d->displayname; -} - -BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls)); - d->displayname = fromJson<QString>(json.value("displayname"_ls)); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"), + makePath("/_matrix/client/v3", "/profile/", userId), false) +{} diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 23094aff..b00c944b 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -6,154 +6,137 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// 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``. - class SetDisplayNameJob : public BaseJob - { - public: - /*! Set the user's display name. - * \param userId - * The user whose display name to set. - * \param displayname - * The new display name for this user. - */ - explicit SetDisplayNameJob(const QString& userId, const QString& displayname = {}); - }; - - /// Get the user's display name. - /// - /// Get the user's display name. This API may be used to fetch the user's - /// own displayname or to query the name of other users; either locally or - /// on remote homeservers. - class GetDisplayNameJob : public BaseJob - { - public: - /*! Get the user's display name. - * \param userId - * The user whose display name to get. - */ - explicit GetDisplayNameJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetDisplayNameJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetDisplayNameJob() override; - - // Result properties - - /// The user's display name if they have set one, otherwise not present. - const QString& displayname() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// 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``. - class SetAvatarUrlJob : public BaseJob - { - public: - /*! Set the user's avatar URL. - * \param userId - * The user whose avatar URL to set. - * \param avatarUrl - * The new avatar URL for this user. - */ - explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl = {}); - }; - - /// Get the user's avatar URL. - /// - /// Get the user's avatar URL. This API may be used to fetch the user's - /// own avatar URL or to query the URL of other users; either locally or - /// on remote homeservers. - class GetAvatarUrlJob : public BaseJob +/*! \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`. + */ +class QUOTIENT_API SetDisplayNameJob : public BaseJob { +public: + /*! \brief Set the user's display name. + * + * \param userId + * The user whose display name to set. + * + * \param displayname + * The new display name for this user. + */ + explicit SetDisplayNameJob(const QString& userId, + const QString& displayname); +}; + +/*! \brief Get the user's display name. + * + * Get the user's display name. This API may be used to fetch the user's + * own displayname or to query the name of other users; either locally or + * on remote homeservers. + */ +class QUOTIENT_API GetDisplayNameJob : public BaseJob { +public: + /*! \brief Get the user's display name. + * + * \param userId + * The user whose display name to get. + */ + explicit GetDisplayNameJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetDisplayNameJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The user's display name if they have set one, otherwise not present. + QString displayname() const { - public: - /*! Get the user's avatar URL. - * \param userId - * The user whose avatar URL to get. - */ - explicit GetAvatarUrlJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetAvatarUrlJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetAvatarUrlJob() override; - - // Result properties - - /// The user's avatar URL if they have set one, otherwise not present. - const QString& avatarUrl() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get this user's profile information. - /// - /// 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``. - class GetUserProfileJob : public BaseJob + return loadFromJson<QString>("displayname"_ls); + } +}; + +/*! \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`. + */ +class QUOTIENT_API SetAvatarUrlJob : public BaseJob { +public: + /*! \brief Set the user's avatar URL. + * + * \param userId + * The user whose avatar URL to set. + * + * \param avatarUrl + * The new avatar URL for this user. + */ + explicit SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl); +}; + +/*! \brief Get the user's avatar URL. + * + * Get the user's avatar URL. This API may be used to fetch the user's + * own avatar URL or to query the URL of other users; either locally or + * on remote homeservers. + */ +class QUOTIENT_API GetAvatarUrlJob : public BaseJob { +public: + /*! \brief Get the user's avatar URL. + * + * \param userId + * The user whose avatar URL to get. + */ + explicit GetAvatarUrlJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAvatarUrlJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The user's avatar URL if they have set one, otherwise not present. + QUrl avatarUrl() const { return loadFromJson<QUrl>("avatar_url"_ls); } +}; + +/*! \brief Get this user's profile information. + * + * 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`. + */ +class QUOTIENT_API GetUserProfileJob : public BaseJob { +public: + /*! \brief Get this user's profile information. + * + * \param userId + * The user whose profile information to get. + */ + explicit GetUserProfileJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetUserProfileJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The user's avatar URL if they have set one, otherwise not present. + 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 { - public: - /*! Get this user's profile information. - * \param userId - * The user whose profile information to get. - */ - explicit GetUserProfileJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetUserProfileJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetUserProfileJob() override; - - // Result properties - - /// The user's avatar URL if they have set one, otherwise not present. - const QString& avatarUrl() const; - /// The user's display name if they have set one, otherwise not present. - const QString& displayname() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<QString>("displayname"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index d20db88a..fb6595fc 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -4,123 +4,37 @@ #include "pusher.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct FromJsonObject<GetPushersJob::PusherData> - { - GetPushersJob::PusherData operator()(const QJsonObject& jo) const - { - GetPushersJob::PusherData result; - result.url = - fromJson<QString>(jo.value("url"_ls)); - result.format = - fromJson<QString>(jo.value("format"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<GetPushersJob::Pusher> - { - GetPushersJob::Pusher operator()(const QJsonObject& jo) const - { - GetPushersJob::Pusher result; - result.pushkey = - fromJson<QString>(jo.value("pushkey"_ls)); - result.kind = - fromJson<QString>(jo.value("kind"_ls)); - result.appId = - fromJson<QString>(jo.value("app_id"_ls)); - result.appDisplayName = - fromJson<QString>(jo.value("app_display_name"_ls)); - result.deviceDisplayName = - fromJson<QString>(jo.value("device_display_name"_ls)); - result.profileTag = - fromJson<QString>(jo.value("profile_tag"_ls)); - result.lang = - fromJson<QString>(jo.value("lang"_ls)); - result.data = - fromJson<GetPushersJob::PusherData>(jo.value("data"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class GetPushersJob::Private -{ - public: - QVector<Pusher> pushers; -}; +using namespace Quotient; QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushers"); + makePath("/_matrix/client/v3", "/pushers")); } -static const auto GetPushersJobName = QStringLiteral("GetPushersJob"); - GetPushersJob::GetPushersJob() - : BaseJob(HttpVerb::Get, GetPushersJobName, - basePath % "/pushers") - , d(new Private) -{ -} - -GetPushersJob::~GetPushersJob() = default; - -const QVector<GetPushersJob::Pusher>& GetPushersJob::pushers() const -{ - return d->pushers; -} - -BaseJob::Status GetPushersJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->pushers = fromJson<QVector<Pusher>>(json.value("pushers"_ls)); - return Success; -} - -namespace QMatrixClient -{ - // Converters - - QJsonObject toJson(const PostPusherJob::PusherData& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); - addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); - return jo; - } -} // namespace QMatrixClient - -static const auto PostPusherJobName = QStringLiteral("PostPusherJob"); - -PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, bool append) - : BaseJob(HttpVerb::Post, PostPusherJobName, - basePath % "/pushers/set") -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("pushkey"), pushkey); - addParam<>(_data, QStringLiteral("kind"), kind); - addParam<>(_data, QStringLiteral("app_id"), appId); - addParam<>(_data, QStringLiteral("app_display_name"), appDisplayName); - addParam<>(_data, QStringLiteral("device_display_name"), deviceDisplayName); - addParam<IfNotEmpty>(_data, QStringLiteral("profile_tag"), profileTag); - addParam<>(_data, QStringLiteral("lang"), lang); - addParam<>(_data, QStringLiteral("data"), data); - addParam<IfNotEmpty>(_data, QStringLiteral("append"), append); - setRequestData(_data); + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"), + makePath("/_matrix/client/v3", "/pushers")) +{} + +PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, + const QString& appId, const QString& appDisplayName, + const QString& deviceDisplayName, + const QString& lang, const PusherData& data, + const QString& profileTag, Omittable<bool> append) + : BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"), + makePath("/_matrix/client/v3", "/pushers/set")) +{ + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("pushkey"), pushkey); + addParam<>(_dataJson, QStringLiteral("kind"), kind); + addParam<>(_dataJson, QStringLiteral("app_id"), appId); + addParam<>(_dataJson, QStringLiteral("app_display_name"), appDisplayName); + addParam<>(_dataJson, QStringLiteral("device_display_name"), + deviceDisplayName); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("profile_tag"), profileTag); + addParam<>(_dataJson, QStringLiteral("lang"), lang); + addParam<>(_dataJson, QStringLiteral("data"), data); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("append"), append); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 2b506183..d859ffc4 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -6,164 +6,199 @@ #include "jobs/basejob.h" -#include <QtCore/QVector> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Gets the current pushers for the authenticated user + * + * Gets all currently active pushers for the authenticated user. + */ +class QUOTIENT_API GetPushersJob : public BaseJob { +public: + // Inner data structures + + /// A dictionary of information for the pusher implementation + /// itself. + struct PusherData { + /// Required if `kind` is `http`. The URL to use to send + /// notifications to. + QUrl url; + /// The format to use when sending notifications to the Push + /// Gateway. + QString format; + }; - /// Gets the current pushers for the authenticated user - /// /// Gets all currently active pushers for the authenticated user. - class GetPushersJob : public BaseJob - { - public: - // Inner data structures - - /// A dictionary of information for the pusher implementation - /// itself. - struct PusherData - { - /// Required if ``kind`` is ``http``. The URL to use to send - /// notifications to. - QString url; - /// The format to use when sending notifications to the Push - /// Gateway. - QString format; - }; - - /// Gets all currently active pushers for the authenticated user. - struct Pusher - { - /// 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 - /// sends HTTP pokes. - QString kind; - /// This is a reverse-DNS style identifier for the application. - /// Max length, 64 chars. - QString appId; - /// A string that will allow the user to identify what application - /// owns this pusher. - QString appDisplayName; - /// A string that will allow the user to identify what device owns - /// this pusher. - QString deviceDisplayName; - /// This string determines which set of device specific rules this - /// pusher executes. - QString profileTag; - /// The preferred language for receiving notifications (e.g. 'en' - /// or 'en-US') - QString lang; - /// A dictionary of information for the pusher implementation - /// itself. - PusherData data; - }; - - // Construction/destruction - - explicit GetPushersJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushersJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetPushersJob() override; - - // Result properties - - /// An array containing the current pushers for the user - const QVector<Pusher>& pushers() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct Pusher { + /// 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 + /// sends HTTP pokes. + QString kind; + /// This is a reverse-DNS style identifier for the application. + /// Max length, 64 chars. + QString appId; + /// A string that will allow the user to identify what application + /// owns this pusher. + QString appDisplayName; + /// A string that will allow the user to identify what device owns + /// this pusher. + QString deviceDisplayName; + /// This string determines which set of device specific rules this + /// pusher executes. + QString profileTag; + /// The preferred language for receiving notifications (e.g. 'en' + /// or 'en-US') + QString lang; + /// A dictionary of information for the pusher implementation + /// itself. + PusherData data; }; - /// 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. - class PostPusherJob : public BaseJob + // Construction/destruction + + /// Gets the current pushers for the authenticated user + explicit GetPushersJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushersJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// An array containing the current pushers for the user + QVector<Pusher> pushers() const + { + return loadFromJson<QVector<Pusher>>("pushers"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetPushersJob::PusherData> { + static void fillFrom(const QJsonObject& jo, + GetPushersJob::PusherData& result) { - public: - // Inner data structures - - /// A dictionary of information for the pusher implementation - /// 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 - /// notifications to. MUST be an HTTPS URL with a path of - /// ``/_matrix/push/v1/notify``. - QString url; - /// The format to send notifications in to Push Gateways if 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'. - QString format; - }; - - // Construction/destruction - - /*! Modify a pusher for this user on the homeserver. - * \param pushkey - * This is a unique identifier for this pusher. The value you - * should use for this is the routing or destination address - * information for the notification, for example, the APNS token - * for APNS or the Registration ID for GCM. If your notification - * client has no such concept, use any unique identifier. - * Max length, 512 bytes. - * - * 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. - * \param appId - * This is a reverse-DNS style identifier for the application. - * It is recommended that this end with the platform, such that - * different platform versions get different app identifiers. - * Max length, 64 chars. - * - * If the ``kind`` is ``"email"``, this is ``"m.email"``. - * \param appDisplayName - * A string that will allow the user to identify what application - * owns this pusher. - * \param deviceDisplayName - * A string that will allow the user to identify what device owns - * this pusher. - * \param lang - * The preferred language for receiving notifications (e.g. 'en' - * or 'en-US'). - * \param data - * A dictionary of information for the pusher implementation - * itself. If ``kind`` is ``http``, this should contain ``url`` - * which is the URL to use to send notifications to. - * \param profileTag - * This string determines which set of device specific rules this - * pusher executes. - * \param append - * If true, the homeserver should add another pusher with the - * 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``. - */ - explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, bool append = false); + fromJson(jo.value("url"_ls), result.url); + fromJson(jo.value("format"_ls), result.format); + } +}; + +template <> +struct JsonObjectConverter<GetPushersJob::Pusher> { + static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result) + { + fromJson(jo.value("pushkey"_ls), result.pushkey); + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("app_id"_ls), result.appId); + fromJson(jo.value("app_display_name"_ls), result.appDisplayName); + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("lang"_ls), result.lang); + fromJson(jo.value("data"_ls), result.data); + } +}; + +/*! \brief Modify a pusher for this user on the homeserver. + * + * 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 QUOTIENT_API PostPusherJob : public BaseJob { +public: + // Inner data structures + + /// A dictionary of information for the pusher implementation + /// 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 + /// notifications to. MUST be an HTTPS URL with a path of + /// `/_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 + /// homeserver should send to the push gateway are defined in the + /// [Push Gateway Specification](/push-gateway-api/). Currently the only + /// format available is 'event_id_only'. + QString format; }; -} // namespace QMatrixClient + + // Construction/destruction + + /*! \brief Modify a pusher for this user on the homeserver. + * + * \param pushkey + * This is a unique identifier for this pusher. The value you + * should use for this is the routing or destination address + * information for the notification, for example, the APNS token + * for APNS or the Registration ID for GCM. If your notification + * client has no such concept, use any unique identifier. + * Max length, 512 bytes. + * + * 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. + * + * \param appId + * This is a reverse-DNS style identifier for the application. + * It is recommended that this end with the platform, such that + * different platform versions get different app identifiers. + * Max length, 64 chars. + * + * If the `kind` is `"email"`, this is `"m.email"`. + * + * \param appDisplayName + * A string that will allow the user to identify what application + * owns this pusher. + * + * \param deviceDisplayName + * A string that will allow the user to identify what device owns + * this pusher. + * + * \param lang + * The preferred language for receiving notifications (e.g. 'en' + * or 'en-US'). + * + * \param data + * A dictionary of information for the pusher implementation + * itself. If `kind` is `http`, this should contain `url` + * which is the URL to use to send notifications to. + * + * \param profileTag + * This string determines which set of device specific rules this + * pusher executes. + * + * \param append + * If true, the homeserver should add another pusher with the + * 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`. + */ + explicit PostPusherJob(const QString& pushkey, const QString& kind, + const QString& appId, const QString& appDisplayName, + const QString& deviceDisplayName, + const QString& lang, const PusherData& data, + const QString& profileTag = {}, + Omittable<bool> append = none); +}; + +template <> +struct JsonObjectConverter<PostPusherJob::PusherData> { + static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); + addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index ea8ad02a..2376654a 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -4,217 +4,139 @@ #include "pushrules.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetPushRulesJob::Private -{ - public: - PushRuleset global; -}; +using namespace Quotient; QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/v3", "/pushrules")); } -static const auto GetPushRulesJobName = QStringLiteral("GetPushRulesJob"); - GetPushRulesJob::GetPushRulesJob() - : BaseJob(HttpVerb::Get, GetPushRulesJobName, - basePath % "/pushrules") - , d(new Private) -{ -} - -GetPushRulesJob::~GetPushRulesJob() = default; - -const PushRuleset& GetPushRulesJob::global() const -{ - return d->global; -} - -BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"), + makePath("/_matrix/client/v3", "/pushrules")) { - auto json = data.object(); - if (!json.contains("global"_ls)) - return { JsonParseError, - "The key 'global' not found in the response" }; - d->global = fromJson<PushRuleset>(json.value("global"_ls)); - return Success; + addExpectedKey("global"); } -class GetPushRuleJob::Private -{ - public: - PushRule data; -}; - -QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId); + makePath("/_matrix/client/v3", "/pushrules/", + scope, "/", kind, "/", ruleId)); } -static const auto GetPushRuleJobName = QStringLiteral("GetPushRuleJob"); +GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"), + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, + "/", ruleId)) +{} -GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Get, GetPushRuleJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId) - , d(new Private) -{ -} - -GetPushRuleJob::~GetPushRuleJob() = default; - -const PushRule& GetPushRuleJob::data() const -{ - return d->data; -} - -BaseJob::Status GetPushRuleJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<PushRule>(data); - return Success; -} - -QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, + const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId); + makePath("/_matrix/client/v3", "/pushrules/", + scope, "/", kind, "/", ruleId)); } -static const auto DeletePushRuleJobName = QStringLiteral("DeletePushRuleJob"); +DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"), + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, + "/", ruleId)) +{} -DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Delete, DeletePushRuleJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId) +auto queryToSetPushRule(const QString& before, const QString& after) { -} - -BaseJob::Query 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; } -static const auto SetPushRuleJobName = QStringLiteral("SetPushRuleJob"); - -SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions, const QString& before, const QString& after, const QVector<PushCondition>& conditions, const QString& pattern) - : BaseJob(HttpVerb::Put, SetPushRuleJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId, - queryToSetPushRule(before, after)) +SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions, + const QString& before, const QString& after, + const QVector<PushCondition>& conditions, + const QString& pattern) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"), + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, + "/", ruleId), + queryToSetPushRule(before, after)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("actions"), actions); - addParam<IfNotEmpty>(_data, QStringLiteral("conditions"), conditions); - addParam<IfNotEmpty>(_data, QStringLiteral("pattern"), pattern); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("actions"), actions); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("conditions"), conditions); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("pattern"), pattern); + setRequestData({ _dataJson }); } -class IsPushRuleEnabledJob::Private -{ - public: - bool enabled; -}; - -QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, + const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled"); -} - -static const auto IsPushRuleEnabledJobName = QStringLiteral("IsPushRuleEnabledJob"); - -IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Get, IsPushRuleEnabledJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled") - , d(new Private) -{ -} - -IsPushRuleEnabledJob::~IsPushRuleEnabledJob() = default; - -bool IsPushRuleEnabledJob::enabled() const -{ - return d->enabled; + makePath("/_matrix/client/v3", "/pushrules/", + scope, "/", kind, "/", ruleId, + "/enabled")); } -BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data) +IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, + const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"), + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { - auto json = data.object(); - if (!json.contains("enabled"_ls)) - return { JsonParseError, - "The key 'enabled' not found in the response" }; - d->enabled = fromJson<bool>(json.value("enabled"_ls)); - return Success; + addExpectedKey("enabled"); } -static const auto SetPushRuleEnabledJobName = QStringLiteral("SetPushRuleEnabledJob"); - -SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled) - : BaseJob(HttpVerb::Put, SetPushRuleEnabledJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled") +SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, + const QString& kind, + const QString& ruleId, bool enabled) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"), + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("enabled"), enabled); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("enabled"), enabled); + setRequestData({ _dataJson }); } -class GetPushRuleActionsJob::Private -{ - public: - QStringList actions; -}; - -QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, + const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions"); + makePath("/_matrix/client/v3", "/pushrules/", + scope, "/", kind, "/", ruleId, + "/actions")); } -static const auto GetPushRuleActionsJobName = QStringLiteral("GetPushRuleActionsJob"); - -GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Get, GetPushRuleActionsJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions") - , d(new Private) +GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, + const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"), + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { + addExpectedKey("actions"); } -GetPushRuleActionsJob::~GetPushRuleActionsJob() = default; - -const QStringList& GetPushRuleActionsJob::actions() const +SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, + const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"), + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { - return d->actions; + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("actions"), actions); + setRequestData({ _dataJson }); } - -BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("actions"_ls)) - return { JsonParseError, - "The key 'actions' not found in the response" }; - d->actions = fromJson<QStringList>(json.value("actions"_ls)); - return Success; -} - -static const auto SetPushRuleActionsJobName = QStringLiteral("SetPushRuleActionsJob"); - -SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions) - : BaseJob(HttpVerb::Put, SetPushRuleActionsJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions") -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("actions"), actions); - setRequestData(_data); -} - diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index c038401c..d6c57efd 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -4,270 +4,279 @@ #pragma once -#include "jobs/basejob.h" - -#include "csapi/definitions/push_ruleset.h" -#include "converters.h" -#include "csapi/definitions/push_rule.h" -#include <QtCore/QVector> #include "csapi/definitions/push_condition.h" +#include "csapi/definitions/push_rule.h" +#include "csapi/definitions/push_ruleset.h" -namespace QMatrixClient -{ - // Operations - - /// 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. - class GetPushRulesJob : public BaseJob - { - public: - explicit GetPushRulesJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushRulesJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetPushRulesJob() override; - - // Result properties - - /// The global ruleset. - const PushRuleset& global() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Retrieve a push rule. - /// - /// Retrieve a single specified push rule. - class GetPushRuleJob : public BaseJob - { - public: - /*! Retrieve a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushRuleJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); - - ~GetPushRuleJob() override; - - // Result properties - - /// The push rule. - const PushRule& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Delete a push rule. - /// - /// This endpoint removes the push rule defined in the path. - class DeletePushRuleJob : public BaseJob - { - public: - /*! Delete a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * DeletePushRuleJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); - - }; - - /// Add or change a push rule. - /// - /// 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. - /// - /// When creating push rules, they MUST be enabled by default. - class SetPushRuleJob : public BaseJob - { - public: - /*! Add or change a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - * \param actions - * 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 - * 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. - * \param after - * This makes the new rule the next-less important rule relative to the - * given user defined rule. It is not possible to add a rule relative - * to a predefined server rule. - * \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. - * \param pattern - * Only applicable to ``content`` rules. The glob-style pattern to match against. - */ - explicit SetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions, const QString& before = {}, const QString& after = {}, const QVector<PushCondition>& conditions = {}, const QString& pattern = {}); - }; - - /// Get whether a push rule is enabled - /// - /// This endpoint gets whether the specified push rule is enabled. - class IsPushRuleEnabledJob : public BaseJob - { - public: - /*! 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``. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * IsPushRuleEnabledJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); - - ~IsPushRuleEnabledJob() override; - - // Result properties +#include "jobs/basejob.h" - /// Whether the push rule is enabled or not. - bool enabled() const; +namespace Quotient { - protected: - Status parseJson(const QJsonDocument& data) override; +/*! \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. + */ +class QUOTIENT_API GetPushRulesJob : public BaseJob { +public: + /// Retrieve all push rulesets. + explicit GetPushRulesJob(); - private: - class Private; - QScopedPointer<Private> d; - }; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushRulesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - /// Enable or disable a push rule. - /// - /// This endpoint allows clients to enable or disable the specified push rule. - class SetPushRuleEnabledJob : public BaseJob - { - public: - /*! Enable or disable a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - * \param enabled - * Whether the push rule is enabled or not. - */ - explicit SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled); - }; + // Result properties - /// The actions for a push rule - /// - /// This endpoint get the actions for the specified push rule. - class GetPushRuleActionsJob : public BaseJob + /// The global ruleset. + PushRuleset global() const { - public: - /*! 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``. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushRuleActionsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); + return loadFromJson<PushRuleset>("global"_ls); + } +}; - ~GetPushRuleActionsJob() override; - - // Result properties - - /// The action(s) to perform for this rule. - const QStringList& actions() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Set the actions for a push rule. - /// - /// This endpoint allows clients to change the actions of a push rule. - /// This can be used to change the actions of builtin rules. - class SetPushRuleActionsJob : public BaseJob +/*! \brief Retrieve a push rule. + * + * Retrieve a single specified push rule. + */ +class QUOTIENT_API GetPushRuleJob : public BaseJob { +public: + /*! \brief Retrieve a push rule. + * + * \param scope + * `global` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit GetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushRuleJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); + + // 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. + PushRule pushRule() const { return fromJson<PushRule>(jsonData()); } +}; + +/*! \brief Delete a push rule. + * + * This endpoint removes the push rule defined in the path. + */ +class QUOTIENT_API DeletePushRuleJob : public BaseJob { +public: + /*! \brief Delete a push rule. + * + * \param scope + * `global` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit DeletePushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for DeletePushRuleJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); +}; + +/*! \brief Add or change a push rule. + * + * 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. + * + * When creating push rules, they MUST be enabled by default. + */ +class QUOTIENT_API SetPushRuleJob : public BaseJob { +public: + /*! \brief Add or change a push rule. + * + * \param scope + * `global` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + * + * \param actions + * 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 + * 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. + * + * \param after + * This makes the new rule the next-less important rule relative to the + * given user defined rule. It is not possible to add a rule relative + * to a predefined server rule. + * + * \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. + * + * \param pattern + * Only applicable to `content` rules. The glob-style pattern to match + * against. + */ + explicit SetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions, + const QString& before = {}, + const QString& after = {}, + const QVector<PushCondition>& conditions = {}, + const QString& pattern = {}); +}; + +/*! \brief Get whether a push rule is enabled + * + * This endpoint gets whether the specified push rule is enabled. + */ +class QUOTIENT_API IsPushRuleEnabledJob : public BaseJob { +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`. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit IsPushRuleEnabledJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for IsPushRuleEnabledJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); + + // Result properties + + /// Whether the push rule is enabled or not. + bool enabled() const { return loadFromJson<bool>("enabled"_ls); } +}; + +/*! \brief Enable or disable a push rule. + * + * This endpoint allows clients to enable or disable the specified push rule. + */ +class QUOTIENT_API SetPushRuleEnabledJob : public BaseJob { +public: + /*! \brief Enable or disable a push rule. + * + * \param scope + * `global` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + * + * \param enabled + * Whether the push rule is enabled or not. + */ + explicit SetPushRuleEnabledJob(const QString& scope, const QString& kind, + const QString& ruleId, bool enabled); +}; + +/*! \brief The actions for a push rule + * + * This endpoint get the actions for the specified push rule. + */ +class QUOTIENT_API GetPushRuleActionsJob : public BaseJob { +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`. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit GetPushRuleActionsJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushRuleActionsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); + + // Result properties + + /// The action(s) to perform for this rule. + QVector<QVariant> actions() const { - public: - /*! Set the actions for a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - * \param actions - * The action(s) to perform for this rule. - */ - explicit SetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions); - }; -} // namespace QMatrixClient + return loadFromJson<QVector<QVariant>>("actions"_ls); + } +}; + +/*! \brief Set the actions for a push rule. + * + * This endpoint allows clients to change the actions of a push rule. + * This can be used to change the actions of builtin rules. + */ +class QUOTIENT_API SetPushRuleActionsJob : public BaseJob { +public: + /*! \brief Set the actions for a push rule. + * + * \param scope + * `global` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + * + * \param actions + * The action(s) to perform for this rule. + */ + explicit SetPushRuleActionsJob(const QString& scope, const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions); +}; + +} // namespace Quotient diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index 1bc67ba0..febd6d3a 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -4,23 +4,19 @@ #include "read_markers.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetReadMarkerJobName = QStringLiteral("SetReadMarkerJob"); - -SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead) - : BaseJob(HttpVerb::Post, SetReadMarkerJobName, - basePath % "/rooms/" % roomId % "/read_markers") +using namespace Quotient; + +SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, + const QString& mFullyRead, + const QString& mRead, + const QString& mReadPrivate) + : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/read_markers")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); - addParam<IfNotEmpty>(_data, QStringLiteral("m.read"), mRead); - setRequestData(_data); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.fully_read"), mFullyRead); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read"), mRead); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read.private"), + mReadPrivate); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index f19f46b0..1024076f 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -6,29 +6,38 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Set the position of the read marker for a room. + * + * Sets the position of the read marker for a given room, and optionally + * the read receipt's location. + */ +class QUOTIENT_API SetReadMarkerJob : public BaseJob { +public: + /*! \brief Set the position of the read marker for a room. + * + * \param roomId + * The room ID to set the read marker in for the user. + * + * \param mFullyRead + * The event ID the read marker should be located at. The + * event MUST belong to the room. + * + * \param mRead + * The event ID to set the read receipt location at. This is + * equivalent to calling `/receipt/m.read/$elsewhere:example.org` + * and is provided here to save that extra call. + * + * \param mReadPrivate + * The event ID to set the *private* read receipt location at. This + * equivalent to calling `/receipt/m.read.private/$elsewhere:example.org` + * and is provided here to save that extra call. + */ + explicit SetReadMarkerJob(const QString& roomId, + const QString& mFullyRead = {}, + const QString& mRead = {}, + const QString& mReadPrivate = {}); +}; - /// Set the position of the read marker for a room. - /// - /// Sets the position of the read marker for a given room, and optionally - /// the read receipt's location. - class SetReadMarkerJob : public BaseJob - { - public: - /*! Set the position of the read marker for a room. - * \param roomId - * The room ID to set the read marker in for the user. - * \param mFullyRead - * The event ID the read marker should be located at. The - * event MUST belong to the room. - * \param mRead - * The event ID to set the read receipt location at. This is - * equivalent to calling ``/receipt/m.read/$elsewhere:domain.com`` - * and is provided here to save that extra call. - */ - explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index b78ba533..0194603d 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -4,20 +4,14 @@ #include "receipts.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto PostReceiptJobName = QStringLiteral("PostReceiptJob"); - -PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt) - : BaseJob(HttpVerb::Post, PostReceiptJobName, - basePath % "/rooms/" % roomId % "/receipt/" % receiptType % "/" % eventId) +using namespace Quotient; + +PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, + const QString& eventId, + const QJsonObject& receipt) + : BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/receipt/", + receiptType, "/", eventId)) { - setRequestData(Data(toJson(receipt))); + setRequestData({ toJson(receipt) }); } - diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index 47e2f3c7..98bc5004 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -6,30 +6,39 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Send a receipt for the given event ID. + * + * This API updates the marker for the given receipt type to the event ID + * specified. + */ +class QUOTIENT_API PostReceiptJob : public BaseJob { +public: + /*! \brief Send a receipt for the given event ID. + * + * \param roomId + * The room in which to send the event. + * + * \param receiptType + * The type of receipt to send. This can also be `m.fully_read` as an + * alternative to + * [`/read_makers`](/client-server-api/#post_matrixclientv3roomsroomidread_markers). + * + * Note that `m.fully_read` does not appear under `m.receipt`: this + * endpoint effectively calls `/read_markers` internally when presented with + * a receipt type of `m.fully_read`. + * + * \param eventId + * 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. + */ + explicit PostReceiptJob(const QString& roomId, const QString& receiptType, + const QString& eventId, + const QJsonObject& receipt = {}); +}; - /// Send a receipt for the given event ID. - /// - /// This API updates the marker for the given receipt type to the event ID - /// specified. - class PostReceiptJob : public BaseJob - { - public: - /*! Send a receipt for the given event ID. - * \param roomId - * The room in which to send the event. - * \param receiptType - * The type of receipt to send. - * \param eventId - * 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. - */ - explicit PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 64098670..154abd9b 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -4,43 +4,15 @@ #include "redaction.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class RedactEventJob::Private -{ - public: - QString eventId; -}; - -static const auto RedactEventJobName = QStringLiteral("RedactEventJob"); - -RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) - : BaseJob(HttpVerb::Put, RedactEventJobName, - basePath % "/rooms/" % roomId % "/redact/" % eventId % "/" % txnId) - , d(new Private) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); -} - -RedactEventJob::~RedactEventJob() = default; - -const QString& RedactEventJob::eventId() const +RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, + const QString& txnId, const QString& reason) + : BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/redact/", + eventId, "/", txnId)) { - return d->eventId; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } - -BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); - return Success; -} - diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index d02abfd0..2f85793e 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -6,48 +6,47 @@ #include "jobs/basejob.h" - -namespace QMatrixClient -{ - // Operations - - /// Strips all non-integrity-critical information out of an event. - /// - /// Strips all information out of an event which isn't critical to the - /// integrity of the server-side representation of the room. - /// - /// 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. - class RedactEventJob : public BaseJob - { - public: - /*! Strips all non-integrity-critical information out of an event. - * \param roomId - * The room from which to redact the event. - * \param eventId - * The ID of the event to redact - * \param txnId - * The transaction ID for this event. Clients should generate a - * unique ID; it will be used by the server to ensure idempotency of requests. - * \param reason - * The reason for the event being redacted. - */ - explicit RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason = {}); - ~RedactEventJob() override; - - // Result properties - - /// A unique identifier for the event. - const QString& eventId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { + +/*! \brief Strips all non-integrity-critical information out of an event. + * + * Strips all information out of an event which isn't critical to the + * integrity of the server-side representation of the room. + * + * This cannot be undone. + * + * 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 QUOTIENT_API RedactEventJob : public BaseJob { +public: + /*! \brief Strips all non-integrity-critical information out of an event. + * + * \param roomId + * The room from which to redact the event. + * + * \param eventId + * The ID of the event to redact + * + * \param txnId + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate a unique ID; it will be used by the + * server to ensure idempotency of requests. + * + * \param reason + * The reason for the event being redacted. + */ + explicit RedactEventJob(const QString& roomId, const QString& eventId, + const QString& txnId, const QString& reason = {}); + + // Result properties + + /// A unique identifier for the event. + QString eventId() const { return loadFromJson<QString>("event_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/refresh.cpp b/lib/csapi/refresh.cpp new file mode 100644 index 00000000..284ae4ff --- /dev/null +++ b/lib/csapi/refresh.cpp @@ -0,0 +1,18 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "refresh.h" + +using namespace Quotient; + +RefreshJob::RefreshJob(const QString& refreshToken) + : BaseJob(HttpVerb::Post, QStringLiteral("RefreshJob"), + makePath("/_matrix/client/v3", "/refresh"), false) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); + addExpectedKey("access_token"); +} diff --git a/lib/csapi/refresh.h b/lib/csapi/refresh.h new file mode 100644 index 00000000..d432802c --- /dev/null +++ b/lib/csapi/refresh.h @@ -0,0 +1,65 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Refresh an access token + * + * Refresh an access token. Clients should use the returned access token + * when making subsequent API calls, and store the returned refresh token + * (if given) in order to refresh the new access token when necessary. + * + * After an access token has been refreshed, a server can choose to + * invalidate the old access token immediately, or can choose not to, for + * example if the access token would expire soon anyways. Clients should + * not make any assumptions about the old access token still being valid, + * and should use the newly provided access token instead. + * + * The old refresh token remains valid until the new access token or refresh + * token is used, at which point the old refresh token is revoked. + * + * Note that this endpoint does not require authentication via an + * access token. Authentication is provided via the refresh token. + * + * Application Service identity assertion is disabled for this endpoint. + */ +class QUOTIENT_API RefreshJob : public BaseJob { +public: + /*! \brief Refresh an access token + * + * \param refreshToken + * The refresh token + */ + explicit RefreshJob(const QString& refreshToken = {}); + + // Result properties + + /// The new access token to use. + QString accessToken() const + { + return loadFromJson<QString>("access_token"_ls); + } + + /// The new refresh token to use when the access token needs to + /// be refreshed again. If not given, the old refresh token can + /// be re-used. + QString refreshToken() const + { + return loadFromJson<QString>("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. If not + /// given, the client can assume that the access token will not + /// expire. + Omittable<int> expiresInMs() const + { + return loadFromJson<Omittable<int>>("expires_in_ms"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 320ec796..04c0fe12 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -4,292 +4,127 @@ #include "registration.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class RegisterJob::Private -{ - public: - QString userId; - QString accessToken; - QString homeServer; - QString deviceId; -}; - -BaseJob::Query queryToRegister(const QString& kind) +auto queryToRegister(const QString& kind) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("kind"), kind); return _q; } -static const auto RegisterJobName = QStringLiteral("RegisterJob"); - -RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, bool bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, bool inhibitLogin) - : BaseJob(HttpVerb::Post, RegisterJobName, - basePath % "/register", - queryToRegister(kind), - {}, false) - , d(new Private) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - addParam<IfNotEmpty>(_data, QStringLiteral("bind_email"), bindEmail); - addParam<IfNotEmpty>(_data, QStringLiteral("username"), username); - addParam<IfNotEmpty>(_data, QStringLiteral("password"), password); - addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); - addParam<IfNotEmpty>(_data, QStringLiteral("inhibit_login"), inhibitLogin); - setRequestData(_data); -} - -RegisterJob::~RegisterJob() = default; - -const QString& RegisterJob::userId() const -{ - return d->userId; -} - -const QString& RegisterJob::accessToken() const +RegisterJob::RegisterJob(const QString& kind, + const Omittable<AuthenticationData>& auth, + const QString& username, const QString& password, + const QString& deviceId, + const QString& initialDeviceDisplayName, + Omittable<bool> inhibitLogin, + Omittable<bool> refreshToken) + : BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"), + makePath("/_matrix/client/v3", "/register"), + queryToRegister(kind), {}, false) { - return d->accessToken; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("username"), username); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("password"), password); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_id"), deviceId); + addParam<IfNotEmpty>(_dataJson, + QStringLiteral("initial_device_display_name"), + initialDeviceDisplayName); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("inhibit_login"), + inhibitLogin); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); + addExpectedKey("user_id"); } -const QString& RegisterJob::homeServer() const +RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( + const EmailValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"), + makePath("/_matrix/client/v3", "/register/email/requestToken"), + false) { - return d->homeServer; + setRequestData({ toJson(body) }); } -const QString& RegisterJob::deviceId() const +RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( + const MsisdnValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"), + makePath("/_matrix/client/v3", "/register/msisdn/requestToken"), + false) { - return d->deviceId; + setRequestData({ toJson(body) }); } -BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data) +ChangePasswordJob::ChangePasswordJob(const QString& newPassword, + bool logoutDevices, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), + makePath("/_matrix/client/v3", "/account/password")) { - auto json = data.object(); - if (!json.contains("user_id"_ls)) - return { JsonParseError, - "The key 'user_id' not found in the response" }; - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); - d->homeServer = fromJson<QString>(json.value("home_server"_ls)); - d->deviceId = fromJson<QString>(json.value("device_id"_ls)); - return Success; + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("new_password"), newPassword); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("logout_devices"), + logoutDevices); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } -class RequestTokenToRegisterEmailJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToRegisterEmailJobName = QStringLiteral("RequestTokenToRegisterEmailJob"); - -RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToRegisterEmailJobName, - basePath % "/register/email/requestToken", false) - , d(new Private) +RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( + const EmailValidationData& body) + : BaseJob(HttpVerb::Post, + QStringLiteral("RequestTokenToResetPasswordEmailJob"), + makePath("/_matrix/client/v3", + "/account/password/email/requestToken"), + false) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("email"), email); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); + setRequestData({ toJson(body) }); } -RequestTokenToRegisterEmailJob::~RequestTokenToRegisterEmailJob() = default; - -const Sid& RequestTokenToRegisterEmailJob::data() const +RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( + const MsisdnValidationData& body) + : BaseJob(HttpVerb::Post, + QStringLiteral("RequestTokenToResetPasswordMSISDNJob"), + makePath("/_matrix/client/v3", + "/account/password/msisdn/requestToken"), + false) { - return d->data; + setRequestData({ toJson(body) }); } -BaseJob::Status RequestTokenToRegisterEmailJob::parseJson(const QJsonDocument& data) +DeactivateAccountJob::DeactivateAccountJob( + const Omittable<AuthenticationData>& auth, const QString& idServer) + : BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"), + makePath("/_matrix/client/v3", "/account/deactivate")) { - d->data = fromJson<Sid>(data); - return Success; + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer); + setRequestData({ _dataJson }); + addExpectedKey("id_server_unbind_result"); } -class RequestTokenToRegisterMSISDNJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToRegisterMSISDNJobName = QStringLiteral("RequestTokenToRegisterMSISDNJob"); - -RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToRegisterMSISDNJobName, - basePath % "/register/msisdn/requestToken", false) - , d(new Private) +auto queryToCheckUsernameAvailability(const QString& username) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("country"), country); - addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); -} - -RequestTokenToRegisterMSISDNJob::~RequestTokenToRegisterMSISDNJob() = default; - -const Sid& RequestTokenToRegisterMSISDNJob::data() const -{ - return d->data; -} - -BaseJob::Status RequestTokenToRegisterMSISDNJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Sid>(data); - return Success; -} - -static const auto ChangePasswordJobName = QStringLiteral("ChangePasswordJob"); - -ChangePasswordJob::ChangePasswordJob(const QString& newPassword, const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Post, ChangePasswordJobName, - basePath % "/account/password") -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("new_password"), newPassword); - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); -} - -class RequestTokenToResetPasswordEmailJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToResetPasswordEmailJobName = QStringLiteral("RequestTokenToResetPasswordEmailJob"); - -RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToResetPasswordEmailJobName, - basePath % "/account/password/email/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("email"), email); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); -} - -RequestTokenToResetPasswordEmailJob::~RequestTokenToResetPasswordEmailJob() = default; - -const Sid& RequestTokenToResetPasswordEmailJob::data() const -{ - return d->data; -} - -BaseJob::Status RequestTokenToResetPasswordEmailJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Sid>(data); - return Success; -} - -class RequestTokenToResetPasswordMSISDNJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToResetPasswordMSISDNJobName = QStringLiteral("RequestTokenToResetPasswordMSISDNJob"); - -RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToResetPasswordMSISDNJobName, - basePath % "/account/password/msisdn/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("country"), country); - addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); -} - -RequestTokenToResetPasswordMSISDNJob::~RequestTokenToResetPasswordMSISDNJob() = default; - -const Sid& RequestTokenToResetPasswordMSISDNJob::data() const -{ - return d->data; -} - -BaseJob::Status RequestTokenToResetPasswordMSISDNJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Sid>(data); - return Success; -} - -static const auto DeactivateAccountJobName = QStringLiteral("DeactivateAccountJob"); - -DeactivateAccountJob::DeactivateAccountJob(const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Post, DeactivateAccountJobName, - basePath % "/account/deactivate") -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); -} - -class CheckUsernameAvailabilityJob::Private -{ - public: - bool available; -}; - -BaseJob::Query queryToCheckUsernameAvailability(const QString& username) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("username"), username); return _q; } -QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) +QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, + const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/register/available", - queryToCheckUsernameAvailability(username)); -} - -static const auto CheckUsernameAvailabilityJobName = QStringLiteral("CheckUsernameAvailabilityJob"); - -CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) - : BaseJob(HttpVerb::Get, CheckUsernameAvailabilityJobName, - basePath % "/register/available", - queryToCheckUsernameAvailability(username), - {}, false) - , d(new Private) -{ -} - -CheckUsernameAvailabilityJob::~CheckUsernameAvailabilityJob() = default; - -bool CheckUsernameAvailabilityJob::available() const -{ - return d->available; -} - -BaseJob::Status CheckUsernameAvailabilityJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->available = fromJson<bool>(json.value("available"_ls)); - return Success; + makePath("/_matrix/client/v3", + "/register/available"), + queryToCheckUsernameAvailability(username)); } +CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob( + const QString& username) + : BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"), + makePath("/_matrix/client/v3", "/register/available"), + queryToCheckUsernameAvailability(username), {}, false) +{} diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 9002b5c8..21d7f9d7 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -4,427 +4,456 @@ #pragma once -#include "jobs/basejob.h" - -#include "csapi/../identity/definitions/sid.h" -#include "converters.h" #include "csapi/definitions/auth_data.h" +#include "csapi/definitions/request_email_validation.h" +#include "csapi/definitions/request_msisdn_validation.h" +#include "csapi/definitions/request_token_response.h" -namespace QMatrixClient -{ - // Operations +#include "jobs/basejob.h" - /// Register for an account on this homeserver. +namespace Quotient { + +/*! \brief Register for an account on this homeserver. + * + * 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. + * + * There are two kinds of user account: + * + * - `user` accounts. These accounts may use the full API described in this + * specification. + * + * - `guest` accounts. These accounts may have limited permissions and may not + * be supported by all servers. + * + * 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 + * 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 + * 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`. + * + * Matrix clients MUST NOT assume that localpart of the registered + * `user_id` matches the provided `username`. + * + * 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](/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 + * regardless of input. + * + * Any user ID returned by this API must conform to the grammar given in the + * [Matrix specification](/appendices/#user-identifiers). + */ +class QUOTIENT_API RegisterJob : public BaseJob { +public: + /*! \brief Register for an account on this homeserver. + * + * \param kind + * 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. + * + * \param username + * The basis for the localpart of the desired Matrix ID. If omitted, + * the homeserver MUST generate a Matrix ID local part. + * + * \param password + * The desired password for the account. + * + * \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. + * + * \param initialDeviceDisplayName + * A display name to assign to the newly-created device. Ignored + * if `device_id` corresponds to a known device. + * + * \param inhibitLogin + * If true, an `access_token` and `device_id` should not be + * returned from this call, therefore preventing an automatic + * login. Defaults to false. + * + * \param refreshToken + * If true, the client supports refresh tokens. + */ + explicit RegisterJob(const QString& kind = QStringLiteral("user"), + const Omittable<AuthenticationData>& auth = none, + const QString& username = {}, + const QString& password = {}, + const QString& deviceId = {}, + const QString& initialDeviceDisplayName = {}, + Omittable<bool> inhibitLogin = none, + Omittable<bool> refreshToken = none); + + // Result properties + + /// The fully-qualified Matrix user ID (MXID) that has been registered. /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// Register for an account on this homeserver. - /// - /// There are two kinds of user account: - /// - /// - `user` accounts. These accounts may use the full API described in this specification. - /// - /// - `guest` accounts. These accounts may have limited permissions and may not be supported by all servers. - /// - /// 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 - /// 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 - /// 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``. - /// - /// Matrix clients MUST NOT assume that localpart of the registered - /// ``user_id`` matches the provided ``username``. - /// - /// 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`_. - class RegisterJob : public BaseJob + /// Any user ID returned by this API must conform to the grammar given in + /// 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. + QString accessToken() const { - public: - /*! Register for an account on this homeserver. - * \param kind - * 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. It should be - * left empty, or omitted, unless an earlier call returned an - * response with status code 401. - * \param bindEmail - * If true, the server binds the email used for authentication to - * the Matrix ID with the identity server. - * \param username - * The basis for the localpart of the desired Matrix ID. If omitted, - * the homeserver MUST generate a Matrix ID local part. - * \param password - * The desired password for the account. - * \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. - * \param initialDeviceDisplayName - * A display name to assign to the newly-created device. Ignored - * if ``device_id`` corresponds to a known device. - * \param inhibitLogin - * If true, an ``access_token`` and ``device_id`` should not be - * returned from this call, therefore preventing an automatic - * login. Defaults to false. - */ - explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, bool bindEmail = false, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, bool inhibitLogin = false); - ~RegisterJob() override; - - // Result properties - - /// 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 <https://matrix.org/docs/spec/appendices.html#user-identifiers>`_. - const QString& userId() const; - /// 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. - const QString& accessToken() const; - /// The server_name of the homeserver on which the account has - /// 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. - const QString& homeServer() const; - /// 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. - const QString& deviceId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Begins the validation process for an email to be used during registration. + return loadFromJson<QString>("access_token"_ls); + } + + /// A refresh token for the account. This token can be used to + /// obtain a new access token when it expires by calling the + /// `/refresh` endpoint. /// - /// Proxies the Identity Service API ``validate/email/requestToken``, but - /// first checks that the given email address is not already associated - /// with an account on this homeserver. See the Identity Service API for - /// further information. - class RequestTokenToRegisterEmailJob : public BaseJob + /// Omitted if the `inhibit_login` option is true. + QString refreshToken() const { - public: - /*! Begins the validation process for an email to be used during registration. - * \param clientSecret - * 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 - * must not be empty. - * \param email - * The email address to validate. - * \param sendAttempt - * 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 - * 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 - * email (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToRegisterEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToRegisterEmailJob() override; - - // Result properties - - /// An email has been sent to the specified address. - /// Note that this may be an email containing the validation token or it may be informing - /// the user of an error. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Requests a validation token be sent to the given phone number for the purpose of registering an account + return loadFromJson<QString>("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. Once + /// the access token has expired a new access token can be + /// obtained by using the provided refresh token. If no + /// refresh token is provided, the client will need to re-log in + /// to obtain a new access token. If not given, the client can + /// assume that the access token will not expire. /// - /// Proxies the Identity Service API ``validate/msisdn/requestToken``, but - /// first checks that the given phone number is not already associated - /// with an account on this homeserver. See the Identity Service API for - /// further information. - class RequestTokenToRegisterMSISDNJob : public BaseJob + /// Omitted if the `inhibit_login` option is true. + Omittable<int> expiresInMs() const { - public: - /*! Requests a validation token be sent to the given phone number for the purpose of registering an account - * \param clientSecret - * 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 - * must not be empty. - * \param country - * The two-letter uppercase ISO country code that the number in - * ``phone_number`` should be parsed as if it were dialled from. - * \param phoneNumber - * The phone number to validate. - * \param sendAttempt - * 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`` - * 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 - * they desire a new SMS (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToRegisterMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToRegisterMSISDNJob() override; - - // Result properties - - /// An SMS message has been sent to the specified phone number. - /// Note that this may be an SMS message containing the validation token or it may be informing - /// the user of an error. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Changes a user's password. - /// - /// Changes the password for an account on this homeserver. - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// An access token should be submitted to this endpoint if the client has - /// an active session. - /// - /// The homeserver may change the flows available depending on whether a - /// valid access token is provided. - class ChangePasswordJob : public BaseJob + return loadFromJson<Omittable<int>>("expires_in_ms"_ls); + } + + /// 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. + QString deviceId() const { return loadFromJson<QString>("device_id"_ls); } +}; + +/*! \brief Begins the validation process for an email to be used during + * registration. + * + * The homeserver must check that the given email address is **not** + * already associated with an account on this homeserver. The homeserver + * should validate the email itself, either by sending a validation email + * itself or by using a service it has control over. + */ +class QUOTIENT_API RequestTokenToRegisterEmailJob : public BaseJob { +public: + /*! \brief Begins the validation process for an email to be used during + * registration. + * + * \param body + * The homeserver must check that the given email address is **not** + * already associated with an account on this homeserver. The homeserver + * should validate the email itself, either by sending a validation email + * itself or by using a service it has control over. + */ + explicit RequestTokenToRegisterEmailJob(const EmailValidationData& body); + + // Result properties + + /// An email has been sent to the specified address. Note that this + /// may be an email containing the validation token or it may be + /// informing the user of an error. + RequestTokenResponse response() const { - public: - /*! Changes a user's password. - * \param newPassword - * The new password for the account. - * \param auth - * Additional authentication information for the user-interactive authentication API. - */ - explicit ChangePasswordJob(const QString& newPassword, const Omittable<AuthenticationData>& auth = none); - }; - - /// Requests a validation token be sent to the given email address for the purpose of resetting a user's password - /// - /// Proxies the Identity Service API ``validate/email/requestToken``, but - /// first checks 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. This API's parameters and response are - /// identical to that of the HS API |/register/email/requestToken|_ 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. - /// - /// .. |/register/email/requestToken| replace:: ``/register/email/requestToken`` - /// - /// .. _/register/email/requestToken: #post-matrix-client-r0-register-email-requesttoken - class RequestTokenToResetPasswordEmailJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Requests a validation token be sent to the given phone number for the + * purpose of registering an account + * + * The homeserver must check that the given phone number is **not** + * already associated with an account on this homeserver. The homeserver + * should validate the phone number itself, either by sending a validation + * message itself or by using a service it has control over. + */ +class QUOTIENT_API RequestTokenToRegisterMSISDNJob : public BaseJob { +public: + /*! \brief Requests a validation token be sent to the given phone number for + * the purpose of registering an account + * + * \param body + * The homeserver must check that the given phone number is **not** + * already associated with an account on this homeserver. 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 RequestTokenToRegisterMSISDNJob(const MsisdnValidationData& body); + + // Result properties + + /// An SMS message has been sent to the specified phone number. Note + /// that this may be an SMS message containing the validation token or + /// it may be informing the user of an error. + RequestTokenResponse response() const { - public: - /*! Requests a validation token be sent to the given email address for the purpose of resetting a user's password - * \param clientSecret - * 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 - * must not be empty. - * \param email - * The email address to validate. - * \param sendAttempt - * 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 - * 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 - * email (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToResetPasswordEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToResetPasswordEmailJob() override; - - // Result properties - - /// An email was sent to the given address. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Requests a validation token be sent to the given phone number for the purpose of resetting a user's password. - /// - /// Proxies the Identity Service API ``validate/msisdn/requestToken``, but - /// first checks 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. This API's parameters and response are - /// identical to that of the HS API |/register/msisdn/requestToken|_ 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 an - /// SMS message to the given address prompting the user to create an account. - /// `M_THREEPID_IN_USE` may not be returned. - /// - /// .. |/register/msisdn/requestToken| replace:: ``/register/msisdn/requestToken`` - /// - /// .. _/register/msisdn/requestToken: #post-matrix-client-r0-register-email-requesttoken - class RequestTokenToResetPasswordMSISDNJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Changes a user's password. + * + * Changes the password for an account on this homeserver. + * + * 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. + * + * The homeserver may change the flows available depending on whether a + * valid access token is provided. The homeserver SHOULD NOT revoke the + * access token provided in the request. Whether other access tokens for + * the user are revoked depends on the request parameters. + */ +class QUOTIENT_API ChangePasswordJob : public BaseJob { +public: + /*! \brief Changes a user's password. + * + * \param newPassword + * The new password for the account. + * + * \param logoutDevices + * 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](/client-server-api/#soft-logout) for the user's remaining + * devices. + * + * \param auth + * Additional authentication information for the user-interactive + * authentication API. + */ + explicit ChangePasswordJob(const QString& newPassword, + bool logoutDevices = true, + const Omittable<AuthenticationData>& auth = none); +}; + +/*! \brief Requests a validation token be sent to the given email address for + * the purpose of resetting a user's password + * + * 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. + * + * This API's parameters and response are identical to that of the + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) + * 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. + * + * The homeserver should validate the email itself, either by sending a + * validation email itself or by using a service it has control over. + */ +class QUOTIENT_API RequestTokenToResetPasswordEmailJob : public BaseJob { +public: + /*! \brief Requests a validation token be sent to the given email address + * for the purpose of resetting a user's password + * + * \param body + * 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. + * + * This API's parameters and response are identical to that of the + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) + * 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. + * + * The homeserver should validate the email itself, either by sending a + * validation email itself or by using a service it has control over. + */ + explicit RequestTokenToResetPasswordEmailJob(const EmailValidationData& body); + + // Result properties + + /// An email was sent to the given address. + RequestTokenResponse response() const { - public: - /*! Requests a validation token be sent to the given phone number for the purpose of resetting a user's password. - * \param clientSecret - * 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 - * must not be empty. - * \param country - * The two-letter uppercase ISO country code that the number in - * ``phone_number`` should be parsed as if it were dialled from. - * \param phoneNumber - * The phone number to validate. - * \param sendAttempt - * 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`` - * 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 - * they desire a new SMS (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToResetPasswordMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToResetPasswordMSISDNJob() override; - - // Result properties - - /// An SMS message was sent to the given phone number. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Deactivate a user's account. - /// - /// Deactivate the user's account, removing all ability for the user to - /// login again. - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// An access token should be submitted to this endpoint if the client has - /// an active session. - /// - /// The homeserver may change the flows available depending on whether a - /// valid access token is provided. - class DeactivateAccountJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Requests a validation token be sent to the given phone number for the + * purpose of resetting a user's password. + * + * 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. + * + * This API's parameters and response are identical to that of the + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) + * 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. + * + * The homeserver should validate the phone number itself, either by sending a + * validation message itself or by using a service it has control over. + */ +class QUOTIENT_API RequestTokenToResetPasswordMSISDNJob : public BaseJob { +public: + /*! \brief Requests a validation token be sent to the given phone number for + * the purpose of resetting a user's password. + * + * \param body + * 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. + * + * This API's parameters and response are identical to that of the + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) + * 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. + * + * 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 RequestTokenToResetPasswordMSISDNJob( + const MsisdnValidationData& body); + + // Result properties + + /// An SMS message was sent to the given phone number. + RequestTokenResponse response() const { - public: - /*! Deactivate a user's account. - * \param auth - * Additional authentication information for the user-interactive authentication API. - */ - explicit DeactivateAccountJob(const Omittable<AuthenticationData>& auth = none); - }; - - /// Checks to see if a username is available on the server. - /// - /// Checks to see if a username is available, and valid, for the server. - /// - /// The server should check to ensure that, at the time of the request, the - /// username requested is available for use. This includes verifying that an - /// application service has not claimed the username and that the username - /// fits the server's desired requirements (for example, a server could dictate - /// that it does not permit usernames with underscores). - /// - /// Matrix clients may wish to use this API prior to attempting registration, - /// however the clients must also be aware that using this API does not normally - /// reserve the username. This can mean that the username becomes unavailable - /// between checking its availability and attempting to register it. - class CheckUsernameAvailabilityJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Deactivate a user's account. + * + * Deactivate the user's account, removing all ability for the user to + * login again. + * + * 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. + * + * 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` + * parameter because the homeserver is expected to sign the request to the + * identity server instead. + */ +class QUOTIENT_API DeactivateAccountJob : public BaseJob { +public: + /*! \brief Deactivate a user's account. + * + * \param auth + * Additional authentication information for the user-interactive + * authentication API. + * + * \param idServer + * The identity server to unbind all of the user's 3PIDs from. + * 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`. + */ + explicit DeactivateAccountJob( + const Omittable<AuthenticationData>& auth = none, + const QString& idServer = {}); + + // 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 + /// that all identifiers have been unbound from the identity server while + /// `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 + /// for the user. + QString idServerUnbindResult() const + { + return loadFromJson<QString>("id_server_unbind_result"_ls); + } +}; + +/*! \brief Checks to see if a username is available on the server. + * + * Checks to see if a username is available, and valid, for the server. + * + * The server should check to ensure that, at the time of the request, the + * username requested is available for use. This includes verifying that an + * application service has not claimed the username and that the username + * fits the server's desired requirements (for example, a server could dictate + * that it does not permit usernames with underscores). + * + * Matrix clients may wish to use this API prior to attempting registration, + * however the clients must also be aware that using this API does not normally + * reserve the username. This can mean that the username becomes unavailable + * between checking its availability and attempting to register it. + */ +class QUOTIENT_API CheckUsernameAvailabilityJob : public BaseJob { +public: + /*! \brief Checks to see if a username is available on the server. + * + * \param username + * The username to check the availability of. + */ + explicit CheckUsernameAvailabilityJob(const QString& username); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for CheckUsernameAvailabilityJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& username); + + // Result properties + + /// A flag to indicate that the username is available. This should always + /// be `true` when the server replies with 200 OK. + Omittable<bool> available() const { - public: - /*! Checks to see if a username is available on the server. - * \param username - * The username to check the availability of. - */ - explicit CheckUsernameAvailabilityJob(const QString& username); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * CheckUsernameAvailabilityJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& username); - - ~CheckUsernameAvailabilityJob() override; - - // Result properties - - /// A flag to indicate that the username is available. This should always - /// be ``true`` when the server replies with 200 OK. - bool available() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<Omittable<bool>>("available"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/registration_tokens.cpp b/lib/csapi/registration_tokens.cpp new file mode 100644 index 00000000..9c1f0587 --- /dev/null +++ b/lib/csapi/registration_tokens.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "registration_tokens.h" + +using namespace Quotient; + +auto queryToRegistrationTokenValidity(const QString& token) +{ + QUrlQuery _q; + addParam<>(_q, QStringLiteral("token"), token); + return _q; +} + +QUrl RegistrationTokenValidityJob::makeRequestUrl(QUrl baseUrl, + const QString& token) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", + "/register/m.login.registration_token/validity"), + queryToRegistrationTokenValidity(token)); +} + +RegistrationTokenValidityJob::RegistrationTokenValidityJob(const QString& token) + : BaseJob(HttpVerb::Get, QStringLiteral("RegistrationTokenValidityJob"), + makePath("/_matrix/client/v1", + "/register/m.login.registration_token/validity"), + queryToRegistrationTokenValidity(token), {}, false) +{ + addExpectedKey("valid"); +} diff --git a/lib/csapi/registration_tokens.h b/lib/csapi/registration_tokens.h new file mode 100644 index 00000000..e3008dd4 --- /dev/null +++ b/lib/csapi/registration_tokens.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Query if a given registration token is still valid. + * + * Queries the server to determine if a given registration token is still + * valid at the time of request. This is a point-in-time check where the + * token might still expire by the time it is used. + * + * Servers should be sure to rate limit this endpoint to avoid brute force + * attacks. + */ +class QUOTIENT_API RegistrationTokenValidityJob : public BaseJob { +public: + /*! \brief Query if a given registration token is still valid. + * + * \param token + * The token to check validity of. + */ + explicit RegistrationTokenValidityJob(const QString& token); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for RegistrationTokenValidityJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& token); + + // Result properties + + /// True if the token is still valid, false otherwise. This should + /// additionally be false if the token is not a recognised token by + /// the server. + bool valid() const { return loadFromJson<bool>("valid"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/relations.cpp b/lib/csapi/relations.cpp new file mode 100644 index 00000000..1d8febcc --- /dev/null +++ b/lib/csapi/relations.cpp @@ -0,0 +1,118 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "relations.h" + +using namespace Quotient; + +auto queryToGetRelatingEvents(const QString& from, const QString& to, + Omittable<int> limit, const QString& dir) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir); + return _q; +} + +QUrl GetRelatingEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + const QString& from, const QString& to, + Omittable<int> limit, + const QString& dir) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", + roomId, "/relations/", eventId), + queryToGetRelatingEvents(from, to, limit, + dir)); +} + +GetRelatingEventsJob::GetRelatingEventsJob( + const QString& roomId, const QString& eventId, const QString& from, + const QString& to, Omittable<int> limit, const QString& dir) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId), + queryToGetRelatingEvents(from, to, limit, dir)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelType(const QString& from, const QString& to, + Omittable<int> limit, + const QString& dir) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir); + return _q; +} + +QUrl GetRelatingEventsWithRelTypeJob::makeRequestUrl( + QUrl baseUrl, const QString& roomId, const QString& eventId, + const QString& relType, const QString& from, const QString& to, + Omittable<int> limit, const QString& dir) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit, dir)); +} + +GetRelatingEventsWithRelTypeJob::GetRelatingEventsWithRelTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& from, const QString& to, Omittable<int> limit, + const QString& dir) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit, dir)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelTypeAndEventType(const QString& from, + const QString& to, + Omittable<int> limit, + const QString& dir) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir); + return _q; +} + +QUrl GetRelatingEventsWithRelTypeAndEventTypeJob::makeRequestUrl( + QUrl baseUrl, const QString& roomId, const QString& eventId, + const QString& relType, const QString& eventType, const QString& from, + const QString& to, Omittable<int> limit, const QString& dir) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit, dir)); +} + +GetRelatingEventsWithRelTypeAndEventTypeJob:: + GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from, const QString& to, + Omittable<int> limit, const QString& dir) + : BaseJob(HttpVerb::Get, + QStringLiteral("GetRelatingEventsWithRelTypeAndEventTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit, + dir)) +{ + addExpectedKey("chunk"); +} diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h new file mode 100644 index 00000000..5d6efd1c --- /dev/null +++ b/lib/csapi/relations.h @@ -0,0 +1,298 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/roomevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Get the child events for a given parent event. + * + * Retrieve all of the child events for a given parent event. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsJob : public BaseJob { +public: + /*! \brief Get the child events for a given parent event. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. + */ + explicit GetRelatingEventsJob(const QString& roomId, const QString& eventId, + const QString& from = {}, + const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRelatingEventsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& from = {}, + const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } +}; + +/*! \brief Get the child events for a given parent event, with a given + * `relType`. + * + * Retrieve all of the child events for a given parent event which relate to the + * parent using the given `relType`. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsWithRelTypeJob : public BaseJob { +public: + /*! \brief Get the child events for a given parent event, with a given + * `relType`. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param relType + * The [relationship type](/client-server-api/#relationship-types) to + * search for. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. + */ + explicit GetRelatingEventsWithRelTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& from = {}, const QString& to = {}, + Omittable<int> limit = none, const QString& dir = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRelatingEventsWithRelTypeJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& relType, + const QString& from = {}, const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. The events returned will match the `relType` + /// supplied in the URL. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } +}; + +/*! \brief Get the child events for a given parent event, with a given `relType` + * and `eventType`. + * + * Retrieve all of the child events for a given parent event which relate to the + * parent using the given `relType` and have the given `eventType`. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsWithRelTypeAndEventTypeJob + : public BaseJob { +public: + /*! \brief Get the child events for a given parent event, with a given + * `relType` and `eventType`. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param relType + * The [relationship type](/client-server-api/#relationship-types) to + * search for. + * + * \param eventType + * The event type of child events to search for. + * + * Note that in encrypted rooms this will typically always be + * `m.room.encrypted` regardless of the event type contained within the + * encrypted payload. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. + */ + explicit GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from = {}, + const QString& to = {}, Omittable<int> limit = none, + const QString& dir = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetRelatingEventsWithRelTypeAndEventTypeJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& relType, + const QString& eventType, + const QString& from = {}, const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. The events returned will match the `relType` and + /// `eventType` supplied in the URL. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index a79d4dad..bc52208f 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -4,23 +4,16 @@ #include "report_content.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto ReportContentJobName = QStringLiteral("ReportContentJob"); - -ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, int score, const QString& reason) - : BaseJob(HttpVerb::Post, ReportContentJobName, - basePath % "/rooms/" % roomId % "/report/" % eventId) +ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, + Omittable<int> score, const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/report/", + eventId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("score"), score); - addParam<>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("score"), score); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h index a20c6838..8c533c19 100644 --- a/lib/csapi/report_content.h +++ b/lib/csapi/report_content.h @@ -6,30 +6,33 @@ #include "jobs/basejob.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Reports an event as inappropriate. + * + * Reports an event as inappropriate to the server, which may then notify + * the appropriate people. + */ +class QUOTIENT_API ReportContentJob : public BaseJob { +public: + /*! \brief Reports an event as inappropriate. + * + * \param roomId + * The room in which the event being reported is located. + * + * \param eventId + * The event to report. + * + * \param score + * The score to rate this content as where -100 is most offensive + * and 0 is inoffensive. + * + * \param reason + * The reason the content is being reported. May be blank. + */ + explicit ReportContentJob(const QString& roomId, const QString& eventId, + Omittable<int> score = none, + const QString& reason = {}); +}; - /// Reports an event as inappropriate. - /// - /// Reports an event as inappropriate to the server, which may then notify - /// the appropriate people. - class ReportContentJob : public BaseJob - { - public: - /*! Reports an event as inappropriate. - * \param roomId - * The room in which the event being reported is located. - * \param eventId - * The event to report. - * \param score - * The score to rate this content as where -100 is most offensive - * and 0 is inoffensive. - * \param reason - * The reason the content is being reported. May be blank. - */ - explicit ReportContentJob(const QString& roomId, const QString& eventId, int score, const QString& reason); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 2b39ede2..2319496f 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -4,41 +4,14 @@ #include "room_send.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class SendMessageJob::Private -{ - public: - QString eventId; -}; - -static const auto SendMessageJobName = QStringLiteral("SendMessageJob"); - -SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body) - : BaseJob(HttpVerb::Put, SendMessageJobName, - basePath % "/rooms/" % roomId % "/send/" % eventType % "/" % txnId) - , d(new Private) -{ - setRequestData(Data(toJson(body))); -} - -SendMessageJob::~SendMessageJob() = default; - -const QString& SendMessageJob::eventId() const +SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, + const QString& txnId, const QJsonObject& body) + : BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/send/", + eventType, "/", txnId)) { - return d->eventId; + setRequestData({ toJson(body) }); + addExpectedKey("event_id"); } - -BaseJob::Status SendMessageJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); - return Success; -} - diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index 85c298e0..abe5f207 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -6,55 +6,52 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations - - /// Send a message event to the given room. - /// - /// This endpoint is used to send a message event to a room. Message events - /// allow access to historical events and pagination, making them suited - /// for "once-off" activity in a room. - /// - /// 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. - class SendMessageJob : public BaseJob - { - public: - /*! Send a message event to the given room. - * \param roomId - * The room to send the event to. - * \param eventType - * The type of event to send. - * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. - * \param body - * This endpoint is used to send a message event to a room. Message events - * allow access to historical events and pagination, making them suited - * for "once-off" activity in a room. - * - * 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. - */ - explicit SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body = {}); - ~SendMessageJob() override; - - // Result properties - - /// A unique identifier for the event. - const QString& eventId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { + +/*! \brief Send a message event to the given room. + * + * This endpoint is used to send a message event to a room. Message events + * allow access to historical events and pagination, making them suited + * for "once-off" activity in a room. + * + * 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](/client-server-api/#room-events) for the m. event + * specification. + */ +class QUOTIENT_API SendMessageJob : public BaseJob { +public: + /*! \brief Send a message event to the given room. + * + * \param roomId + * The room to send the event to. + * + * \param eventType + * The type of event to send. + * + * \param txnId + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate an ID unique across requests with the + * same access token; it will be used by the server to ensure idempotency of + * requests. + * + * \param body + * This endpoint is used to send a message event to a room. Message events + * allow access to historical events and pagination, making them suited + * for "once-off" activity in a room. + * + * 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](/client-server-api/#room-events) for the m. event + * specification. + */ + explicit SendMessageJob(const QString& roomId, const QString& eventType, + const QString& txnId, const QJsonObject& body = {}); + + // Result properties + + /// A unique identifier for the event. + QString eventId() const { return loadFromJson<QString>("event_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index 8f87979d..b4adb739 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -4,71 +4,16 @@ #include "room_state.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class SetRoomStateWithKeyJob::Private -{ - public: - QString eventId; -}; - -static const auto SetRoomStateWithKeyJobName = QStringLiteral("SetRoomStateWithKeyJob"); - -SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey, const QJsonObject& body) - : BaseJob(HttpVerb::Put, SetRoomStateWithKeyJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) - , d(new Private) -{ - setRequestData(Data(toJson(body))); -} - -SetRoomStateWithKeyJob::~SetRoomStateWithKeyJob() = default; - -const QString& SetRoomStateWithKeyJob::eventId() const -{ - return d->eventId; -} - -BaseJob::Status SetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); - return Success; +using namespace Quotient; + +SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey, + const QJsonObject& body) + : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", + eventType, "/", stateKey)) +{ + setRequestData({ toJson(body) }); + addExpectedKey("event_id"); } - -class SetRoomStateJob::Private -{ - public: - QString eventId; -}; - -static const auto SetRoomStateJobName = QStringLiteral("SetRoomStateJob"); - -SetRoomStateJob::SetRoomStateJob(const QString& roomId, const QString& eventType, const QJsonObject& body) - : BaseJob(HttpVerb::Put, SetRoomStateJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType) - , d(new Private) -{ - setRequestData(Data(toJson(body))); -} - -SetRoomStateJob::~SetRoomStateJob() = default; - -const QString& SetRoomStateJob::eventId() const -{ - return d->eventId; -} - -BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); - return Success; -} - diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index 67420545..a00b0947 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -6,113 +6,72 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Send a state event to the given room. - /// - /// State events can be sent using this endpoint. These events will be - /// 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. - /// - /// 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. - class SetRoomStateWithKeyJob : public BaseJob - { - public: - /*! Send a state event to the given room. - * \param roomId - * The room to set the state in - * \param eventType - * The type of event to send. - * \param stateKey - * The state_key for the state to send. Defaults to the empty string. - * \param body - * State events can be sent using this endpoint. These events will be - * 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. - * - * 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. - */ - explicit SetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey, const QJsonObject& body = {}); - ~SetRoomStateWithKeyJob() override; - - // Result properties - - /// A unique identifier for the event. - const QString& eventId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Send a state event to the given room. - /// - /// State events can be sent using this endpoint. This endpoint is - /// equivalent to calling `/rooms/{roomId}/state/{eventType}/{stateKey}` - /// with an empty `stateKey`. Previous state events with matching - /// `<roomId>` and `<eventType>`, and empty `<stateKey>`, will be overwritten. - /// - /// 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. - /// - /// 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. - class SetRoomStateJob : public BaseJob - { - public: - /*! Send a state event to the given room. - * \param roomId - * The room to set the state in - * \param eventType - * The type of event to send. - * \param body - * State events can be sent using this endpoint. This endpoint is - * equivalent to calling `/rooms/{roomId}/state/{eventType}/{stateKey}` - * with an empty `stateKey`. Previous state events with matching - * `<roomId>` and `<eventType>`, and empty `<stateKey>`, will be overwritten. - * - * 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. - * - * 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. - */ - explicit SetRoomStateJob(const QString& roomId, const QString& eventType, const QJsonObject& body = {}); - ~SetRoomStateJob() override; - - // Result properties +/*! \brief Send a state event to the given room. + * + * State events can be sent using this endpoint. These events will be + * 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. + * + * 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](/client-server-api/#room-events) for the `m.` event + * specification. + * + * 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 + * being removed or are already present in the state event. + */ +class QUOTIENT_API SetRoomStateWithKeyJob : public BaseJob { +public: + /*! \brief Send a state event to the given room. + * + * \param roomId + * The room to set the state in + * + * \param eventType + * The type of event to send. + * + * \param stateKey + * The state_key for the state to send. Defaults to the empty string. When + * an empty string, the trailing slash on this endpoint is optional. + * + * \param body + * State events can be sent using this endpoint. These events will be + * 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. + * + * 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](/client-server-api/#room-events) for the `m.` event + * specification. + * + * 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 + * being removed or are already present in the state event. + */ + explicit SetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey, + const QJsonObject& body = {}); - /// A unique identifier for the event. - const QString& eventId() const; + // Result properties - protected: - Status parseJson(const QJsonDocument& data) override; + /// A unique identifier for the event. + QString eventId() const { return loadFromJson<QString>("event_id"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp new file mode 100644 index 00000000..b03fb6e8 --- /dev/null +++ b/lib/csapi/room_upgrades.cpp @@ -0,0 +1,17 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "room_upgrades.h" + +using namespace Quotient; + +UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) + : BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/upgrade")) +{ + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("new_version"), newVersion); + setRequestData({ _dataJson }); + addExpectedKey("replacement_room"); +} diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h new file mode 100644 index 00000000..0432f667 --- /dev/null +++ b/lib/csapi/room_upgrades.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Upgrades a room to a new room version. + * + * Upgrades the given room to a particular room version. + */ +class QUOTIENT_API UpgradeRoomJob : public BaseJob { +public: + /*! \brief Upgrades a room to a new room version. + * + * \param roomId + * The ID of the room to upgrade. + * + * \param newVersion + * The new version for the room. + */ + explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion); + + // Result properties + + /// The ID of the new room. + QString replacementRoom() const + { + return loadFromJson<QString>("replacement_room"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 3befeee5..563f4fa5 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -4,236 +4,93 @@ #include "rooms.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetOneRoomEventJob::Private -{ - public: - EventPtr data; -}; - -QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/event/" % eventId); -} - -static const auto GetOneRoomEventJobName = QStringLiteral("GetOneRoomEventJob"); - -GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, const QString& eventId) - : BaseJob(HttpVerb::Get, GetOneRoomEventJobName, - basePath % "/rooms/" % roomId % "/event/" % eventId) - , d(new Private) -{ -} - -GetOneRoomEventJob::~GetOneRoomEventJob() = default; - -EventPtr&& GetOneRoomEventJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<EventPtr>(data); - return Success; -} - -class GetRoomStateWithKeyJob::Private -{ - public: - StateEventPtr data; -}; - -QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey) +QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey); -} - -static const auto GetRoomStateWithKeyJobName = QStringLiteral("GetRoomStateWithKeyJob"); - -GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) - : BaseJob(HttpVerb::Get, GetRoomStateWithKeyJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) - , d(new Private) -{ + makePath("/_matrix/client/v3", "/rooms/", + roomId, "/event/", eventId)); } -GetRoomStateWithKeyJob::~GetRoomStateWithKeyJob() = default; - -StateEventPtr&& GetRoomStateWithKeyJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<StateEventPtr>(data); - return Success; -} +GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, + const QString& eventId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/event/", + eventId)) +{} -class GetRoomStateByTypeJob::Private -{ - public: - StateEventPtr data; -}; - -QUrl GetRoomStateByTypeJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType) +QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventType, + const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/state/" % eventType); -} - -static const auto GetRoomStateByTypeJobName = QStringLiteral("GetRoomStateByTypeJob"); - -GetRoomStateByTypeJob::GetRoomStateByTypeJob(const QString& roomId, const QString& eventType) - : BaseJob(HttpVerb::Get, GetRoomStateByTypeJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType) - , d(new Private) -{ + makePath("/_matrix/client/v3", "/rooms/", + roomId, "/state/", eventType, "/", + stateKey)); } -GetRoomStateByTypeJob::~GetRoomStateByTypeJob() = default; - -StateEventPtr&& GetRoomStateByTypeJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetRoomStateByTypeJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<StateEventPtr>(data); - return Success; -} - -class GetRoomStateJob::Private -{ - public: - StateEvents data; -}; +GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", + eventType, "/", stateKey)) +{} QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/state"); + makePath("/_matrix/client/v3", "/rooms/", + roomId, "/state")); } -static const auto GetRoomStateJobName = QStringLiteral("GetRoomStateJob"); - GetRoomStateJob::GetRoomStateJob(const QString& roomId) - : BaseJob(HttpVerb::Get, GetRoomStateJobName, - basePath % "/rooms/" % roomId % "/state") - , d(new Private) -{ -} - -GetRoomStateJob::~GetRoomStateJob() = default; + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state")) +{} -StateEvents&& GetRoomStateJob::data() +auto queryToGetMembersByRoom(const QString& at, const QString& membership, + const QString& notMembership) { - return std::move(d->data); + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("at"), at); + addParam<IfNotEmpty>(_q, QStringLiteral("membership"), membership); + addParam<IfNotEmpty>(_q, QStringLiteral("not_membership"), notMembership); + return _q; } -BaseJob::Status GetRoomStateJob::parseJson(const QJsonDocument& data) +QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& at, + const QString& membership, + const QString& notMembership) { - d->data = fromJson<StateEvents>(data); - return Success; + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"), + queryToGetMembersByRoom(at, membership, notMembership)); } -class GetMembersByRoomJob::Private -{ - public: - EventsArray<RoomMemberEvent> chunk; -}; +GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, + const QString& at, + const QString& membership, + const QString& notMembership) + : BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"), + queryToGetMembersByRoom(at, membership, notMembership)) +{} -QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, + const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/members"); + makePath("/_matrix/client/v3", "/rooms/", + roomId, "/joined_members")); } -static const auto GetMembersByRoomJobName = QStringLiteral("GetMembersByRoomJob"); - -GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Get, GetMembersByRoomJobName, - basePath % "/rooms/" % roomId % "/members") - , d(new Private) -{ -} - -GetMembersByRoomJob::~GetMembersByRoomJob() = default; - -EventsArray<RoomMemberEvent>&& GetMembersByRoomJob::chunk() -{ - return std::move(d->chunk); -} - -BaseJob::Status GetMembersByRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->chunk = fromJson<EventsArray<RoomMemberEvent>>(json.value("chunk"_ls)); - return Success; -} - -namespace QMatrixClient -{ - // Converters - - template <> struct FromJsonObject<GetJoinedMembersByRoomJob::RoomMember> - { - GetJoinedMembersByRoomJob::RoomMember operator()(const QJsonObject& jo) const - { - GetJoinedMembersByRoomJob::RoomMember result; - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class GetJoinedMembersByRoomJob::Private -{ - public: - QHash<QString, RoomMember> joined; -}; - -QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/joined_members"); -} - -static const auto GetJoinedMembersByRoomJobName = QStringLiteral("GetJoinedMembersByRoomJob"); - GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Get, GetJoinedMembersByRoomJobName, - basePath % "/rooms/" % roomId % "/joined_members") - , d(new Private) -{ -} - -GetJoinedMembersByRoomJob::~GetJoinedMembersByRoomJob() = default; - -const QHash<QString, GetJoinedMembersByRoomJob::RoomMember>& GetJoinedMembersByRoomJob::joined() const -{ - return d->joined; -} - -BaseJob::Status GetJoinedMembersByRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->joined = fromJson<QHash<QString, RoomMember>>(json.value("joined"_ls)); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, + "/joined_members")) +{} diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 2366918b..7823a1b0 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -4,257 +4,209 @@ #pragma once +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" -#include "events/roommemberevent.h" -#include "events/eventloader.h" -#include <QtCore/QHash> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Get a single event by event ID. - /// - /// 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 - { - public: - /*! Get a single event by event ID. - * \param roomId - * The ID of the room the event is in. - * \param eventId - * The event ID to get. - */ - explicit GetOneRoomEventJob(const QString& roomId, const QString& eventId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetOneRoomEventJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId); - - ~GetOneRoomEventJob() override; - - // Result properties - - /// The full event. - EventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get the state identified by the type and key. - /// - /// 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 - /// taken from the state of the room when they left. - class GetRoomStateWithKeyJob : public BaseJob - { - public: - /*! Get the state identified by the type and key. - * \param roomId - * The room to look up the state in. - * \param eventType - * The type of state to look up. - * \param stateKey - * The key of the state to look up. - */ - explicit GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomStateWithKeyJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey); - - ~GetRoomStateWithKeyJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get the state identified by the type, with the empty state key. - /// - /// 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 - /// taken from the state of the room when they left. - /// - /// This looks up the state event with the empty state key. - class GetRoomStateByTypeJob : public BaseJob - { - public: - /*! Get the state identified by the type, with the empty state key. - * \param roomId - * The room to look up the state in. - * \param eventType - * The type of state to look up. - */ - explicit GetRoomStateByTypeJob(const QString& roomId, const QString& eventType); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomStateByTypeJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType); - - ~GetRoomStateByTypeJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; +/*! \brief Get a single event by event ID. + * + * 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 QUOTIENT_API GetOneRoomEventJob : public BaseJob { +public: + /*! \brief Get a single event by event ID. + * + * \param roomId + * The ID of the room the event is in. + * + * \param eventId + * The event ID to get. + */ + explicit GetOneRoomEventJob(const QString& roomId, const QString& eventId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetOneRoomEventJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId); + + // Result properties + + /// The full event. + RoomEventPtr event() { return fromJson<RoomEventPtr>(jsonData()); } +}; + +/*! \brief Get the state identified by the type and key. + * + * 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 + * taken from the state of the room when they left. + */ +class QUOTIENT_API GetRoomStateWithKeyJob : public BaseJob { +public: + /*! \brief Get the state identified by the type and key. + * + * \param roomId + * The room to look up the state in. + * + * \param eventType + * The type of state to look up. + * + * \param stateKey + * The key of the state to look up. Defaults to an empty string. When + * an empty string, the trailing slash on this endpoint is optional. + */ + explicit GetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomStateWithKeyJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventType, + const QString& stateKey); +}; + +/*! \brief Get all state events in the current state of a room. + * + * Get the state events for the current state of a room. + */ +class QUOTIENT_API GetRoomStateJob : public BaseJob { +public: + /*! \brief Get all state events in the current state of a room. + * + * \param roomId + * The room to look up the state for. + */ + explicit GetRoomStateJob(const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomStateJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + // Result properties + + /// The current state of the room + StateEvents events() { return fromJson<StateEvents>(jsonData()); } +}; + +/*! \brief Get the m.room.member events for the room. + * + * Get the list of members for this room. + */ +class QUOTIENT_API GetMembersByRoomJob : public BaseJob { +public: + /*! \brief Get the m.room.member events for the room. + * + * \param roomId + * The room to get the member events for. + * + * \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 + * 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 + * parameters create an 'or' condition: either the membership *is* + * 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 + * filtering if unspecified. + */ + explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, + const QString& membership = {}, + const QString& notMembership = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetMembersByRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& at = {}, + const QString& membership = {}, + const QString& notMembership = {}); + + // Result properties - private: - class Private; - QScopedPointer<Private> d; + /// Get the list of members for this room. + StateEvents chunk() { return takeFromJson<StateEvents>("chunk"_ls); } +}; + +/*! \brief Gets the list of currently joined users and their profile data. + * + * This API returns a map of MXIDs to member info objects for members of 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 efficiently on the server. + */ +class QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob { +public: + // Inner data structures + + /// This API returns a map of MXIDs to member info objects for members of + /// 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 + /// 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. + QUrl avatarUrl; }; - /// Get all state events in the current state of a room. - /// - /// Get the state events for the current state of a room. - class GetRoomStateJob : public BaseJob - { - public: - /*! Get all state events in the current state of a room. - * \param roomId - * The room to look up the state for. - */ - explicit GetRoomStateJob(const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomStateJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - ~GetRoomStateJob() override; + // Construction/destruction - // Result properties + /*! \brief Gets the list of currently joined users and their profile data. + * + * \param roomId + * The room to get the members of. + */ + explicit GetJoinedMembersByRoomJob(const QString& roomId); - /// If the user is a member of the room this will be the - /// current state of the room as a list of events. If the user - /// has left the room then this will be the state of the room - /// when they left as a list of events. - StateEvents&& data(); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetJoinedMembersByRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - protected: - Status parseJson(const QJsonDocument& data) override; + // Result properties - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get the m.room.member events for the room. - /// - /// Get the list of members for this room. - class GetMembersByRoomJob : public BaseJob + /// A map from user ID to a RoomMember object. + QHash<QString, RoomMember> joined() const { - public: - /*! Get the m.room.member events for the room. - * \param roomId - * The room to get the member events for. - */ - explicit GetMembersByRoomJob(const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetMembersByRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - ~GetMembersByRoomJob() override; - - // Result properties - - /// Get the list of members for this room. - EventsArray<RoomMemberEvent>&& chunk(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Gets the list of currently joined users and their profile data. - /// - /// This API returns a map of MXIDs to member info objects for members of 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 efficiently on the server. - class GetJoinedMembersByRoomJob : public BaseJob + return loadFromJson<QHash<QString, RoomMember>>("joined"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetJoinedMembersByRoomJob::RoomMember> { + static void fillFrom(const QJsonObject& jo, + GetJoinedMembersByRoomJob::RoomMember& result) { - public: - // Inner data structures - - /// This API returns a map of MXIDs to member info objects for members of 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 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; - }; - - // Construction/destruction - - /*! Gets the list of currently joined users and their profile data. - * \param roomId - * The room to get the members of. - */ - explicit GetJoinedMembersByRoomJob(const QString& roomId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + } +}; - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetJoinedMembersByRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - ~GetJoinedMembersByRoomJob() override; - - // Result properties - - /// A map from user ID to a RoomMember object. - const QHash<QString, RoomMember>& joined() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 9436eb47..4e2c9e92 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -4,202 +4,23 @@ #include "search.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient +auto queryToSearch(const QString& nextBatch) { - // Converters - - QJsonObject toJson(const SearchJob::IncludeEventContext& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile); - return jo; - } - - QJsonObject toJson(const SearchJob::Group& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); - return jo; - } - - QJsonObject toJson(const SearchJob::Groupings& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); - return jo; - } - - QJsonObject toJson(const SearchJob::RoomEventsCriteria& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); - addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); - addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); - addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); - addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext); - addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState); - addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); - return jo; - } - - QJsonObject toJson(const SearchJob::Categories& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); - return jo; - } - - template <> struct FromJsonObject<SearchJob::UserProfile> - { - SearchJob::UserProfile operator()(const QJsonObject& jo) const - { - SearchJob::UserProfile result; - result.displayname = - fromJson<QString>(jo.value("displayname"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<SearchJob::EventContext> - { - SearchJob::EventContext operator()(const QJsonObject& jo) const - { - SearchJob::EventContext result; - result.begin = - fromJson<QString>(jo.value("start"_ls)); - result.end = - fromJson<QString>(jo.value("end"_ls)); - result.profileInfo = - fromJson<QHash<QString, SearchJob::UserProfile>>(jo.value("profile_info"_ls)); - result.eventsBefore = - fromJson<RoomEvents>(jo.value("events_before"_ls)); - result.eventsAfter = - fromJson<RoomEvents>(jo.value("events_after"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<SearchJob::Result> - { - SearchJob::Result operator()(const QJsonObject& jo) const - { - SearchJob::Result result; - result.rank = - fromJson<double>(jo.value("rank"_ls)); - result.result = - fromJson<RoomEventPtr>(jo.value("result"_ls)); - result.context = - fromJson<SearchJob::EventContext>(jo.value("context"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<SearchJob::GroupValue> - { - SearchJob::GroupValue operator()(const QJsonObject& jo) const - { - SearchJob::GroupValue result; - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - result.order = - fromJson<int>(jo.value("order"_ls)); - result.results = - fromJson<QStringList>(jo.value("results"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<SearchJob::ResultRoomEvents> - { - SearchJob::ResultRoomEvents operator()(const QJsonObject& jo) const - { - SearchJob::ResultRoomEvents result; - result.count = - fromJson<int>(jo.value("count"_ls)); - result.highlights = - fromJson<QStringList>(jo.value("highlights"_ls)); - result.results = - fromJson<std::vector<SearchJob::Result>>(jo.value("results"_ls)); - result.state = - fromJson<std::unordered_map<QString, StateEvents>>(jo.value("state"_ls)); - result.groups = - fromJson<QHash<QString, QHash<QString, SearchJob::GroupValue>>>(jo.value("groups"_ls)); - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - - return result; - } - }; - - template <> struct FromJsonObject<SearchJob::ResultCategories> - { - SearchJob::ResultCategories operator()(const QJsonObject& jo) const - { - SearchJob::ResultCategories result; - result.roomEvents = - fromJson<SearchJob::ResultRoomEvents>(jo.value("room_events"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class SearchJob::Private -{ - public: - ResultCategories searchCategories; -}; - -BaseJob::Query queryToSearch(const QString& nextBatch) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch); return _q; } -static const auto SearchJobName = QStringLiteral("SearchJob"); - -SearchJob::SearchJob(const Categories& searchCategories, const QString& nextBatch) - : BaseJob(HttpVerb::Post, SearchJobName, - basePath % "/search", - queryToSearch(nextBatch)) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("search_categories"), searchCategories); - setRequestData(_data); -} - -SearchJob::~SearchJob() = default; - -const SearchJob::ResultCategories& SearchJob::searchCategories() const -{ - return d->searchCategories; -} - -BaseJob::Status SearchJob::parseJson(const QJsonDocument& data) +SearchJob::SearchJob(const Categories& searchCategories, + const QString& nextBatch) + : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"), + makePath("/_matrix/client/v3", "/search"), + queryToSearch(nextBatch)) { - auto json = data.object(); - if (!json.contains("search_categories"_ls)) - return { JsonParseError, - "The key 'search_categories' not found in the response" }; - d->searchCategories = fromJson<ResultCategories>(json.value("search_categories"_ls)); - return Success; + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("search_categories"), searchCategories); + setRequestData({ _dataJson }); + addExpectedKey("search_categories"); } - diff --git a/lib/csapi/search.h b/lib/csapi/search.h index 85b0886b..30095f32 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -4,202 +4,304 @@ #pragma once +#include "csapi/definitions/room_event_filter.h" + +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" -#include "csapi/definitions/room_event_filter.h" -#include "converters.h" -#include <QtCore/QVector> -#include "events/eventloader.h" -#include <unordered_map> -#include <QtCore/QHash> - -namespace QMatrixClient -{ - // Operations - - /// Perform a server-side search. - /// +namespace Quotient { + +/*! \brief Perform a server-side search. + * + * Performs a full text search across different categories. + */ +class QUOTIENT_API SearchJob : public BaseJob { +public: + // Inner data structures + + /// Configures whether any context for the events + /// returned are included in the response. + struct IncludeEventContext { + /// How many events before the result are + /// returned. By default, this is `5`. + Omittable<int> beforeLimit; + /// How many events after the result are + /// 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`. + Omittable<bool> includeProfile; + }; + + /// Configuration for group. + struct Group { + /// Key that defines the group. + QString key; + }; + + /// Requests that the server partitions the result set + /// based on the provided list of keys. + struct Groupings { + /// List of groups to request. + QVector<Group> groupBy; + }; + + /// Mapping of category name to search criteria. + struct RoomEventsCriteria { + /// The string to search events for + QString searchTerm; + /// The keys to search. Defaults to all. + QStringList keys; + /// This takes a [filter](/client-server-api/#filtering). + RoomEventFilter filter; + /// The order in which to search for results. + /// By default, this is `"rank"`. + QString orderBy; + /// Configures whether any context for the events + /// returned are included in the response. + Omittable<IncludeEventContext> eventContext; + /// Requests the server return the current state for + /// each room returned. + Omittable<bool> includeState; + /// Requests that the server partitions the result set + /// based on the provided list of keys. + Omittable<Groupings> groupings; + }; + + /// Describes which categories to search in and their criteria. + struct Categories { + /// Mapping of category name to search criteria. + Omittable<RoomEventsCriteria> roomEvents; + }; + /// Performs a full text search across different categories. - class SearchJob : public BaseJob - { - public: - // Inner data structures - - /// Configures whether any context for the events - /// returned are included in the response. - struct IncludeEventContext - { - /// How many events before the result are - /// returned. By default, this is ``5``. - Omittable<int> beforeLimit; - /// How many events after the result are - /// 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``. - bool includeProfile; - }; - - /// Configuration for group. - struct Group - { - /// Key that defines the group. - QString key; - }; - - /// Requests that the server partitions the result set - /// based on the provided list of keys. - struct Groupings - { - /// List of groups to request. - QVector<Group> groupBy; - }; - - /// Mapping of category name to search criteria. - struct RoomEventsCriteria - { - /// The string to search events for - QString searchTerm; - /// The keys to search. Defaults to all. - QStringList keys; - /// This takes a `filter`_. - Omittable<RoomEventFilter> filter; - /// The order in which to search for results. - /// By default, this is ``"rank"``. - QString orderBy; - /// Configures whether any context for the events - /// returned are included in the response. - Omittable<IncludeEventContext> eventContext; - /// Requests the server return the current state for - /// each room returned. - bool includeState; - /// Requests that the server partitions the result set - /// based on the provided list of keys. - Omittable<Groupings> groupings; - }; - - /// Describes which categories to search in and their criteria. - struct Categories - { - /// Mapping of category name to search criteria. - Omittable<RoomEventsCriteria> roomEvents; - }; - - /// Performs a full text search across different categories. - struct UserProfile - { - /// Performs a full text search across different categories. - QString displayname; - /// Performs a full text search across different categories. - QString avatarUrl; - }; - - /// Context for result, if requested. - struct EventContext - { - /// Pagination token for the start of the chunk - QString begin; - /// Pagination token for the end of the chunk - QString end; - /// The historic profile information of the - /// users that sent the events returned. - /// - /// The ``string`` key is the user ID for which - /// the profile belongs to. - QHash<QString, UserProfile> profileInfo; - /// Events just before the result. - RoomEvents eventsBefore; - /// Events just after the result. - RoomEvents eventsAfter; - }; - - /// The result object. - struct Result - { - /// A number that describes how closely this result matches the search. Higher is closer. - Omittable<double> rank; - /// The event that matched. - RoomEventPtr result; - /// Context for result, if requested. - Omittable<EventContext> context; - }; - - /// The results for a particular group value. - struct GroupValue - { - /// Token that can be used to get the next batch - /// of results in the group, by passing as the - /// `next_batch` parameter to the next call. If - /// this field is absent, there are no more - /// results in this group. - QString nextBatch; - /// Key that can be used to order different - /// groups. - Omittable<int> order; - /// Which results are in this group. - QStringList results; - }; - - /// Mapping of category name to search criteria. - struct ResultRoomEvents - { - /// An approximate count of the total number of results found. - Omittable<int> count; - /// List of words which should be highlighted, useful for stemming which may change the query terms. - QStringList highlights; - /// List of results in the requested order. - 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``. - /// - /// The ``string`` key is the room ID for which the ``State - /// Event`` array belongs to. - std::unordered_map<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: - /// 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 - /// results, by passing as the `next_batch` parameter to - /// the next call. If this field is absent, there are no - /// more results. - QString nextBatch; - }; - - /// Describes which categories to search in and their criteria. - struct ResultCategories - { - /// Mapping of category name to search criteria. - Omittable<ResultRoomEvents> roomEvents; - }; - - // Construction/destruction - - /*! Perform a server-side search. - * \param searchCategories - * Describes which categories to search in and their criteria. - * \param nextBatch - * The point to return events from. If given, this should be a - * ``next_batch`` result from a previous call to this endpoint. - */ - explicit SearchJob(const Categories& searchCategories, const QString& nextBatch = {}); - ~SearchJob() override; - - // Result properties - - /// Describes which categories to search in and their criteria. - const ResultCategories& searchCategories() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct UserProfile { + /// Performs a full text search across different categories. + QString displayname; + /// Performs a full text search across different categories. + QUrl avatarUrl; + }; + + /// Context for result, if requested. + struct EventContext { + /// Pagination token for the start of the chunk + QString begin; + /// Pagination token for the end of the chunk + QString end; + /// The historic profile information of the + /// users that sent the events returned. + /// + /// The `string` key is the user ID for which + /// the profile belongs to. + QHash<QString, UserProfile> profileInfo; + /// Events just before the result. + RoomEvents eventsBefore; + /// Events just after the result. + RoomEvents eventsAfter; + }; + + /// The result object. + struct Result { + /// A number that describes how closely this result matches the search. + /// Higher is closer. + Omittable<double> rank; + /// The event that matched. + RoomEventPtr result; + /// Context for result, if requested. + Omittable<EventContext> context; }; -} // namespace QMatrixClient + + /// The results for a particular group value. + struct GroupValue { + /// Token that can be used to get the next batch + /// of results in the group, by passing as the + /// `next_batch` parameter to the next call. If + /// this field is absent, there are no more + /// results in this group. + QString nextBatch; + /// Key that can be used to order different + /// groups. + Omittable<int> order; + /// Which results are in this group. + QStringList results; + }; + + /// Mapping of category name to search criteria. + struct ResultRoomEvents { + /// An approximate count of the total number of results found. + Omittable<int> count; + /// List of words which should be highlighted, useful for stemming which + /// may change the query terms. + QStringList highlights; + /// List of results in the requested order. + 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`. + /// + /// 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: + /// 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 + /// results, by passing as the `next_batch` parameter to + /// the next call. If this field is absent, there are no + /// more results. + QString nextBatch; + }; + + /// Describes which categories to search in and their criteria. + struct ResultCategories { + /// Mapping of category name to search criteria. + Omittable<ResultRoomEvents> roomEvents; + }; + + // Construction/destruction + + /*! \brief Perform a server-side search. + * + * \param searchCategories + * Describes which categories to search in and their criteria. + * + * \param nextBatch + * The point to return events from. If given, this should be a + * `next_batch` result from a previous call to this endpoint. + */ + explicit SearchJob(const Categories& searchCategories, + const QString& nextBatch = {}); + + // Result properties + + /// Describes which categories to search in and their criteria. + ResultCategories searchCategories() const + { + return loadFromJson<ResultCategories>("search_categories"_ls); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::IncludeEventContext> { + static void dumpTo(QJsonObject& jo, + const SearchJob::IncludeEventContext& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), + pod.beforeLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), + pod.includeProfile); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Group> { + static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Groupings> { + static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::RoomEventsCriteria> { + static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) + { + addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); + addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); + addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); + addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); + addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), + pod.eventContext); + addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), + pod.includeState); + addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Categories> { + static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::UserProfile> { + static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) + { + fromJson(jo.value("displayname"_ls), result.displayname); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::EventContext> { + static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) + { + fromJson(jo.value("start"_ls), result.begin); + fromJson(jo.value("end"_ls), result.end); + fromJson(jo.value("profile_info"_ls), result.profileInfo); + fromJson(jo.value("events_before"_ls), result.eventsBefore); + fromJson(jo.value("events_after"_ls), result.eventsAfter); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Result> { + static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) + { + fromJson(jo.value("rank"_ls), result.rank); + fromJson(jo.value("result"_ls), result.result); + fromJson(jo.value("context"_ls), result.context); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::GroupValue> { + static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) + { + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("order"_ls), result.order); + fromJson(jo.value("results"_ls), result.results); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::ResultRoomEvents> { + static void fillFrom(const QJsonObject& jo, + SearchJob::ResultRoomEvents& result) + { + fromJson(jo.value("count"_ls), result.count); + fromJson(jo.value("highlights"_ls), result.highlights); + fromJson(jo.value("results"_ls), result.results); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("groups"_ls), result.groups); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::ResultCategories> { + static void fillFrom(const QJsonObject& jo, + SearchJob::ResultCategories& result) + { + fromJson(jo.value("room_events"_ls), result.roomEvents); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/space_hierarchy.cpp b/lib/csapi/space_hierarchy.cpp new file mode 100644 index 00000000..7b5c7eac --- /dev/null +++ b/lib/csapi/space_hierarchy.cpp @@ -0,0 +1,43 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "space_hierarchy.h" + +using namespace Quotient; + +auto queryToGetSpaceHierarchy(Omittable<bool> suggestedOnly, + Omittable<int> limit, Omittable<int> maxDepth, + const QString& from) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("suggested_only"), suggestedOnly); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("max_depth"), maxDepth); + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + return _q; +} + +QUrl GetSpaceHierarchyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + Omittable<bool> suggestedOnly, + Omittable<int> limit, + Omittable<int> maxDepth, + const QString& from) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"), + queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from)); +} + +GetSpaceHierarchyJob::GetSpaceHierarchyJob(const QString& roomId, + Omittable<bool> suggestedOnly, + Omittable<int> limit, + Omittable<int> maxDepth, + const QString& from) + : BaseJob(HttpVerb::Get, QStringLiteral("GetSpaceHierarchyJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"), + queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from)) +{ + addExpectedKey("rooms"); +} diff --git a/lib/csapi/space_hierarchy.h b/lib/csapi/space_hierarchy.h new file mode 100644 index 00000000..e5da6df2 --- /dev/null +++ b/lib/csapi/space_hierarchy.h @@ -0,0 +1,152 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/stateevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Retrieve a portion of a space tree. + * + * Paginates over the space tree in a depth-first manner to locate child rooms + * of a given space. + * + * Where a child room is unknown to the local server, federation is used to fill + * in the details. The servers listed in the `via` array should be contacted to + * attempt to fill in missing rooms. + * + * Only [`m.space.child`](#mspacechild) state events of the room are considered. + * Invalid child rooms and parent events are not covered by this endpoint. + */ +class QUOTIENT_API GetSpaceHierarchyJob : public BaseJob { +public: + // Inner data structures + + /// Paginates over the space tree in a depth-first manner to locate child + /// rooms of a given space. + /// + /// Where a child room is unknown to the local server, federation is used to + /// fill in the details. The servers listed in the `via` array should be + /// contacted to attempt to fill in missing rooms. + /// + /// Only [`m.space.child`](#mspacechild) state events of the room are + /// considered. Invalid child rooms and parent events are not covered by + /// this endpoint. + struct ChildRoomsChunk { + /// The canonical alias of the room, if any. + QString canonicalAlias; + /// The name of the room, if any. + QString name; + /// The number of members joined to the room. + int numJoinedMembers; + /// The ID of the room. + QString roomId; + /// The topic of the room, if any. + QString topic; + /// Whether the room may be viewed by guest users without joining. + bool worldReadable; + /// Whether guest users may join the room and participate in it. + /// If they can, they will be subject to ordinary power level + /// rules like any other user. + bool guestCanJoin; + /// The URL for the room's avatar, if one is set. + QUrl avatarUrl; + /// The room's join rule. When not present, the room is assumed to + /// be `public`. + QString joinRule; + /// The `type` of room (from + /// [`m.room.create`](/client-server-api/#mroomcreate)), if any. + QString roomType; + /// The [`m.space.child`](#mspacechild) events of the space-room, + /// represented as [Stripped State Events](#stripped-state) with an + /// added `origin_server_ts` key. + /// + /// If the room is not a space-room, this should be empty. + StateEvents childrenState; + }; + + // Construction/destruction + + /*! \brief Retrieve a portion of a space tree. + * + * \param roomId + * The room ID of the space to get a hierarchy for. + * + * \param suggestedOnly + * Optional (default `false`) flag to indicate whether or not the server + * should only consider suggested rooms. Suggested rooms are annotated in + * their [`m.space.child`](#mspacechild) event contents. + * + * \param limit + * Optional limit for the maximum number of rooms to include per response. + * Must be an integer greater than zero. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param maxDepth + * Optional limit for how far to go into the space. Must be a non-negative + * integer. + * + * When reached, no further child rooms will be returned. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param from + * A pagination token from a previous result. If specified, `max_depth` + * and `suggested_only` cannot be changed from the first request. + */ + explicit GetSpaceHierarchyJob(const QString& roomId, + Omittable<bool> suggestedOnly = none, + Omittable<int> limit = none, + Omittable<int> maxDepth = none, + const QString& from = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetSpaceHierarchyJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + Omittable<bool> suggestedOnly = none, + Omittable<int> limit = none, + Omittable<int> maxDepth = none, + const QString& from = {}); + + // Result properties + + /// The rooms for the current page, with the current filters. + std::vector<ChildRoomsChunk> rooms() + { + return takeFromJson<std::vector<ChildRoomsChunk>>("rooms"_ls); + } + + /// A token to supply to `from` to keep paginating the responses. Not + /// present when there are no further results. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } +}; + +template <> +struct JsonObjectConverter<GetSpaceHierarchyJob::ChildRoomsChunk> { + static void fillFrom(const QJsonObject& jo, + GetSpaceHierarchyJob::ChildRoomsChunk& result) + { + fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); + fromJson(jo.value("name"_ls), result.name); + fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("topic"_ls), result.topic); + fromJson(jo.value("world_readable"_ls), result.worldReadable); + fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + fromJson(jo.value("join_rule"_ls), result.joinRule); + fromJson(jo.value("room_type"_ls), result.roomType); + fromJson(jo.value("children_state"_ls), result.childrenState); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp new file mode 100644 index 00000000..71f8147c --- /dev/null +++ b/lib/csapi/sso_login_redirect.cpp @@ -0,0 +1,51 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "sso_login_redirect.h" + +using namespace Quotient; + +auto queryToRedirectToSSO(const QString& redirectUrl) +{ + QUrlQuery _q; + addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); + return _q; +} + +QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v3", + "/login/sso/redirect"), + queryToRedirectToSSO(redirectUrl)); +} + +RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) + : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"), + makePath("/_matrix/client/v3", "/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/v3", + "/login/sso/redirect/", idpId), + queryToRedirectToIdP(redirectUrl)); +} + +RedirectToIdPJob::RedirectToIdPJob(const QString& idpId, + const QString& redirectUrl) + : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"), + makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId), + queryToRedirectToIdP(redirectUrl), {}, false) +{} diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h new file mode 100644 index 00000000..f4f81c1e --- /dev/null +++ b/lib/csapi/sso_login_redirect.h @@ -0,0 +1,70 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Redirect the user's browser to the SSO interface. + * + * A web-based Matrix client should instruct the user's browser to + * navigate to this endpoint in order to log in via SSO. + * + * The server MUST respond with an HTTP redirect to the SSO interface, + * or present a page which lets the user select an IdP to continue + * with in the event multiple are supported by the server. + */ +class QUOTIENT_API RedirectToSSOJob : public BaseJob { +public: + /*! \brief Redirect the user's browser to the SSO interface. + * + * \param redirectUrl + * URI to which the user will be redirected after the homeserver has + * authenticated the user with SSO. + */ + explicit RedirectToSSOJob(const QString& redirectUrl); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for RedirectToSSOJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); +}; + +/*! \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 QUOTIENT_API 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 808915ac..2c85842d 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -4,89 +4,47 @@ #include "tags.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct FromJsonObject<GetRoomTagsJob::Tag> - { - GetRoomTagsJob::Tag operator()(QJsonObject jo) const - { - GetRoomTagsJob::Tag result; - result.order = - fromJson<float>(jo.take("order"_ls)); - - result.additionalProperties = fromJson<QVariantHash>(jo); - return result; - } - }; -} // namespace QMatrixClient - -class GetRoomTagsJob::Private -{ - public: - QHash<QString, Tag> tags; -}; - -QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId) +QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags"); + makePath("/_matrix/client/v3", "/user/", + userId, "/rooms/", roomId, "/tags")); } -static const auto GetRoomTagsJobName = QStringLiteral("GetRoomTagsJob"); - GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId) - : BaseJob(HttpVerb::Get, GetRoomTagsJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags") - , d(new Private) -{ -} - -GetRoomTagsJob::~GetRoomTagsJob() = default; - -const QHash<QString, GetRoomTagsJob::Tag>& GetRoomTagsJob::tags() const -{ - return d->tags; + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"), + makePath("/_matrix/client/v3", "/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"), + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) +{ + QJsonObject _dataJson; + fillJson(_dataJson, additionalProperties); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("order"), order); + setRequestData({ _dataJson }); } -BaseJob::Status GetRoomTagsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->tags = fromJson<QHash<QString, Tag>>(json.value("tags"_ls)); - return Success; -} - -static const auto SetRoomTagJobName = QStringLiteral("SetRoomTagJob"); - -SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable<float> order) - : BaseJob(HttpVerb::Put, SetRoomTagJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("order"), order); - setRequestData(_data); -} - -QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag) +QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId, const QString& tag) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag); -} - -static const auto DeleteRoomTagJobName = QStringLiteral("DeleteRoomTagJob"); - -DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag) - : BaseJob(HttpVerb::Delete, DeleteRoomTagJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag) -{ + makePath("/_matrix/client/v3", "/user/", + userId, "/rooms/", roomId, "/tags/", + tag)); } +DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, + const QString& tag) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"), + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) +{} diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h index 2c20c2a2..f4250674 100644 --- a/lib/csapi/tags.h +++ b/lib/csapi/tags.h @@ -6,111 +6,122 @@ #include "jobs/basejob.h" -#include <QtCore/QVariant> -#include <QtCore/QHash> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief List the tags for a room. + * + * List the tags set by a user on a room. + */ +class QUOTIENT_API GetRoomTagsJob : public BaseJob { +public: + // Inner data structures - /// List the tags for a room. - /// /// List the tags set by a user on a room. - class GetRoomTagsJob : public BaseJob - { - public: - // Inner data structures - - /// List the tags set by a user on a room. - struct Tag - { - /// 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. - QVariantHash additionalProperties; - }; - - // Construction/destruction - - /*! List the tags for a room. - * \param userId - * The id of the user to get tags for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to get tags for. - */ - explicit GetRoomTagsJob(const QString& userId, const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomTagsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId); - - ~GetRoomTagsJob() override; - - // Result properties - - /// List the tags set by a user on a room. - const QHash<QString, Tag>& tags() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct Tag { + /// 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. + QVariantHash additionalProperties; }; - /// Add a tag to a room. - /// - /// Add a tag to the room. - class SetRoomTagJob : public BaseJob - { - public: - /*! Add a tag to a room. - * \param userId - * The id of the user to add a tag for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to add a tag to. - * \param tag - * The tag to add. - * \param order - * A number in a range ``[0,1]`` describing a relative - * position of the room under the given tag. - */ - explicit SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable<float> order = none); - }; + // Construction/destruction + + /*! \brief List the tags for a room. + * + * \param userId + * The id of the user to get tags for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to get tags for. + */ + explicit GetRoomTagsJob(const QString& userId, const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomTagsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId); + + // Result properties - /// Remove a tag from the room. - /// - /// Remove a tag from the room. - class DeleteRoomTagJob : public BaseJob + /// List the tags set by a user on a room. + QHash<QString, Tag> tags() const { - public: - /*! Remove a tag from the room. - * \param userId - * The id of the user to remove a tag for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to remove a tag from. - * \param tag - * The tag to remove. - */ - explicit DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * DeleteRoomTagJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag); + return loadFromJson<QHash<QString, Tag>>("tags"_ls); + } +}; - }; -} // namespace QMatrixClient +template <> +struct JsonObjectConverter<GetRoomTagsJob::Tag> { + static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result) + { + fromJson(jo.take("order"_ls), result.order); + fromJson(jo, result.additionalProperties); + } +}; + +/*! \brief Add a tag to a room. + * + * Add a tag to the room. + */ +class QUOTIENT_API SetRoomTagJob : public BaseJob { +public: + /*! \brief Add a tag to a room. + * + * \param userId + * The id of the user to add a tag for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to add a tag to. + * + * \param tag + * The tag to add. + * + * \param order + * A number in a range `[0,1]` describing a relative + * position of the room under the given tag. + * + * \param additionalProperties + * Add a tag to the room. + */ + explicit SetRoomTagJob(const QString& userId, const QString& roomId, + const QString& tag, Omittable<float> order = none, + const QVariantHash& additionalProperties = {}); +}; + +/*! \brief Remove a tag from the room. + * + * Remove a tag from the room. + */ +class QUOTIENT_API DeleteRoomTagJob : public BaseJob { +public: + /*! \brief Remove a tag from the room. + * + * \param userId + * The id of the user to remove a tag for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to remove a tag from. + * + * \param tag + * The tag to remove. + */ + explicit DeleteRoomTagJob(const QString& userId, const QString& roomId, + const QString& tag); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for DeleteRoomTagJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId, const QString& tag); +}; + +} // namespace Quotient diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 3ba1a5ad..1e5870ce 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -4,177 +4,84 @@ #include "third_party_lookup.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetProtocolsJob::Private -{ - public: - QHash<QString, ThirdPartyProtocol> data; -}; +using namespace Quotient; QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/protocols"); + makePath("/_matrix/client/v3", + "/thirdparty/protocols")); } -static const auto GetProtocolsJobName = QStringLiteral("GetProtocolsJob"); - GetProtocolsJob::GetProtocolsJob() - : BaseJob(HttpVerb::Get, GetProtocolsJobName, - basePath % "/thirdparty/protocols") - , d(new Private) -{ -} + : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"), + makePath("/_matrix/client/v3", "/thirdparty/protocols")) +{} -GetProtocolsJob::~GetProtocolsJob() = default; - -const QHash<QString, ThirdPartyProtocol>& GetProtocolsJob::data() const -{ - return d->data; -} - -BaseJob::Status GetProtocolsJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<QHash<QString, ThirdPartyProtocol>>(data); - return Success; -} - -class GetProtocolMetadataJob::Private -{ - public: - ThirdPartyProtocol data; -}; - -QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, const QString& protocol) +QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, + const QString& protocol) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/protocol/" % protocol); + makePath("/_matrix/client/v3", + "/thirdparty/protocol/", protocol)); } -static const auto GetProtocolMetadataJobName = QStringLiteral("GetProtocolMetadataJob"); - GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) - : BaseJob(HttpVerb::Get, GetProtocolMetadataJobName, - basePath % "/thirdparty/protocol/" % protocol) - , d(new Private) -{ -} - -GetProtocolMetadataJob::~GetProtocolMetadataJob() = default; - -const ThirdPartyProtocol& GetProtocolMetadataJob::data() const -{ - return d->data; -} - -BaseJob::Status GetProtocolMetadataJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<ThirdPartyProtocol>(data); - return Success; -} - -class QueryLocationByProtocolJob::Private -{ - public: - QVector<ThirdPartyLocation> data; -}; + : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"), + makePath("/_matrix/client/v3", "/thirdparty/protocol/", protocol)) +{} -BaseJob::Query queryToQueryLocationByProtocol(const QString& searchFields) +auto queryToQueryLocationByProtocol(const QString& searchFields) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("searchFields"), searchFields); return _q; } -QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& searchFields) +QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, + const QString& protocol, + const QString& searchFields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/location/" % protocol, - queryToQueryLocationByProtocol(searchFields)); -} - -static const auto QueryLocationByProtocolJobName = QStringLiteral("QueryLocationByProtocolJob"); - -QueryLocationByProtocolJob::QueryLocationByProtocolJob(const QString& protocol, const QString& searchFields) - : BaseJob(HttpVerb::Get, QueryLocationByProtocolJobName, - basePath % "/thirdparty/location/" % protocol, - queryToQueryLocationByProtocol(searchFields)) - , d(new Private) -{ -} - -QueryLocationByProtocolJob::~QueryLocationByProtocolJob() = default; - -const QVector<ThirdPartyLocation>& QueryLocationByProtocolJob::data() const -{ - return d->data; -} - -BaseJob::Status QueryLocationByProtocolJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<QVector<ThirdPartyLocation>>(data); - return Success; + makePath("/_matrix/client/v3", + "/thirdparty/location/", protocol), + queryToQueryLocationByProtocol(searchFields)); } -class QueryUserByProtocolJob::Private -{ - public: - QVector<ThirdPartyUser> data; -}; +QueryLocationByProtocolJob::QueryLocationByProtocolJob( + const QString& protocol, const QString& searchFields) + : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"), + makePath("/_matrix/client/v3", "/thirdparty/location/", protocol), + queryToQueryLocationByProtocol(searchFields)) +{} -BaseJob::Query queryToQueryUserByProtocol(const QString& fields) +auto queryToQueryUserByProtocol(const QString& fields) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("fields..."), fields); return _q; } -QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& fields) +QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, + const QString& protocol, + const QString& fields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/user/" % protocol, - queryToQueryUserByProtocol(fields)); -} - -static const auto QueryUserByProtocolJobName = QStringLiteral("QueryUserByProtocolJob"); - -QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, const QString& fields) - : BaseJob(HttpVerb::Get, QueryUserByProtocolJobName, - basePath % "/thirdparty/user/" % protocol, - queryToQueryUserByProtocol(fields)) - , d(new Private) -{ + makePath("/_matrix/client/v3", + "/thirdparty/user/", protocol), + queryToQueryUserByProtocol(fields)); } -QueryUserByProtocolJob::~QueryUserByProtocolJob() = default; - -const QVector<ThirdPartyUser>& QueryUserByProtocolJob::data() const -{ - return d->data; -} +QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, + const QString& fields) + : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"), + makePath("/_matrix/client/v3", "/thirdparty/user/", protocol), + queryToQueryUserByProtocol(fields)) +{} -BaseJob::Status QueryUserByProtocolJob::parseJson(const QJsonDocument& data) +auto queryToQueryLocationByAlias(const QString& alias) { - d->data = fromJson<QVector<ThirdPartyUser>>(data); - return Success; -} - -class QueryLocationByAliasJob::Private -{ - public: - QVector<ThirdPartyLocation> data; -}; - -BaseJob::Query queryToQueryLocationByAlias(const QString& alias) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("alias"), alias); return _q; } @@ -182,42 +89,20 @@ BaseJob::Query queryToQueryLocationByAlias(const QString& alias) QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/location", - queryToQueryLocationByAlias(alias)); + makePath("/_matrix/client/v3", + "/thirdparty/location"), + queryToQueryLocationByAlias(alias)); } -static const auto QueryLocationByAliasJobName = QStringLiteral("QueryLocationByAliasJob"); - QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) - : BaseJob(HttpVerb::Get, QueryLocationByAliasJobName, - basePath % "/thirdparty/location", - queryToQueryLocationByAlias(alias)) - , d(new Private) -{ -} - -QueryLocationByAliasJob::~QueryLocationByAliasJob() = default; + : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"), + makePath("/_matrix/client/v3", "/thirdparty/location"), + queryToQueryLocationByAlias(alias)) +{} -const QVector<ThirdPartyLocation>& QueryLocationByAliasJob::data() const +auto queryToQueryUserByID(const QString& userid) { - return d->data; -} - -BaseJob::Status QueryLocationByAliasJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<QVector<ThirdPartyLocation>>(data); - return Success; -} - -class QueryUserByIDJob::Private -{ - public: - QVector<ThirdPartyUser> data; -}; - -BaseJob::Query queryToQueryUserByID(const QString& userid) -{ - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("userid"), userid); return _q; } @@ -225,30 +110,13 @@ BaseJob::Query queryToQueryUserByID(const QString& userid) QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/user", - queryToQueryUserByID(userid)); + makePath("/_matrix/client/v3", + "/thirdparty/user"), + queryToQueryUserByID(userid)); } -static const auto QueryUserByIDJobName = QStringLiteral("QueryUserByIDJob"); - QueryUserByIDJob::QueryUserByIDJob(const QString& userid) - : BaseJob(HttpVerb::Get, QueryUserByIDJobName, - basePath % "/thirdparty/user", - queryToQueryUserByID(userid)) - , d(new Private) -{ -} - -QueryUserByIDJob::~QueryUserByIDJob() = default; - -const QVector<ThirdPartyUser>& QueryUserByIDJob::data() const -{ - return d->data; -} - -BaseJob::Status QueryUserByIDJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<QVector<ThirdPartyUser>>(data); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"), + makePath("/_matrix/client/v3", "/thirdparty/user"), + queryToQueryUserByID(userid)) +{} diff --git a/lib/csapi/third_party_lookup.h b/lib/csapi/third_party_lookup.h index 3a60432b..30c5346e 100644 --- a/lib/csapi/third_party_lookup.h +++ b/lib/csapi/third_party_lookup.h @@ -4,238 +4,209 @@ #pragma once -#include "jobs/basejob.h" - -#include "csapi/../application-service/definitions/user.h" #include "csapi/../application-service/definitions/location.h" -#include <QtCore/QHash> -#include <QtCore/QVector> -#include "converters.h" #include "csapi/../application-service/definitions/protocol.h" +#include "csapi/../application-service/definitions/user.h" -namespace QMatrixClient -{ - // Operations - - /// Retrieve metadata about all protocols that a homeserver supports. - /// - /// Fetches the overall metadata about protocols supported by the - /// homeserver. Includes both the available protocols and all fields - /// required for queries against each protocol. - class GetProtocolsJob : public BaseJob - { - public: - explicit GetProtocolsJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetProtocolsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetProtocolsJob() override; +#include "jobs/basejob.h" - // Result properties +namespace Quotient { - /// The protocols supported by the homeserver. - const QHash<QString, ThirdPartyProtocol>& data() const; +/*! \brief Retrieve metadata about all protocols that a homeserver supports. + * + * Fetches the overall metadata about protocols supported by the + * homeserver. Includes both the available protocols and all fields + * required for queries against each protocol. + */ +class QUOTIENT_API GetProtocolsJob : public BaseJob { +public: + /// Retrieve metadata about all protocols that a homeserver supports. + explicit GetProtocolsJob(); - protected: - Status parseJson(const QJsonDocument& data) override; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetProtocolsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - private: - class Private; - QScopedPointer<Private> d; - }; + // Result properties - /// Retrieve metadata about a specific protocol that the homeserver supports. - /// - /// Fetches the metadata from the homeserver about a particular third party protocol. - class GetProtocolMetadataJob : public BaseJob + /// The protocols supported by the homeserver. + QHash<QString, ThirdPartyProtocol> protocols() const { - public: - /*! Retrieve metadata about a specific protocol that the homeserver supports. - * \param protocol - * The name of the protocol. - */ - explicit GetProtocolMetadataJob(const QString& protocol); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetProtocolMetadataJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol); - - ~GetProtocolMetadataJob() override; - - // Result properties - - /// The protocol was found and metadata returned. - const ThirdPartyProtocol& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Retrieve Matrix-side portals rooms leading to a third party location. - /// - /// Requesting this endpoint with a valid protocol name results in a list - /// of successful mapping results in a JSON array. Each result contains - /// objects to represent the Matrix room or rooms that represent a portal - /// to this third party network. Each has the Matrix room alias string, - /// an identifier for the particular third party network protocol, and an - /// object containing the network-specific fields that comprise this - /// identifier. It should attempt to canonicalise the identifier as much - /// as reasonably possible given the network type. - class QueryLocationByProtocolJob : public BaseJob + return fromJson<QHash<QString, ThirdPartyProtocol>>(jsonData()); + } +}; + +/*! \brief Retrieve metadata about a specific protocol that the homeserver + * supports. + * + * Fetches the metadata from the homeserver about a particular third party + * protocol. + */ +class QUOTIENT_API GetProtocolMetadataJob : public BaseJob { +public: + /*! \brief Retrieve metadata about a specific protocol that the homeserver + * supports. + * + * \param protocol + * The name of the protocol. + */ + explicit GetProtocolMetadataJob(const QString& protocol); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetProtocolMetadataJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol); + + // Result properties + + /// The protocol was found and metadata returned. + ThirdPartyProtocol data() const + { + return fromJson<ThirdPartyProtocol>(jsonData()); + } +}; + +/*! \brief Retrieve Matrix-side portals rooms leading to a third party location. + * + * Requesting this endpoint with a valid protocol name results in a list + * of successful mapping results in a JSON array. Each result contains + * objects to represent the Matrix room or rooms that represent a portal + * to this third party network. Each has the Matrix room alias string, + * an identifier for the particular third party network protocol, and an + * object containing the network-specific fields that comprise this + * identifier. It should attempt to canonicalise the identifier as much + * as reasonably possible given the network type. + */ +class QUOTIENT_API QueryLocationByProtocolJob : public BaseJob { +public: + /*! \brief Retrieve Matrix-side portals rooms leading to a third party + * location. + * + * \param protocol + * The protocol used to communicate to the third party network. + * + * \param searchFields + * One or more custom fields to help identify the third party + * location. + */ + explicit QueryLocationByProtocolJob(const QString& protocol, + const QString& searchFields = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryLocationByProtocolJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, + const QString& searchFields = {}); + + // Result properties + + /// At least one portal room was found. + QVector<ThirdPartyLocation> data() const { - public: - /*! Retrieve Matrix-side portals rooms leading to a third party location. - * \param protocol - * The protocol used to communicate to the third party network. - * \param searchFields - * One or more custom fields to help identify the third party - * location. - */ - explicit QueryLocationByProtocolJob(const QString& protocol, const QString& searchFields = {}); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryLocationByProtocolJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& searchFields = {}); - - ~QueryLocationByProtocolJob() override; - - // Result properties - - /// At least one portal room was found. - const QVector<ThirdPartyLocation>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Retrieve the Matrix User ID of a corresponding third party user. - /// - /// Retrieve a Matrix User ID linked to a user on the third party service, given - /// a set of user parameters. - class QueryUserByProtocolJob : public BaseJob + return fromJson<QVector<ThirdPartyLocation>>(jsonData()); + } +}; + +/*! \brief Retrieve the Matrix User ID of a corresponding third party user. + * + * Retrieve a Matrix User ID linked to a user on the third party service, given + * a set of user parameters. + */ +class QUOTIENT_API QueryUserByProtocolJob : public BaseJob { +public: + /*! \brief Retrieve the Matrix User ID of a corresponding third party user. + * + * \param protocol + * The name of the protocol. + * + * \param fields + * One or more custom fields that are passed to the AS to help identify + * the user. + */ + explicit QueryUserByProtocolJob(const QString& protocol, + const QString& fields = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryUserByProtocolJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, + const QString& fields = {}); + + // Result properties + + /// The Matrix User IDs found with the given parameters. + QVector<ThirdPartyUser> data() const { - public: - /*! Retrieve the Matrix User ID of a corresponding third party user. - * \param protocol - * The name of the protocol. - * \param fields - * One or more custom fields that are passed to the AS to help identify the user. - */ - explicit QueryUserByProtocolJob(const QString& protocol, const QString& fields = {}); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryUserByProtocolJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& fields = {}); - - ~QueryUserByProtocolJob() override; - - // Result properties - - /// The Matrix User IDs found with the given parameters. - const QVector<ThirdPartyUser>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Reverse-lookup third party locations given a Matrix room alias. - /// - /// Retrieve an array of third party network locations from a Matrix room - /// alias. - class QueryLocationByAliasJob : public BaseJob + return fromJson<QVector<ThirdPartyUser>>(jsonData()); + } +}; + +/*! \brief Reverse-lookup third party locations given a Matrix room alias. + * + * Retrieve an array of third party network locations from a Matrix room + * alias. + */ +class QUOTIENT_API QueryLocationByAliasJob : public BaseJob { +public: + /*! \brief Reverse-lookup third party locations given a Matrix room alias. + * + * \param alias + * The Matrix room alias to look up. + */ + explicit QueryLocationByAliasJob(const QString& alias); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryLocationByAliasJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& alias); + + // Result properties + + /// All found third party locations. + QVector<ThirdPartyLocation> data() const { - public: - /*! Reverse-lookup third party locations given a Matrix room alias. - * \param alias - * The Matrix room alias to look up. - */ - explicit QueryLocationByAliasJob(const QString& alias); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryLocationByAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& alias); - - ~QueryLocationByAliasJob() override; - - // Result properties - - /// All found third party locations. - const QVector<ThirdPartyLocation>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Reverse-lookup third party users given a Matrix User ID. - /// - /// Retrieve an array of third party users from a Matrix User ID. - class QueryUserByIDJob : public BaseJob + return fromJson<QVector<ThirdPartyLocation>>(jsonData()); + } +}; + +/*! \brief Reverse-lookup third party users given a Matrix User ID. + * + * Retrieve an array of third party users from a Matrix User ID. + */ +class QUOTIENT_API QueryUserByIDJob : public BaseJob { +public: + /*! \brief Reverse-lookup third party users given a Matrix User ID. + * + * \param userid + * The Matrix User ID to look up. + */ + explicit QueryUserByIDJob(const QString& userid); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryUserByIDJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userid); + + // Result properties + + /// An array of third party users. + QVector<ThirdPartyUser> data() const { - public: - /*! Reverse-lookup third party users given a Matrix User ID. - * \param userid - * The Matrix User ID to look up. - */ - explicit QueryUserByIDJob(const QString& userid); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryUserByIDJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userid); - - ~QueryUserByIDJob() override; - - // Result properties - - /// An array of third party users. - const QVector<ThirdPartyUser>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return fromJson<QVector<ThirdPartyUser>>(jsonData()); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index c1683338..3ca986c7 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -4,24 +4,18 @@ #include "third_party_membership.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto InviteBy3PIDJobName = QStringLiteral("InviteBy3PIDJob"); - -InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address) - : BaseJob(HttpVerb::Post, InviteBy3PIDJobName, - basePath % "/rooms/" % roomId % "/invite") +InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, + const QString& idAccessToken, + const QString& medium, const QString& address) + : BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index d18fe554..1129a9a8 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -6,70 +6,78 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Invite a user to participate in a particular room. + * + * *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](/client-server-api/#post_matrixclientv3roomsroomidinvite). + * + * This API invites a user to participate in a particular room. + * They do not start participating in the room until they actually join the + * room. + * + * Only users currently in a particular room can invite other users to + * 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` + * event to the room. + * + * If the identity server does not know a Matrix user identifier for the + * passed third party identifier, the homeserver will issue an invitation + * 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, + * containing that token. + * + * When the invitee binds the invited third party identifier to a Matrix + * user ID, the identity server will give the user a list of pending + * invitations, each containing: + * + * - The room ID to which they were invited + * + * - The token given to the homeserver + * + * - A signature of the token, signed with the identity server's private key + * + * - 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. + */ +class QUOTIENT_API InviteBy3PIDJob : public BaseJob { +public: + /*! \brief Invite a user to participate in a particular room. + * + * \param roomId + * The room identifier (not alias) to which to invite the user. + * + * \param idServer + * The hostname+port of the identity server which should be used for third + * party identifier lookups. + * + * \param idAccessToken + * 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. + * + * \param medium + * The kind of address being passed in the address field, for example + * `email`. + * + * \param address + * The invitee's third party identifier. + */ + explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, + const QString& idAccessToken, + const QString& medium, const QString& address); +}; - /// 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`_. - /// - /// This API invites a user to participate in a particular room. - /// They do not start participating in the room until they actually join the - /// room. - /// - /// Only users currently in a particular room can invite other users to - /// 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`` - /// event to the room. - /// - /// If the identity server does not know a Matrix user identifier for the - /// passed third party identifier, the homeserver will issue an invitation - /// 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, - /// containing that token. - /// - /// When the invitee binds the invited third party identifier to a Matrix - /// user ID, the identity server will give the user a list of pending - /// invitations, each containing: - /// - /// - The room ID to which they were invited - /// - /// - The token given to the homeserver - /// - /// - A signature of the token, signed with the identity server's private key - /// - /// - 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`_ - class InviteBy3PIDJob : public BaseJob - { - public: - /*! Invite a user to participate in a particular room. - * \param roomId - * The room identifier (not alias) to which to invite the user. - * \param idServer - * The hostname+port of the identity server which should be used for third party identifier lookups. - * \param medium - * The kind of address being passed in the address field, for example ``email``. - * \param address - * The invitee's third party identifier. - */ - explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/threads_list.cpp b/lib/csapi/threads_list.cpp new file mode 100644 index 00000000..26924f24 --- /dev/null +++ b/lib/csapi/threads_list.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "threads_list.h" + +using namespace Quotient; + +auto queryToGetThreadRoots(const QString& include, Omittable<int> limit, + const QString& from) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("include"), include); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + return _q; +} + +QUrl GetThreadRootsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& include, + Omittable<int> limit, const QString& from) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", + roomId, "/threads"), + queryToGetThreadRoots(include, limit, from)); +} + +GetThreadRootsJob::GetThreadRootsJob(const QString& roomId, + const QString& include, + Omittable<int> limit, const QString& from) + : BaseJob(HttpVerb::Get, QStringLiteral("GetThreadRootsJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/threads"), + queryToGetThreadRoots(include, limit, from)) +{ + addExpectedKey("chunk"); +} diff --git a/lib/csapi/threads_list.h b/lib/csapi/threads_list.h new file mode 100644 index 00000000..7041583a --- /dev/null +++ b/lib/csapi/threads_list.h @@ -0,0 +1,76 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/roomevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Retrieve a list of threads in a room, with optional filters. + * + * Paginates over the thread roots in a room, ordered by the `latest_event` of + * each thread root in its bundle. + */ +class QUOTIENT_API GetThreadRootsJob : public BaseJob { +public: + /*! \brief Retrieve a list of threads in a room, with optional filters. + * + * \param roomId + * The room ID where the thread roots are located. + * + * \param include + * Optional (default `all`) flag to denote which thread roots are of + * interest to the caller. When `all`, all thread roots found in the room + * are returned. When `participated`, only thread roots for threads the user + * has [participated + * in](/client-server-api/#server-side-aggreagtion-of-mthread-relationships) + * will be returned. + * + * \param limit + * Optional limit for the maximum number of thread roots to include per + * response. Must be an integer greater than zero. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param from + * A pagination token from a previous result. When not provided, the + * server starts paginating from the most recent event visible to the user + * (as per history visibility rules; topologically). + */ + explicit GetThreadRootsJob(const QString& roomId, + const QString& include = {}, + Omittable<int> limit = none, + const QString& from = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetThreadRootsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& include = {}, + Omittable<int> limit = none, + const QString& from = {}); + + // Result properties + + /// The thread roots, ordered by the `latest_event` in each event's + /// aggregation bundle. All events returned include bundled + /// [aggregations](/client-server-api/#aggregations). + /// + /// If the thread root event was sent by an [ignored + /// user](/client-server-api/#ignoring-users), the event is returned + /// redacted to the caller. This is to simulate the same behaviour of a + /// client doing aggregation locally on the thread. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// A token to supply to `from` to keep paginating the responses. Not + /// present when there are no further results. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 7c7f495a..e10fac69 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -4,22 +4,16 @@ #include "to_device.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SendToDeviceJobName = QStringLiteral("SendToDeviceJob"); - -SendToDeviceJob::SendToDeviceJob(const QString& eventType, const QString& txnId, const QHash<QString, QHash<QString, QJsonObject>>& messages) - : BaseJob(HttpVerb::Put, SendToDeviceJobName, - basePath % "/sendToDevice/" % eventType % "/" % txnId) +using namespace Quotient; + +SendToDeviceJob::SendToDeviceJob( + const QString& eventType, const QString& txnId, + const QHash<QString, QHash<QString, QJsonObject>>& messages) + : BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"), + makePath("/_matrix/client/v3", "/sendToDevice/", eventType, "/", + txnId)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("messages"), messages); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("messages"), messages); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index 10f6b971..54828337 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -6,32 +6,34 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> -#include <QtCore/QHash> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Send an event to a given set of devices. + * + * This endpoint is used to send send-to-device events to a set of + * client devices. + */ +class QUOTIENT_API SendToDeviceJob : public BaseJob { +public: + /*! \brief Send an event to a given set of devices. + * + * \param eventType + * The type of event to send. + * + * \param txnId + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate an ID unique across requests with the + * same access token; it will be used by the server to ensure idempotency of + * requests. + * + * \param messages + * The messages to send. A map from user ID, to a map from + * device ID to message body. The device ID may also be `*`, + * meaning all known devices for the user. + */ + explicit SendToDeviceJob( + const QString& eventType, const QString& txnId, + const QHash<QString, QHash<QString, QJsonObject>>& messages); +}; - /// Send an event to a given set of devices. - /// - /// This endpoint is used to send send-to-device events to a set of - /// client devices. - class SendToDeviceJob : public BaseJob - { - public: - /*! Send an event to a given set of devices. - * \param eventType - * The type of event to send. - * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. - * \param messages - * The messages to send. A map from user ID, to a map from - * device ID to message body. The device ID may also be `*`, - * meaning all known devices for the user. - */ - explicit SendToDeviceJob(const QString& eventType, const QString& txnId, const QHash<QString, QHash<QString, QJsonObject>>& messages = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp index bf10912b..21bd45ae 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -4,23 +4,16 @@ #include "typing.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetTypingJobName = QStringLiteral("SetTypingJob"); - -SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable<int> timeout) - : BaseJob(HttpVerb::Put, SetTypingJobName, - basePath % "/rooms/" % roomId % "/typing/" % userId) +SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, + bool typing, Omittable<int> timeout) + : BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/typing/", + userId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("typing"), typing); - addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("typing"), typing); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout); + setRequestData({ _dataJson }); } - diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h index c6201440..234e91b0 100644 --- a/lib/csapi/typing.h +++ b/lib/csapi/typing.h @@ -6,32 +6,34 @@ #include "jobs/basejob.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \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 + * user has stopped typing. + */ +class QUOTIENT_API SetTypingJob : public BaseJob { +public: + /*! \brief Informs the server that the user has started or stopped typing. + * + * \param userId + * The user who has started to type. + * + * \param roomId + * The room in which the user is typing. + * + * \param typing + * Whether the user is typing or not. If `false`, the `timeout` + * key can be omitted. + * + * \param timeout + * The length of time in milliseconds to mark this user as typing. + */ + explicit SetTypingJob(const QString& userId, const QString& roomId, + bool typing, Omittable<int> timeout = none); +}; - /// 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 - /// user has stopped typing. - class SetTypingJob : public BaseJob - { - public: - /*! Informs the server that the user has started or stopped typing. - * \param userId - * The user who has started to type. - * \param roomId - * The room in which the user is typing. - * \param typing - * Whether the user is typing or not. If ``false``, the ``timeout`` - * key can be omitted. - * \param timeout - * The length of time in milliseconds to mark this user as typing. - */ - explicit SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable<int> timeout = none); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index deb9cb8a..c65280ee 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -4,78 +4,17 @@ #include "users.h" -#include "converters.h" +using namespace Quotient; -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient +SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, + Omittable<int> limit) + : BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"), + makePath("/_matrix/client/v3", "/user_directory/search")) { - // Converters - - template <> struct FromJsonObject<SearchUserDirectoryJob::User> - { - SearchUserDirectoryJob::User operator()(const QJsonObject& jo) const - { - SearchUserDirectoryJob::User result; - result.userId = - fromJson<QString>(jo.value("user_id"_ls)); - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; - } - }; -} // namespace QMatrixClient - -class SearchUserDirectoryJob::Private -{ - public: - QVector<User> results; - bool limited; -}; - -static const auto SearchUserDirectoryJobName = QStringLiteral("SearchUserDirectoryJob"); - -SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, Omittable<int> limit) - : BaseJob(HttpVerb::Post, SearchUserDirectoryJobName, - basePath % "/user_directory/search") - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("search_term"), searchTerm); - addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit); - setRequestData(_data); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("search_term"), searchTerm); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("limit"), limit); + setRequestData({ _dataJson }); + addExpectedKey("results"); + addExpectedKey("limited"); } - -SearchUserDirectoryJob::~SearchUserDirectoryJob() = default; - -const QVector<SearchUserDirectoryJob::User>& SearchUserDirectoryJob::results() const -{ - return d->results; -} - -bool SearchUserDirectoryJob::limited() const -{ - return d->limited; -} - -BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("results"_ls)) - return { JsonParseError, - "The key 'results' not found in the response" }; - d->results = fromJson<QVector<User>>(json.value("results"_ls)); - if (!json.contains("limited"_ls)) - return { JsonParseError, - "The key 'limited' not found in the response" }; - d->limited = fromJson<bool>(json.value("limited"_ls)); - return Success; -} - diff --git a/lib/csapi/users.h b/lib/csapi/users.h index 1e355b8f..3c99758b 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -6,73 +6,78 @@ #include "jobs/basejob.h" -#include <QtCore/QVector> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Searches the user directory. + * + * Performs a search for users. The homeserver may + * determine which subset of users are searched, however the homeserver + * MUST at a minimum consider the users the requesting user shares a + * room with and those who reside in public rooms (known to the homeserver). + * The search MUST consider local users to the homeserver, and SHOULD + * query remote users as part of the search. + * + * 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. + */ +class QUOTIENT_API SearchUserDirectoryJob : public BaseJob { +public: + // Inner data structures - /// Searches the user directory. - /// - /// Performs a search for users on the homeserver. The homeserver may + /// Performs a search for users. The homeserver may /// determine which subset of users are searched, however the homeserver /// MUST at a minimum consider the users the requesting user shares a - /// room with and those who reside in public rooms (known to the homeserver). - /// The search MUST consider local users to the homeserver, and SHOULD - /// query remote users as part of the search. - /// + /// room with and those who reside in public rooms (known to the + /// homeserver). The search MUST consider local users to the homeserver, and + /// SHOULD query remote users as part of the search. + /// /// 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. - class SearchUserDirectoryJob : public BaseJob - { - public: - // Inner data structures + /// names preferably using a collation determined based upon the + /// `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. + QUrl avatarUrl; + }; - /// Performs a search for users on the homeserver. The homeserver may - /// determine which subset of users are searched, however the homeserver - /// MUST at a minimum consider the users the requesting user shares a - /// room with and those who reside in public rooms (known to the homeserver). - /// The search MUST consider local users to the homeserver, and SHOULD - /// query remote users as part of the search. - /// - /// 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. - 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; - }; + // Construction/destruction - // Construction/destruction + /*! \brief Searches the user directory. + * + * \param searchTerm + * The term to search for + * + * \param limit + * The maximum number of results to return. Defaults to 10. + */ + explicit SearchUserDirectoryJob(const QString& searchTerm, + Omittable<int> limit = none); - /*! Searches the user directory. - * \param searchTerm - * The term to search for - * \param limit - * The maximum number of results to return. Defaults to 10. - */ - explicit SearchUserDirectoryJob(const QString& searchTerm, Omittable<int> limit = none); - ~SearchUserDirectoryJob() override; + // Result properties - // Result properties + /// Ordered by rank and then whether or not profile info is available. + QVector<User> results() const + { + return loadFromJson<QVector<User>>("results"_ls); + } - /// Ordered by rank and then whether or not profile info is available. - const QVector<User>& results() const; - /// Indicates if the result list has been truncated by the limit. - bool limited() const; + /// Indicates if the result list has been truncated by the limit. + bool limited() const { return loadFromJson<bool>("limited"_ls); } +}; - protected: - Status parseJson(const QJsonDocument& data) override; +template <> +struct JsonObjectConverter<SearchUserDirectoryJob::User> { + static void fillFrom(const QJsonObject& jo, + SearchUserDirectoryJob::User& result) + { + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index 128902e2..a1efc33e 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -4,46 +4,17 @@ #include "versions.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client"); - -class GetVersionsJob::Private -{ - public: - QStringList versions; -}; +using namespace Quotient; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/versions"); + makePath("/_matrix/client", "/versions")); } -static const auto GetVersionsJobName = QStringLiteral("GetVersionsJob"); - GetVersionsJob::GetVersionsJob() - : BaseJob(HttpVerb::Get, GetVersionsJobName, - basePath % "/versions", false) - , d(new Private) -{ -} - -GetVersionsJob::~GetVersionsJob() = default; - -const QStringList& GetVersionsJob::versions() const + : BaseJob(HttpVerb::Get, QStringLiteral("GetVersionsJob"), + makePath("/_matrix/client", "/versions"), false) { - return d->versions; + addExpectedKey("versions"); } - -BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - d->versions = fromJson<QStringList>(json.value("versions"_ls)); - return Success; -} - diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 309de184..9f799cb0 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -6,44 +6,56 @@ #include "jobs/basejob.h" - -namespace QMatrixClient -{ - // Operations - - /// Gets the versions of the specification supported by the server. - /// +namespace Quotient { + +/*! \brief Gets the versions of the specification supported by the server. + * + * Gets the versions of the specification supported by the server. + * + * Values will take the form `vX.Y` or `rX.Y.Z` in historical cases. See + * [the Specification Versioning](../#specification-versions) for more + * information. + * + * The server may additionally advertise experimental features it supports + * through `unstable_features`. These features should be namespaced and + * may optionally include version information within their name if desired. + * Features listed here are not for optionally toggling parts of the Matrix + * specification and should only be used to advertise support for a feature + * which has not yet landed in the spec. For example, a feature currently + * undergoing the proposal process may appear here and eventually be taken + * off this list once the feature lands in the spec and the server deems it + * reasonable to do so. Servers may wish to keep advertising features here + * after they've been released into the spec to give clients a chance to + * upgrade appropriately. Additionally, clients should avoid using unstable + * features in their stable releases. + */ +class QUOTIENT_API GetVersionsJob : public BaseJob { +public: /// Gets the versions of the specification supported by the server. - /// - /// 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``. - class GetVersionsJob : public BaseJob - { - public: - explicit GetVersionsJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetVersionsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); + explicit GetVersionsJob(); - ~GetVersionsJob() override; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetVersionsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - // Result properties + // Result properties - /// The supported versions. - const QStringList& versions() const; + /// The supported versions. + QStringList versions() const + { + return loadFromJson<QStringList>("versions"_ls); + } - protected: - Status parseJson(const QJsonDocument& data) override; + /// Experimental features the server supports. Features not listed here, + /// or the lack of this property all together, indicate that a feature is + /// not supported. + QHash<QString, bool> unstableFeatures() const + { + return loadFromJson<QHash<QString, bool>>("unstable_features"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index 0479b645..1e1f2441 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -4,45 +4,15 @@ #include "voip.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetTurnServerJob::Private -{ - public: - QJsonObject data; -}; +using namespace Quotient; QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/voip/turnServer"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/v3", "/voip/turnServer")); } -static const auto GetTurnServerJobName = QStringLiteral("GetTurnServerJob"); - GetTurnServerJob::GetTurnServerJob() - : BaseJob(HttpVerb::Get, GetTurnServerJobName, - basePath % "/voip/turnServer") - , d(new Private) -{ -} - -GetTurnServerJob::~GetTurnServerJob() = default; - -const QJsonObject& GetTurnServerJob::data() const -{ - return d->data; -} - -BaseJob::Status GetTurnServerJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<QJsonObject>(data); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"), + makePath("/_matrix/client/v3", "/voip/turnServer")) +{} diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h index bb858499..38904f60 100644 --- a/lib/csapi/voip.h +++ b/lib/csapi/voip.h @@ -6,41 +6,29 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations +namespace Quotient { +/*! \brief Obtain TURN server credentials. + * + * This API provides credentials for the client to use when initiating + * calls. + */ +class QUOTIENT_API GetTurnServerJob : public BaseJob { +public: /// Obtain TURN server credentials. - /// - /// This API provides credentials for the client to use when initiating - /// calls. - class GetTurnServerJob : public BaseJob - { - public: - explicit GetTurnServerJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetTurnServerJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetTurnServerJob() override; - - // Result properties - - /// The TURN server credentials. - const QJsonObject& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + explicit GetTurnServerJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetTurnServerJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// The TURN server credentials. + QJsonObject data() const { return fromJson<QJsonObject>(jsonData()); } +}; + +} // namespace Quotient diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index d42534a0..0b441279 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -4,56 +4,15 @@ #include "wellknown.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/.well-known"); - -class GetWellknownJob::Private -{ - public: - HomeserverInformation homeserver; - Omittable<IdentityServerInformation> identityServer; -}; +using namespace Quotient; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/matrix/client"); + makePath("/.well-known", "/matrix/client")); } -static const auto GetWellknownJobName = QStringLiteral("GetWellknownJob"); - GetWellknownJob::GetWellknownJob() - : BaseJob(HttpVerb::Get, GetWellknownJobName, - basePath % "/matrix/client", false) - , d(new Private) -{ -} - -GetWellknownJob::~GetWellknownJob() = default; - -const HomeserverInformation& GetWellknownJob::homeserver() const -{ - return d->homeserver; -} - -const Omittable<IdentityServerInformation>& GetWellknownJob::identityServer() const -{ - return d->identityServer; -} - -BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("m.homeserver"_ls)) - return { JsonParseError, - "The key 'm.homeserver' not found in the response" }; - d->homeserver = fromJson<HomeserverInformation>(json.value("m.homeserver"_ls)); - d->identityServer = fromJson<IdentityServerInformation>(json.value("m.identity_server"_ls)); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetWellknownJob"), + makePath("/.well-known", "/matrix/client"), false) +{} diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index df4c8c6e..8615191c 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -4,53 +4,42 @@ #pragma once +#include "csapi/definitions/wellknown/full.h" + #include "jobs/basejob.h" -#include "converters.h" -#include "csapi/definitions/wellknown/identity_server.h" -#include "csapi/definitions/wellknown/homeserver.h" +namespace Quotient { + +/*! \brief Gets Matrix server discovery information about the domain. + * + * 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 + * suitably namespaced for each application and reduces the risk of + * clashes. + * + * Note that this endpoint is not necessarily handled by the homeserver, + * but by another webserver, to be used for discovering the homeserver URL. + */ +class QUOTIENT_API GetWellknownJob : public BaseJob { +public: + /// Gets Matrix server discovery information about the domain. + explicit GetWellknownJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetWellknownJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); -namespace QMatrixClient -{ - // Operations + // Result properties - /// Gets Matrix server discovery information about the domain. - /// - /// 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 - /// suitably namespaced for each application and reduces the risk of - /// clashes. - /// - /// Note that this endpoint is not necessarily handled by the homeserver, - /// but by another webserver, to be used for discovering the homeserver URL. - class GetWellknownJob : public BaseJob + /// Server discovery information. + DiscoveryInformation data() const { - public: - explicit GetWellknownJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetWellknownJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetWellknownJob() override; - - // Result properties - - /// Information about the homeserver to connect to. - const HomeserverInformation& homeserver() const; - /// Optional. Information about the identity server to connect to. - const Omittable<IdentityServerInformation>& identityServer() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return fromJson<DiscoveryInformation>(jsonData()); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index cb6439ef..af0c5d31 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -4,49 +4,17 @@ #include "whoami.h" -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetTokenOwnerJob::Private -{ - public: - QString userId; -}; +using namespace Quotient; QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/account/whoami"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/v3", "/account/whoami")); } -static const auto GetTokenOwnerJobName = QStringLiteral("GetTokenOwnerJob"); - GetTokenOwnerJob::GetTokenOwnerJob() - : BaseJob(HttpVerb::Get, GetTokenOwnerJobName, - basePath % "/account/whoami") - , d(new Private) -{ -} - -GetTokenOwnerJob::~GetTokenOwnerJob() = default; - -const QString& GetTokenOwnerJob::userId() const + : BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"), + makePath("/_matrix/client/v3", "/account/whoami")) { - return d->userId; + addExpectedKey("user_id"); } - -BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("user_id"_ls)) - return { JsonParseError, - "The key 'user_id' not found in the response" }; - d->userId = fromJson<QString>(json.value("user_id"_ls)); - return Success; -} - diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index 71e9d532..3451dbc3 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -6,46 +6,49 @@ #include "jobs/basejob.h" - -namespace QMatrixClient -{ - // Operations - +namespace Quotient { + +/*! \brief Gets information about the owner of an access token. + * + * Gets information about the owner of a given access token. + * + * 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` + * is registered by the appservice, and return it in the response + * body. + */ +class QUOTIENT_API GetTokenOwnerJob : public BaseJob { +public: /// Gets information about the owner of an access token. - /// - /// Gets information about the owner of a given access token. - /// - /// 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`` - /// is registered by the appservice, and return it in the response - /// body. - class GetTokenOwnerJob : public BaseJob + explicit GetTokenOwnerJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetTokenOwnerJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// 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); } + + /// When `true`, the user is a [Guest User](#guest-access). When + /// not present or `false`, the user is presumed to be a non-guest + /// user. + Omittable<bool> isGuest() const { - public: - explicit GetTokenOwnerJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetTokenOwnerJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetTokenOwnerJob() override; - - // Result properties - - /// The user id that owns the access token. - const QString& userId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; + return loadFromJson<Omittable<bool>>("is_guest"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/{{base}}.cpp.mustache b/lib/csapi/{{base}}.cpp.mustache deleted file mode 100644 index 64fd8bf3..00000000 --- a/lib/csapi/{{base}}.cpp.mustache +++ /dev/null @@ -1,120 +0,0 @@ -{{>preamble}} -#include "{{filenameBase}}.h" -{{^models}} -#include "converters.h" -{{/models}}{{#operations}} -{{#producesNonJson?}}#include <QtNetwork/QNetworkReply> -{{/producesNonJson?}}#include <QtCore/QStringBuilder> -{{/operations}} -using namespace QMatrixClient; -{{#models.model}}{{#in?}} -QJsonObject QMatrixClient::toJson(const {{qualifiedName}}& pod) -{ - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; -} -{{/in?}}{{#out?}} -{{qualifiedName}} FromJsonObject<{{qualifiedName}}>::operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const -{ - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); -{{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; -} -{{/out?}}{{/models.model}}{{#operations}} -static const auto basePath = QStringLiteral("{{basePathWithoutHost}}"); -{{# operation}}{{#models}} -namespace QMatrixClient -{ - // Converters -{{#model}}{{#in?}} - QJsonObject toJson(const {{qualifiedName}}& pod) - { - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; - } -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{qualifiedName}}> - { - {{qualifiedName}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const - { - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); -{{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; - } - }; -{{/out?}}{{/model}}} // namespace QMatrixClient -{{/ models}}{{#responses}}{{#normalResponse?}}{{#allProperties?}} -class {{camelCaseOperationId}}Job::Private -{ - public:{{#allProperties}} - {{>maybeOmittableType}} {{paramName}};{{/allProperties}} -}; -{{/ allProperties?}}{{/normalResponse?}}{{/responses}}{{#queryParams?}} -BaseJob::Query queryTo{{camelCaseOperationId}}({{#queryParams}}{{>joinedParamDef}}{{/queryParams}}) -{ - BaseJob::Query _q;{{#queryParams}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(_q, QStringLiteral("{{baseName}}"), {{paramName}});{{/queryParams}} - return _q; -} -{{/queryParams?}}{{^bodyParams}} -QUrl {{camelCaseOperationId}}Job::makeRequestUrl(QUrl baseUrl{{#allParams?}}, {{#allParams}}{{>joinedParamDef}}{{/allParams}}{{/allParams?}}) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath{{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}}, - queryTo{{camelCaseOperationId}}({{>passQueryParams}}){{/queryParams?}}); -} -{{/ bodyParams}} -static const auto {{camelCaseOperationId}}JobName = QStringLiteral("{{camelCaseOperationId}}Job"); - -{{camelCaseOperationId}}Job::{{camelCaseOperationId}}Job({{#allParams}}{{>joinedParamDef}}{{/allParams}}) - : BaseJob(HttpVerb::{{#_cap}}{{#_tolower}}{{httpMethod}}{{/_tolower}}{{/_cap}}, {{camelCaseOperationId}}JobName, - basePath{{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}}, - queryTo{{camelCaseOperationId}}({{>passQueryParams}}){{/queryParams?}}{{#skipAuth}}{{#queryParams?}}, - {}{{/queryParams?}}, false{{/skipAuth}}){{#responses}}{{#normalResponse?}}{{#allProperties?}} - , d(new Private){{/allProperties?}}{{/normalResponse?}}{{/responses}} -{ -{{#headerParams?}}{{#headerParams}} setRequestHeader("{{baseName}}", {{paramName}}.toLatin1()); -{{/headerParams}} -{{/headerParams? -}}{{#bodyParams? -}}{{#inlineBody}} setRequestData(Data({{! - }}{{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? - }}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}}));{{/inlineBody -}}{{^inlineBody}} QJsonObject _data;{{#bodyParams}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(_data, QStringLiteral("{{baseName}}"), {{paramName}});{{/bodyParams}} - setRequestData(_data);{{/inlineBody}} -{{/bodyParams?}}{{#producesNonJson?}} setExpectedContentTypes({ {{#produces}}"{{_}}"{{>cjoin}}{{/produces}} }); -{{/producesNonJson?}}}{{!<- mind the actual brace}} -{{# responses}}{{#normalResponse?}}{{#allProperties?}} -{{camelCaseOperationId}}Job::~{{camelCaseOperationId}}Job() = default; -{{# allProperties}} -{{>qualifiedMaybeCrefType}} {{camelCaseOperationId}}Job::{{paramName}}(){{^moveOnly}} const{{/moveOnly}} -{ - return {{#moveOnly}}std::move({{/moveOnly}}d->{{paramName}}{{#moveOnly}}){{/moveOnly}}; -} -{{/ allProperties}}{{#producesNonJson?}} -BaseJob::Status {{camelCaseOperationId}}Job::parseReply(QNetworkReply* reply) -{ - {{#headers}}d->{{paramName}} = reply->rawHeader("{{baseName}}");{{! We don't check for required headers yet }} - {{/headers}}{{#properties}}d->{{paramName}} = reply;{{/properties}} - return Success; -}{{/ producesNonJson?}}{{^producesNonJson?}} -BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data) -{ -{{#inlineResponse}} d->{{paramName}} = fromJson<{{dataType.name}}>(data); -{{/inlineResponse}}{{^inlineResponse}} auto json = data.object(); -{{#properties}}{{#required?}} if (!json.contains("{{baseName}}"_ls)) - return { JsonParseError, - "The key '{{baseName}}' not found in the response" }; -{{/required?}} d->{{paramName}} = fromJson<{{dataType.name}}>(json.value("{{baseName}}"_ls)); -{{/properties}}{{/inlineResponse}} return Success; -}{{/ producesNonJson?}} -{{/allProperties?}}{{/normalResponse?}}{{/responses}}{{/operation}}{{/operations}} diff --git a/lib/csapi/{{base}}.h.mustache b/lib/csapi/{{base}}.h.mustache deleted file mode 100644 index 147c8607..00000000 --- a/lib/csapi/{{base}}.h.mustache +++ /dev/null @@ -1,79 +0,0 @@ -{{>preamble}} -#pragma once - -{{#operations}}#include "jobs/basejob.h" -{{/operations}}{{#models}}#include "converters.h" -{{/models}} -{{#imports}}#include {{_}} -{{/imports}} -namespace QMatrixClient -{ -{{#models}} // Data structures -{{# model}}{{#description}} - /// {{_}}{{/description}} - struct {{name}}{{#parents?}} : {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} - { -{{#vars}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/vars}}{{#propertyMap}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/propertyMap}} }; -{{#in?}} - QJsonObject toJson(const {{name}}& pod); -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{name}}> - { - {{name}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const; - }; -{{/ out?}}{{/model}} -{{/models}}{{#operations}} // Operations -{{# operation}}{{#summary}} - /// {{summary}}{{#description?}}{{!add a linebreak between summary and description if both exist}} - ///{{/description?}}{{/summary}}{{#description}} - /// {{_}}{{/description}} - class {{camelCaseOperationId}}Job : public BaseJob - { - public:{{#models}} - // Inner data structures -{{# model}}{{#description}} - /// {{_}}{{/description}} - struct {{name}}{{#parents?}} : {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} - { -{{#vars}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/vars}}{{#propertyMap}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/propertyMap}} }; -{{/ model}} - // Construction/destruction -{{/ models}}{{#allParams?}} - /*! {{summary}}{{#allParams}} - * \param {{nameCamelCase}}{{#description}} - * {{_}}{{/description}}{{/allParams}} - */{{/allParams?}} - explicit {{camelCaseOperationId}}Job({{#allParams}}{{>joinedParamDecl}}{{/allParams}});{{^bodyParams}} - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * {{camelCaseOperationId}}Job is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl{{#allParams?}}, {{#allParams}}{{>joinedParamDecl}}{{/allParams}}{{/allParams?}}); -{{/bodyParams}}{{# responses}}{{#normalResponse?}}{{#allProperties?}} - ~{{camelCaseOperationId}}Job() override; - - // Result properties -{{#allProperties}}{{#description}} - /// {{_}}{{/description}} - {{>maybeCrefType}} {{paramName}}(){{^moveOnly}} const{{/moveOnly}};{{/allProperties}} - - protected: - Status {{#producesNonJson?}}parseReply(QNetworkReply* reply){{/producesNonJson?}}{{^producesNonJson?}}parseJson(const QJsonDocument& data){{/producesNonJson?}} override; - - private: - class Private; - QScopedPointer<Private> d;{{/allProperties?}}{{/normalResponse?}}{{/responses}} - }; -{{/operation}}{{/operations}}{{!skip EOL -}}} // namespace QMatrixClient diff --git a/lib/database.cpp b/lib/database.cpp new file mode 100644 index 00000000..2b472648 --- /dev/null +++ b/lib/database.cpp @@ -0,0 +1,419 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "database.h" + +#include <QtSql/QSqlDatabase> +#include <QtSql/QSqlQuery> +#include <QtSql/QSqlError> +#include <QtCore/QStandardPaths> +#include <QtCore/QDebug> +#include <QtCore/QDir> + +#include "e2ee/e2ee.h" +#include "e2ee/qolmsession.h" +#include "e2ee/qolminboundsession.h" +#include "e2ee/qolmoutboundsession.h" + +using namespace Quotient; +Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) + : QObject(parent) + , m_matrixId(matrixId) +{ + m_matrixId.replace(':', '_'); + QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId)); + QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId); + QDir(databasePath).mkpath(databasePath); + database().setDatabaseName(databasePath + QStringLiteral("/quotient_%1.db3").arg(deviceId)); + database().open(); + + switch(version()) { + case 0: migrateTo1(); [[fallthrough]]; + case 1: migrateTo2(); [[fallthrough]]; + case 2: migrateTo3(); [[fallthrough]]; + case 3: migrateTo4(); [[fallthrough]]; + case 4: migrateTo5(); + } +} + +int Database::version() +{ + auto query = execute(QStringLiteral("PRAGMA user_version;")); + if (query.next()) { + bool ok = false; + int value = query.value(0).toInt(&ok); + qCDebug(DATABASE) << "Database version" << value; + if (ok) + return value; + } else { + qCritical() << "Failed to check database version"; + } + return -1; +} + +QSqlQuery Database::execute(const QString &queryString) +{ + auto query = database().exec(queryString); + if (query.lastError().type() != QSqlError::NoError) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +QSqlQuery Database::execute(QSqlQuery &query) +{ + if (!query.exec()) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +void Database::transaction() +{ + database().transaction(); +} + +void Database::commit() +{ + database().commit(); +} + +void Database::migrateTo1() +{ + qCDebug(DATABASE) << "Migrating database to version 1"; + transaction(); + execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); + execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); + + execute(QStringLiteral("PRAGMA user_version = 1;")); + commit(); +} + +void Database::migrateTo2() +{ + qCDebug(DATABASE) << "Migrating database to version 2"; + transaction(); + + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); + execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); + + // Add indexes for improving queries speed on larger database + execute(QStringLiteral("CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)")); + execute(QStringLiteral("CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)")); + execute(QStringLiteral("CREATE INDEX inbound_room_idx ON inbound_megolm_sessions(roomId)")); + execute(QStringLiteral("CREATE INDEX group_session_idx ON group_session_record_index(roomId, sessionId, i)")); + execute(QStringLiteral("PRAGMA user_version = 2;")); + commit(); +} + +void Database::migrateTo3() +{ + qCDebug(DATABASE) << "Migrating database to version 3"; + transaction(); + + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions_temp AS SELECT roomId, sessionId, pickle FROM inbound_megolm_sessions;")); + execute(QStringLiteral("DROP TABLE inbound_megolm_sessions;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions_temp RENAME TO inbound_megolm_sessions;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD olmSessionId TEXT;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD senderId TEXT;")); + execute(QStringLiteral("PRAGMA user_version = 3;")); + commit(); +} + +void Database::migrateTo4() +{ + qCDebug(DATABASE) << "Migrating database to version 4"; + transaction(); + + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;")); + execute(QStringLiteral("PRAGMA user_version = 4;")); + commit(); +} + +void Database::migrateTo5() +{ + qCDebug(DATABASE) << "Migrating database to version 5"; + transaction(); + + execute(QStringLiteral("ALTER TABLE tracked_devices ADD verified BOOL;")); + execute(QStringLiteral("PRAGMA user_version = 5")); + commit(); +} + +QByteArray Database::accountPickle() +{ + auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); + execute(query); + if (query.next()) { + return query.value(QStringLiteral("pickle")).toByteArray(); + } + return {}; +} + +void Database::setAccountPickle(const QByteArray &pickle) +{ + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto query = prepareQuery(QStringLiteral("INSERT INTO accounts(pickle) VALUES(:pickle);")); + query.bindValue(":pickle", pickle); + transaction(); + execute(deleteQuery); + execute(query); + commit(); +} + +void Database::clear() +{ + auto query = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto sessionsQuery = prepareQuery(QStringLiteral("DELETE FROM olm_sessions;")); + auto megolmSessionsQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions;")); + auto groupSessionIndexRecordQuery = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index;")); + + transaction(); + execute(query); + execute(sessionsQuery); + execute(megolmSessionsQuery); + execute(groupSessionIndexRecordQuery); + commit(); + +} + +void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp) +{ + auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle, lastReceived) VALUES(:senderKey, :sessionId, :pickle, :lastReceived);")); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + query.bindValue(":lastReceived", timestamp); + transaction(); + execute(query); + commit(); +} + +UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions ORDER BY lastReceived DESC;")); + transaction(); + execute(query); + commit(); + UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions; + while (query.next()) { + if (auto expectedSession = + QOlmSession::unpickle(query.value("pickle").toByteArray(), + picklingMode)) { + sessions[query.value("senderKey").toString()].emplace_back( + std::move(*expectedSession)); + } else + qCWarning(E2EE) + << "Failed to unpickle olm session:" << expectedSession.error(); + } + return sessions; +} + +UnorderedMap<QString, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;")); + query.bindValue(":roomId", roomId); + transaction(); + execute(query); + commit(); + UnorderedMap<QString, QOlmInboundGroupSessionPtr> sessions; + while (query.next()) { + if (auto expectedSession = QOlmInboundGroupSession::unpickle( + query.value("pickle").toByteArray(), picklingMode)) { + auto& sessionPtr = sessions[query.value("sessionId").toString()] = + std::move(*expectedSession); + sessionPtr->setOlmSessionId(query.value("olmSessionId").toString()); + sessionPtr->setSenderId(query.value("senderId").toString()); + } else + qCWarning(E2EE) << "Failed to unpickle megolm session:" + << expectedSession.error(); + } + return sessions; +} + +void Database::saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId) +{ + auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId);")); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + query.bindValue(":senderId", senderId); + query.bindValue(":olmSessionId", olmSessionId); + transaction(); + execute(query); + commit(); +} + +void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) +{ + auto query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);"); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + query.bindValue(":eventId", eventId); + query.bindValue(":ts", ts); + transaction(); + execute(query); + commit(); +} + +std::pair<QString, qint64> Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;")); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + transaction(); + execute(query); + commit(); + if (!query.next()) { + return {}; + } + return {query.value("eventId").toString(), query.value("ts").toLongLong()}; +} + +QSqlDatabase Database::database() +{ + return QSqlDatabase::database(QStringLiteral("Quotient_%1").arg(m_matrixId)); +} + +QSqlQuery Database::prepareQuery(const QString& queryString) +{ + QSqlQuery query(database()); + query.prepare(queryString); + return query; +} + +void Database::clearRoomData(const QString& roomId) +{ + auto query = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;")); + auto query2 = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;")); + auto query3 = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index WHERE roomId=:roomId;")); + transaction(); + execute(query); + execute(query2); + execute(query3); + commit(); +} + +void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET lastReceived=:lastReceived WHERE sessionId=:sessionId;")); + query.bindValue(":lastReceived", timestamp); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} + +void Database::saveCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode, + const QOlmOutboundGroupSession& session) +{ + const auto pickle = session.pickle(picklingMode); + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); + deleteQuery.bindValue(":roomId", roomId); + deleteQuery.bindValue(":sessionId", session.sessionId()); + + auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); + insertQuery.bindValue(":roomId", roomId); + insertQuery.bindValue(":sessionId", session.sessionId()); + insertQuery.bindValue(":pickle", pickle); + insertQuery.bindValue(":creationTime", session.creationTime()); + insertQuery.bindValue(":messageCount", session.messageCount()); + + transaction(); + execute(deleteQuery); + execute(insertQuery); + commit(); +} + +QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM outbound_megolm_sessions WHERE roomId=:roomId ORDER BY creationTime DESC;")); + query.bindValue(":roomId", roomId); + execute(query); + if (query.next()) { + auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (sessionResult) { + auto session = std::move(*sessionResult); + session->setCreationTime(query.value("creationTime").toDateTime()); + session->setMessageCount(query.value("messageCount").toInt()); + return session; + } + } + return nullptr; +} + +void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index) +{ + transaction(); + for (const auto& [user, device, curveKey] : devices) { + auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); + query.bindValue(":roomId", roomId); + query.bindValue(":userId", user); + query.bindValue(":deviceId", device); + query.bindValue(":identityKey", curveKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":i", index); + execute(query); + } + commit(); +} + +QMultiHash<QString, QString> Database::devicesWithoutKey( + const QString& roomId, QMultiHash<QString, QString> devices, + const QString& sessionId) +{ + auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); + while (query.next()) { + devices.remove(query.value("userId").toString(), + query.value("deviceId").toString()); + } + return devices; +} + +void Database::updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET pickle=:pickle WHERE senderKey=:senderKey AND sessionId=:sessionId;")); + query.bindValue(":pickle", pickle); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} + +void Database::setSessionVerified(const QString& edKeyId) +{ + auto query = prepareQuery(QStringLiteral("UPDATE tracked_devices SET verified=true WHERE edKeyId=:edKeyId;")); + query.bindValue(":edKeyId", edKeyId); + transaction(); + execute(query); + commit(); +} + +bool Database::isSessionVerified(const QString& edKey) +{ + auto query = prepareQuery(QStringLiteral("SELECT verified FROM tracked_devices WHERE edKey=:edKey")); + query.bindValue(":edKey", edKey); + execute(query); + return query.next() && query.value("verified").toBool(); +} diff --git a/lib/database.h b/lib/database.h new file mode 100644 index 00000000..8a133f8e --- /dev/null +++ b/lib/database.h @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QtCore/QObject> +#include <QtSql/QSqlQuery> +#include <QtCore/QVector> + +#include <QtCore/QHash> + +#include "e2ee/e2ee.h" + +namespace Quotient { + +class QUOTIENT_API Database : public QObject +{ + Q_OBJECT +public: + Database(const QString& matrixId, const QString& deviceId, QObject* parent); + + int version(); + void transaction(); + void commit(); + QSqlQuery execute(const QString &queryString); + QSqlQuery execute(QSqlQuery &query); + QSqlDatabase database(); + QSqlQuery prepareQuery(const QString& quaryString); + + QByteArray accountPickle(); + void setAccountPickle(const QByteArray &pickle); + void clear(); + void saveOlmSession(const QString& senderKey, const QString& sessionId, + const QByteArray& pickle, const QDateTime& timestamp); + UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions( + const PicklingMode& picklingMode); + UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadMegolmSessions( + const QString& roomId, const PicklingMode& picklingMode); + void saveMegolmSession(const QString& roomId, const QString& sessionId, + const QByteArray& pickle, const QString& senderId, + const QString& olmSessionId); + void addGroupSessionIndexRecord(const QString& roomId, + const QString& sessionId, uint32_t index, + const QString& eventId, qint64 ts); + std::pair<QString, qint64> groupSessionIndexRecord(const QString& roomId, + const QString& sessionId, + qint64 index); + void clearRoomData(const QString& roomId); + void setOlmSessionLastReceived(const QString& sessionId, + const QDateTime& timestamp); + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode); + void saveCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode, + const QOlmOutboundGroupSession& session); + void updateOlmSession(const QString& senderKey, const QString& sessionId, + const QByteArray& pickle); + + // Returns a map UserId -> [DeviceId] that have not received key yet + QMultiHash<QString, QString> devicesWithoutKey( + const QString& roomId, QMultiHash<QString, QString> devices, + const QString& sessionId); + // 'devices' contains tuples {userId, deviceId, curveKey} + void setDevicesReceivedKey( + const QString& roomId, + const QVector<std::tuple<QString, QString, QString>>& devices, + const QString& sessionId, int index); + + bool isSessionVerified(const QString& edKey); + void setSessionVerified(const QString& edKeyId); + +private: + void migrateTo1(); + void migrateTo2(); + void migrateTo3(); + void migrateTo4(); + void migrateTo5(); + + QString m_matrixId; +}; +} // namespace Quotient diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h new file mode 100644 index 00000000..5999c0be --- /dev/null +++ b/lib/e2ee/e2ee.h @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +#include <QtCore/QMetaType> +#include <QtCore/QStringBuilder> + +#include <array> + +#ifdef Quotient_E2EE_ENABLED +# include "expected.h" + +# include <olm/error.h> +# include <variant> +#endif + +namespace Quotient { + +constexpr auto AlgorithmKeyL = "algorithm"_ls; +constexpr auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +constexpr auto AlgorithmKey = "algorithm"_ls; +constexpr auto RotationPeriodMsKey = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKey = "rotation_period_msgs"_ls; + +constexpr auto Ed25519Key = "ed25519"_ls; +constexpr auto Curve25519Key = "curve25519"_ls; +constexpr auto SignedCurve25519Key = "signed_curve25519"_ls; + +constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls; +constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; + +constexpr std::array SupportedAlgorithms { OlmV1Curve25519AesSha2AlgoKey, + MegolmV1AesSha2AlgoKey }; + +inline bool isSupportedAlgorithm(const QString& algorithm) +{ + return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), + algorithm) + != SupportedAlgorithms.cend(); +} + +#ifdef Quotient_E2EE_ENABLED +struct Unencrypted {}; +struct Encrypted { + QByteArray key; +}; + +using PicklingMode = std::variant<Unencrypted, Encrypted>; + +class QOlmSession; +using QOlmSessionPtr = std::unique_ptr<QOlmSession>; + +class QOlmInboundGroupSession; +using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>; + +class QOlmOutboundGroupSession; +using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>; + +template <typename T> +using QOlmExpected = Expected<T, OlmErrorCode>; +#endif + +struct IdentityKeys +{ + QByteArray curve25519; + QByteArray ed25519; +}; + +//! Struct representing the one-time keys. +struct UnsignedOneTimeKeys +{ + QHash<QString, QHash<QString, QString>> keys; + + //! Get the HashMap containing the curve25519 one-time keys. + QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; } +}; + +class SignedOneTimeKey { +public: + explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId, + const QString& deviceId, + const QByteArray& signature) + : payload { { "key"_ls, unsignedKey }, + { "signatures"_ls, + QJsonObject { + { userId, QJsonObject { { "ed25519:"_ls % deviceId, + QString(signature) } } } } } } + {} + explicit SignedOneTimeKey(const QJsonObject& jo = {}) + : payload(jo) + {} + + //! Unpadded Base64-encoded 32-byte Curve25519 public key + QByteArray key() const { return payload["key"_ls].toString().toLatin1(); } + + //! \brief Signatures of the key object + //! + //! The signature is calculated using the process described at + //! https://spec.matrix.org/v1.3/appendices/#signing-json + auto signatures() const + { + return fromJson<QHash<QString, QHash<QString, QString>>>( + payload["signatures"_ls]); + } + + QByteArray signature(QStringView userId, QStringView deviceId) const + { + return payload["signatures"_ls][userId]["ed25519:"_ls % deviceId] + .toString() + .toLatin1(); + } + + //! Whether the key is a fallback key + bool isFallback() const { return payload["fallback"_ls].toBool(); } + auto toJson() const { return payload; } + auto toJsonForVerification() const + { + auto json = payload; + json.remove("signatures"_ls); + json.remove("unsigned"_ls); + return QJsonDocument(json).toJson(QJsonDocument::Compact); + } + +private: + QJsonObject payload; +}; + +using OneTimeKeys = QHash<QString, std::variant<QString, SignedOneTimeKey>>; + +} // namespace Quotient + +Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp new file mode 100644 index 00000000..345ab16b --- /dev/null +++ b/lib/e2ee/qolmaccount.cpp @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmaccount.h" + +#include "connection.h" +#include "e2ee/qolmsession.h" +#include "e2ee/qolmutility.h" +#include "e2ee/qolmutils.h" + +#include "csapi/keys.h" + +#include <QtCore/QRandomGenerator> + +#include <olm/olm.h> + +using namespace Quotient; + +// Convert olm error to enum +OlmErrorCode QOlmAccount::lastErrorCode() const { + return olm_account_last_error_code(m_account); +} + +const char* QOlmAccount::lastError() const +{ + return olm_account_last_error(m_account); +} + +QOlmAccount::QOlmAccount(QStringView userId, QStringView deviceId, + QObject* parent) + : QObject(parent) + , m_userId(userId.toString()) + , m_deviceId(deviceId.toString()) +{} + +QOlmAccount::~QOlmAccount() +{ + olm_clear_account(m_account); + delete[](reinterpret_cast<uint8_t *>(m_account)); +} + +void QOlmAccount::createNewAccount() +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + if (const auto randomLength = olm_create_account_random_length(m_account); + olm_create_account(m_account, RandomBuffer(randomLength), randomLength) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to create a new account"); + + emit needsSave(); +} + +OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled, + const PicklingMode& mode) +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + if (const auto key = toKey(mode); + olm_unpickle_account(m_account, key.data(), key.length(), + pickled.data(), pickled.size()) + == olm_error()) { + // Probably log the user out since we have no way of getting to the keys + return lastErrorCode(); + } + return OLM_SUCCESS; +} + +QByteArray QOlmAccount::pickle(const PicklingMode &mode) +{ + const QByteArray key = toKey(mode); + const size_t pickleLength = olm_pickle_account_length(m_account); + QByteArray pickleBuffer(pickleLength, '\0'); + if (olm_pickle_account(m_account, key.data(), key.length(), + pickleBuffer.data(), pickleLength) + == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable("Failed to pickle Olm account " + + accountId())); + + return pickleBuffer; +} + +IdentityKeys QOlmAccount::identityKeys() const +{ + const size_t keyLength = olm_account_identity_keys_length(m_account); + QByteArray keyBuffer(keyLength, '\0'); + if (olm_account_identity_keys(m_account, keyBuffer.data(), keyLength) + == olm_error()) { + QOLM_INTERNAL_ERROR( + qPrintable("Failed to get " % accountId() % " identity keys")); + } + const auto key = QJsonDocument::fromJson(keyBuffer).object(); + return IdentityKeys { + key.value(QStringLiteral("curve25519")).toString().toUtf8(), + key.value(QStringLiteral("ed25519")).toString().toUtf8() + }; +} + +QByteArray QOlmAccount::sign(const QByteArray &message) const +{ + QByteArray signatureBuffer(olm_account_signature_length(m_account), '\0'); + + if (olm_account_sign(m_account, message.data(), message.length(), + signatureBuffer.data(), signatureBuffer.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to sign a message"); + + return signatureBuffer; +} + +QByteArray QOlmAccount::sign(const QJsonObject &message) const +{ + return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); +} + +QByteArray QOlmAccount::signIdentityKeys() const +{ + const auto keys = identityKeys(); + return sign(QJsonObject{ + { "algorithms", QJsonArray{ "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" } }, + { "user_id", m_userId }, + { "device_id", m_deviceId }, + { "keys", QJsonObject{ { QStringLiteral("curve25519:") + m_deviceId, + QString::fromUtf8(keys.curve25519) }, + { QStringLiteral("ed25519:") + m_deviceId, + QString::fromUtf8(keys.ed25519) } } } }); +} + +size_t QOlmAccount::maxNumberOfOneTimeKeys() const +{ + return olm_account_max_number_of_one_time_keys(m_account); +} + +size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) +{ + const auto randomLength = + olm_account_generate_one_time_keys_random_length(m_account, + numberOfKeys); + const auto result = olm_account_generate_one_time_keys( + m_account, numberOfKeys, RandomBuffer(randomLength), randomLength); + + if (result == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable( + "Failed to generate one-time keys for account " + accountId())); + + emit needsSave(); + return result; +} + +UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const +{ + const auto oneTimeKeyLength = olm_account_one_time_keys_length(m_account); + QByteArray oneTimeKeysBuffer(static_cast<int>(oneTimeKeyLength), '\0'); + + if (olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), + oneTimeKeyLength) + == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable( + "Failed to obtain one-time keys for account" % accountId())); + + const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); + UnsignedOneTimeKeys oneTimeKeys; + fromJson(json, oneTimeKeys.keys); + return oneTimeKeys; +} + +OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const +{ + OneTimeKeys signedOneTimeKeys; + for (const auto& curveKeys = keys.curve25519(); + const auto& [keyId, key] : asKeyValueRange(curveKeys)) + signedOneTimeKeys.insert("signed_curve25519:" % keyId, + SignedOneTimeKey { + key, m_userId, m_deviceId, + sign(QJsonObject { { "key", key } }) }); + return signedOneTimeKeys; +} + +OlmErrorCode QOlmAccount::removeOneTimeKeys(const QOlmSession& session) +{ + if (olm_remove_one_time_keys(m_account, session.raw()) == olm_error()) { + qWarning(E2EE).nospace() + << "Failed to remove one-time keys for session " + << session.sessionId() << ": " << lastError(); + return lastErrorCode(); + } + emit needsSave(); + return OLM_SUCCESS; +} + +OlmAccount* QOlmAccount::data() { return m_account; } + +DeviceKeys QOlmAccount::deviceKeys() const +{ + static QStringList Algorithms(SupportedAlgorithms.cbegin(), + SupportedAlgorithms.cend()); + + const auto idKeys = identityKeys(); + return DeviceKeys{ + .userId = m_userId, + .deviceId = m_deviceId, + .algorithms = Algorithms, + .keys{ { "curve25519:" + m_deviceId, idKeys.curve25519 }, + { "ed25519:" + m_deviceId, idKeys.ed25519 } }, + .signatures{ + { m_userId, { { "ed25519:" + m_deviceId, signIdentityKeys() } } } } + }; +} + +UploadKeysJob* QOlmAccount::createUploadKeyRequest( + const UnsignedOneTimeKeys& oneTimeKeys) const +{ + return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys)); +} + +QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSession( + const QOlmMessage& preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSession(this, preKeyMessage); +} + +QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, + preKeyMessage); +} + +QOlmExpected<QOlmSessionPtr> QOlmAccount::createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey) +{ + return QOlmSession::createOutboundSession(this, theirIdentityKey, + theirOneTimeKey); +} + +void QOlmAccount::markKeysAsPublished() +{ + olm_account_mark_keys_as_published(m_account); + emit needsSave(); +} + +bool Quotient::verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId) +{ + const auto signKeyId = "ed25519:" + deviceId; + const auto signingKey = deviceKeys.keys[signKeyId]; + const auto signature = deviceKeys.signatures[userId][signKeyId]; + + return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); +} + +bool Quotient::ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature) +{ + if (signature.isEmpty()) + return false; + + QJsonObject obj1 = obj; + + obj1.remove("unsigned"); + obj1.remove("signatures"); + + auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact); + + QByteArray signingKeyBuf = signingKey.toUtf8(); + QOlmUtility utility; + auto signatureBuf = signature.toUtf8(); + return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); +} + +QString QOlmAccount::accountId() const { return m_userId % '/' % m_deviceId; } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h new file mode 100644 index 00000000..a5faa82a --- /dev/null +++ b/lib/e2ee/qolmaccount.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#pragma once + +#include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" + +#include "csapi/keys.h" + +#include <QtCore/QObject> + +struct OlmAccount; + +namespace Quotient { + +//! An olm account manages all cryptographic keys used on a device. +//! \code{.cpp} +//! const auto olmAccount = new QOlmAccount(this); +//! \endcode +class QUOTIENT_API QOlmAccount : public QObject +{ + Q_OBJECT +public: + QOlmAccount(QStringView userId, QStringView deviceId, + QObject* parent = nullptr); + ~QOlmAccount() override; + + //! Creates a new instance of OlmAccount. During the instantiation + //! the Ed25519 fingerprint key pair and the Curve25519 identity key + //! pair are generated. For more information see <a + //! href="https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption">here</a>. + //! This needs to be called before any other action or use unpickle() instead. + void createNewAccount(); + + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. + //! This needs to be called before any other action or use createNewAccount() instead. + [[nodiscard]] OlmErrorCode unpickle(QByteArray&& pickled, + const PicklingMode& mode); + + //! Serialises an OlmAccount to encrypted Base64. + QByteArray pickle(const PicklingMode &mode); + + //! Returns the account's public identity keys already formatted as JSON + IdentityKeys identityKeys() const; + + //! Returns the signature of the supplied message. + QByteArray sign(const QByteArray &message) const; + QByteArray sign(const QJsonObject& message) const; + + //! Sign identity keys. + QByteArray signIdentityKeys() const; + + //! Maximum number of one time keys that this OlmAccount can + //! currently hold. + size_t maxNumberOfOneTimeKeys() const; + + //! Generates the supplied number of one time keys. + size_t generateOneTimeKeys(size_t numberOfKeys); + + //! Gets the OlmAccount's one time keys formatted as JSON. + UnsignedOneTimeKeys oneTimeKeys() const; + + //! Sign all one time keys. + OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const; + + UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const; + + DeviceKeys deviceKeys() const; + + //! Remove the one time key used to create the supplied session. + [[nodiscard]] OlmErrorCode removeOneTimeKeys(const QOlmSession& session); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param preKeyMessage An Olm pre-key message that was encrypted for this account. + QOlmExpected<QOlmSessionPtr> createInboundSession( + const QOlmMessage& preKeyMessage); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param theirIdentityKey - The identity key of the Olm account that + //! encrypted this Olm message. + QOlmExpected<QOlmSessionPtr> createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage); + + //! Creates an outbound session for sending messages to a specific + /// identity and one time key. + QOlmExpected<QOlmSessionPtr> createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey); + + void markKeysAsPublished(); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + + // HACK do not use directly + QOlmAccount(OlmAccount *account); + OlmAccount *data(); + +Q_SIGNALS: + void needsSave(); + +private: + OlmAccount *m_account = nullptr; // owning + QString m_userId; + QString m_deviceId; + + QString accountId() const; +}; + +QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId); + +//! checks if the signature is signed by the signing_key +QUOTIENT_API bool ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature); + +} // namespace Quotient diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp new file mode 100644 index 00000000..18275dc0 --- /dev/null +++ b/lib/e2ee/qolminboundsession.cpp @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolminboundsession.h" +#include "qolmutils.h" +#include "../logging.h" + +#include <cstring> +#include <iostream> +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmInboundGroupSession::lastErrorCode() const { + return olm_inbound_group_session_last_error_code(m_groupSession); +} + +const char* QOlmInboundGroupSession::lastError() const +{ + return olm_inbound_group_session_last_error(m_groupSession); +} + +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) + : m_groupSession(session) +{} + +QOlmInboundGroupSession::~QOlmInboundGroupSession() +{ + olm_clear_inbound_group_session(m_groupSession); + //delete[](reinterpret_cast<uint8_t *>(m_groupSession)); +} + +QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::create( + const QByteArray& key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + if (olm_init_inbound_group_session( + olmInboundGroupSession, + reinterpret_cast<const uint8_t*>(key.constData()), key.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastErrorCode() + qWarning(E2EE) << "Failed to create an inbound group session:" + << olm_inbound_group_session_last_error( + olmInboundGroupSession); + return olm_inbound_group_session_last_error_code(olmInboundGroupSession); + } + + return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession); +} + +QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::importSession( + const QByteArray& key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + + if (olm_import_inbound_group_session( + olmInboundGroupSession, + reinterpret_cast<const uint8_t*>(key.data()), key.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastError() + qWarning(E2EE) << "Failed to import an inbound group session:" + << olm_inbound_group_session_last_error( + olmInboundGroupSession); + return olm_inbound_group_session_last_error_code(olmInboundGroupSession); + } + + return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession); +} + +QByteArray QOlmInboundGroupSession::pickle(const PicklingMode& mode) const +{ + QByteArray pickledBuf( + olm_pickle_inbound_group_session_length(m_groupSession), '\0'); + if (const auto key = toKey(mode); + olm_pickle_inbound_group_session(m_groupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.length()) + == olm_error()) { + QOLM_INTERNAL_ERROR("Failed to pickle the inbound group session"); + } + return pickledBuf; +} + +QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::unpickle( + QByteArray&& pickled, const PicklingMode& mode) +{ + const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + auto key = toKey(mode); + if (olm_unpickle_inbound_group_session(groupSession, key.data(), + key.length(), pickled.data(), + pickled.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastError() + qWarning(E2EE) << "Failed to unpickle an inbound group session:" + << olm_inbound_group_session_last_error(groupSession); + return olm_inbound_group_session_last_error_code(groupSession); + } + key.clear(); + + return std::make_unique<QOlmInboundGroupSession>(groupSession); +} + +QOlmExpected<std::pair<QByteArray, uint32_t>> QOlmInboundGroupSession::decrypt( + const QByteArray& message) +{ + // This is for capturing the output of olm_group_decrypt + uint32_t messageIndex = 0; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(message.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length( + m_groupSession, + reinterpret_cast<uint8_t*>(messageBuf.data()), + messageBuf.length()), + '\0'); + + messageBuf = QByteArray(message.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast<uint8_t *>(messageBuf.data()), + messageBuf.length(), reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); + if (plaintextLen == olm_error()) { + qWarning(E2EE) << "Failed to decrypt the message:" << lastError(); + return lastErrorCode(); + } + + QByteArray output(plaintextLen, '\0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); + + return std::make_pair(output, messageIndex); +} + +QOlmExpected<QByteArray> QOlmInboundGroupSession::exportSession( + uint32_t messageIndex) +{ + const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); + QByteArray keyBuf(keyLength, '\0'); + if (olm_export_inbound_group_session( + m_groupSession, reinterpret_cast<uint8_t*>(keyBuf.data()), + keyLength, messageIndex) + == olm_error()) { + QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to export the inbound group session"); + return lastErrorCode(); + } + return keyBuf; +} + +uint32_t QOlmInboundGroupSession::firstKnownIndex() const +{ + return olm_inbound_group_session_first_known_index(m_groupSession); +} + +QByteArray QOlmInboundGroupSession::sessionId() const +{ + QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), + '\0'); + if (olm_inbound_group_session_id( + m_groupSession, reinterpret_cast<uint8_t*>(sessionIdBuf.data()), + sessionIdBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain the group session id"); + + return sessionIdBuf; +} + +bool QOlmInboundGroupSession::isVerified() const +{ + return olm_inbound_group_session_is_verified(m_groupSession) != 0; +} + +QString QOlmInboundGroupSession::olmSessionId() const +{ + return m_olmSessionId; +} +void QOlmInboundGroupSession::setOlmSessionId(const QString& newOlmSessionId) +{ + m_olmSessionId = newOlmSessionId; +} + +QString QOlmInboundGroupSession::senderId() const +{ + return m_senderId; +} +void QOlmInboundGroupSession::setSenderId(const QString& senderId) +{ + m_senderId = senderId; +} diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h new file mode 100644 index 00000000..b9710354 --- /dev/null +++ b/lib/e2ee/qolminboundsession.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" + +struct OlmInboundGroupSession; + +namespace Quotient { + +//! An in-bound group session is responsible for decrypting incoming +//! communication in a Megolm session. +class QUOTIENT_API QOlmInboundGroupSession +{ +public: + ~QOlmInboundGroupSession(); + //! Creates a new instance of `OlmInboundGroupSession`. + static QOlmExpected<QOlmInboundGroupSessionPtr> create(const QByteArray& key); + //! Import an inbound group session, from a previous export. + static QOlmExpected<QOlmInboundGroupSessionPtr> importSession(const QByteArray& key); + //! Serialises an `OlmInboundGroupSession` to encrypted Base64. + QByteArray pickle(const PicklingMode& mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by pickling + //! an `OlmInboundGroupSession`. + static QOlmExpected<QOlmInboundGroupSessionPtr> unpickle( + QByteArray&& pickled, const PicklingMode& mode); + //! Decrypts ciphertext received for this group session. + QOlmExpected<std::pair<QByteArray, uint32_t> > decrypt(const QByteArray& message); + //! Export the base64-encoded ratchet key for this session, at the given index, + //! in a format which can be used by import. + QOlmExpected<QByteArray> exportSession(uint32_t messageIndex); + //! Get the first message index we know how to decrypt. + uint32_t firstKnownIndex() const; + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + bool isVerified() const; + + //! The olm session that this session was received from. + //! Required to get the device this session is from. + QString olmSessionId() const; + void setOlmSessionId(const QString& newOlmSessionId); + + //! The sender of this session. + QString senderId() const; + void setSenderId(const QString& senderId); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + + QOlmInboundGroupSession(OlmInboundGroupSession* session); +private: + OlmInboundGroupSession* m_groupSession; + QString m_olmSessionId; + QString m_senderId; +}; + +using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>; +} // namespace Quotient diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp new file mode 100644 index 00000000..b9cb8bd2 --- /dev/null +++ b/lib/e2ee/qolmmessage.cpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmmessage.h" + +#include "util.h" + +using namespace Quotient; + +QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) + : QByteArray(std::move(ciphertext)) + , m_messageType(type) +{ + Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty"); +} + +QOlmMessage::Type QOlmMessage::type() const +{ + return m_messageType; +} + +QByteArray QOlmMessage::toCiphertext() const +{ + return SLICE(*this, QByteArray); +} + +QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) +{ + return QOlmMessage(ciphertext, QOlmMessage::General); +} diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h new file mode 100644 index 00000000..ea73b3e3 --- /dev/null +++ b/lib/e2ee/qolmmessage.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "quotient_export.h" + +#include <QtCore/QByteArray> +#include <qobjectdefs.h> +#include <olm/olm.h> + +namespace Quotient { + +/*! \brief A wrapper around an olm encrypted message + * + * This class encapsulates a Matrix olm encrypted message, + * passed in either of 2 forms: a general message or a pre-key message. + * + * The class provides functions to get a type and the ciphertext. + */ +class QUOTIENT_API QOlmMessage : public QByteArray { + Q_GADGET +public: + enum Type { + PreKey = OLM_MESSAGE_TYPE_PRE_KEY, + General = OLM_MESSAGE_TYPE_MESSAGE, + }; + Q_ENUM(Type) + + explicit QOlmMessage(QByteArray ciphertext, Type type = General); + + static QOlmMessage fromCiphertext(const QByteArray &ciphertext); + + Q_INVOKABLE Type type() const; + Q_INVOKABLE QByteArray toCiphertext() const; + +private: + Type m_messageType = General; +}; + +} //namespace Quotient diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp new file mode 100644 index 00000000..1176d790 --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmoutboundsession.h" + +#include "logging.h" +#include "qolmutils.h" + +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmOutboundGroupSession::lastErrorCode() const { + return olm_outbound_group_session_last_error_code(m_groupSession); +} + +const char* QOlmOutboundGroupSession::lastError() const +{ + return olm_outbound_group_session_last_error(m_groupSession); +} + +QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) + : m_groupSession(session) +{} + +QOlmOutboundGroupSession::~QOlmOutboundGroupSession() +{ + olm_clear_outbound_group_session(m_groupSession); + delete[](reinterpret_cast<uint8_t *>(m_groupSession)); +} + +QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() +{ + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + if (const auto randomLength = olm_init_outbound_group_session_random_length( + olmOutboundGroupSession); + olm_init_outbound_group_session(olmOutboundGroupSession, + RandomBuffer(randomLength).bytes(), + randomLength) + == olm_error()) { + // FIXME: create the session object earlier + QOLM_INTERNAL_ERROR_X("Failed to initialise an outbound group session", + olm_outbound_group_session_last_error( + olmOutboundGroupSession)); + } + + return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); +} + +QByteArray QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf( + olm_pickle_outbound_group_session_length(m_groupSession), '\0'); + auto key = toKey(mode); + if (olm_pickle_outbound_group_session(m_groupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to pickle the outbound group session"); + + key.clear(); + return pickledBuf; +} + +QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle( + QByteArray&& pickled, const PicklingMode& mode) +{ + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + auto key = toKey(mode); + if (olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), + key.length(), pickled.data(), + pickled.length()) + == olm_error()) { + // FIXME: create the session object earlier and use lastError() + qWarning(E2EE) << "Failed to unpickle an outbound group session:" + << olm_outbound_group_session_last_error( + olmOutboundGroupSession); + return olm_outbound_group_session_last_error_code( + olmOutboundGroupSession); + } + + key.clear(); + return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); +} + +QByteArray QOlmOutboundGroupSession::encrypt(const QByteArray& plaintext) const +{ + const auto messageMaxLength = + olm_group_encrypt_message_length(m_groupSession, plaintext.length()); + QByteArray messageBuf(messageMaxLength, '\0'); + if (olm_group_encrypt(m_groupSession, + reinterpret_cast<const uint8_t*>(plaintext.data()), + plaintext.length(), + reinterpret_cast<uint8_t*>(messageBuf.data()), + messageBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to encrypt a message"); + + return messageBuf; +} + +uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const +{ + return olm_outbound_group_session_message_index(m_groupSession); +} + +QByteArray QOlmOutboundGroupSession::sessionId() const +{ + const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); + QByteArray idBuffer(idMaxLength, '\0'); + if (olm_outbound_group_session_id( + m_groupSession, reinterpret_cast<uint8_t*>(idBuffer.data()), + idBuffer.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain group session id"); + + return idBuffer; +} + +QByteArray QOlmOutboundGroupSession::sessionKey() const +{ + const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); + QByteArray keyBuffer(keyMaxLength, '\0'); + if (olm_outbound_group_session_key( + m_groupSession, reinterpret_cast<uint8_t*>(keyBuffer.data()), + keyMaxLength) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain group session key"); + + return keyBuffer; +} + +int QOlmOutboundGroupSession::messageCount() const +{ + return m_messageCount; +} + +void QOlmOutboundGroupSession::setMessageCount(int messageCount) +{ + m_messageCount = messageCount; +} + +QDateTime QOlmOutboundGroupSession::creationTime() const +{ + return m_creationTime; +} + +void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime) +{ + m_creationTime = creationTime; +} diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h new file mode 100644 index 00000000..d36fbf69 --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" + +struct OlmOutboundGroupSession; + +namespace Quotient { + +//! An out-bound group session is responsible for encrypting outgoing +//! communication in a Megolm session. +class QUOTIENT_API QOlmOutboundGroupSession +{ +public: + ~QOlmOutboundGroupSession(); + //! Creates a new instance of `QOlmOutboundGroupSession`. + //! Throw OlmError on errors + static QOlmOutboundGroupSessionPtr create(); + //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. + QByteArray pickle(const PicklingMode &mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by + //! pickling a `QOlmOutboundGroupSession`. + static QOlmExpected<QOlmOutboundGroupSessionPtr> unpickle( + QByteArray&& pickled, const PicklingMode& mode); + + //! Encrypts a plaintext message using the session. + QByteArray encrypt(const QByteArray& plaintext) const; + + //! Get the current message index for this session. + //! + //! Each message is sent with an increasing index; this returns the + //! index for the next message. + uint32_t sessionMessageIndex() const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! Get the base64-encoded current ratchet key for this session. + //! + //! Each message is sent with a different ratchet key. This function returns the + //! ratchet key that will be used for the next message. + QByteArray sessionKey() const; + QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); + + int messageCount() const; + void setMessageCount(int messageCount); + + QDateTime creationTime() const; + void setCreationTime(const QDateTime& creationTime); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + +private: + OlmOutboundGroupSession *m_groupSession; + int m_messageCount = 0; + QDateTime m_creationTime = QDateTime::currentDateTime(); +}; + +} // namespace Quotient diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp new file mode 100644 index 00000000..e3f69132 --- /dev/null +++ b/lib/e2ee/qolmsession.cpp @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmsession.h" + +#include "e2ee/qolmutils.h" +#include "logging.h" + +#include <cstring> +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmSession::lastErrorCode() const { + return olm_session_last_error_code(m_session); +} + +const char* QOlmSession::lastError() const +{ + return olm_session_last_error(m_session); +} + +Quotient::QOlmSession::~QOlmSession() +{ + olm_clear_session(m_session); + delete[](reinterpret_cast<uint8_t *>(m_session)); +} + +OlmSession* QOlmSession::create() +{ + return olm_session(new uint8_t[olm_session_size()]); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createInbound( + QOlmAccount* account, const QOlmMessage& preKeyMessage, bool from, + const QString& theirIdentityKey) +{ + if (preKeyMessage.type() != QOlmMessage::PreKey) { + qCCritical(E2EE) << "The message is not a pre-key; will try to create " + "the inbound session anyway"; + } + + const auto olmSession = create(); + + QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + size_t error = 0; + if (from) { + error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } else { + error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } + + if (error == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + const auto lastErr = olm_session_last_error_code(olmSession); + qCWarning(E2EE) << "Error when creating inbound session" << lastErr; + return lastErr; + } + + return std::make_unique<QOlmSession>(olmSession); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage) +{ + return createInbound(account, preKeyMessage); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage) +{ + return createInbound(account, preKeyMessage, true, theirIdentityKey); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createOutboundSession( + QOlmAccount* account, const QByteArray& theirIdentityKey, + const QByteArray& theirOneTimeKey) +{ + auto* olmOutboundSession = create(); + if (const auto randomLength = + olm_create_outbound_session_random_length(olmOutboundSession); + olm_create_outbound_session( + olmOutboundSession, account->data(), theirIdentityKey.data(), + theirIdentityKey.length(), theirOneTimeKey.data(), + theirOneTimeKey.length(), RandomBuffer(randomLength), randomLength) + == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + const auto lastErr = olm_session_last_error_code(olmOutboundSession); + QOLM_FAIL_OR_LOG_X(lastErr == OLM_NOT_ENOUGH_RANDOM, + "Failed to create an outbound Olm session", + olm_session_last_error(olmOutboundSession)); + return lastErr; + } + + return std::make_unique<QOlmSession>(olmOutboundSession); +} + +QByteArray QOlmSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf(olm_pickle_session_length(m_session), '\0'); + QByteArray key = toKey(mode); + if (olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to pickle an Olm session"); + + key.clear(); + return pickledBuf; +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::unpickle(QByteArray&& pickled, + const PicklingMode& mode) +{ + auto *olmSession = create(); + auto key = toKey(mode); + if (olm_unpickle_session(olmSession, key.data(), key.length(), + pickled.data(), pickled.length()) + == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + const auto errorCode = olm_session_last_error_code(olmSession); + QOLM_FAIL_OR_LOG_X(errorCode == OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to unpickle an Olm session", + olm_session_last_error(olmSession)); + return errorCode; + } + + key.clear(); + return std::make_unique<QOlmSession>(olmSession); +} + +QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext) +{ + const auto messageMaxLength = + olm_encrypt_message_length(m_session, plaintext.length()); + QByteArray messageBuf(messageMaxLength, '\0'); + // NB: The type has to be calculated before calling olm_encrypt() + const auto messageType = olm_encrypt_message_type(m_session); + if (const auto randomLength = olm_encrypt_random_length(m_session); + olm_encrypt(m_session, plaintext.data(), plaintext.length(), + RandomBuffer(randomLength), randomLength, messageBuf.data(), + messageBuf.length()) + == olm_error()) { + QOLM_INTERNAL_ERROR("Failed to encrypt the message"); + } + + return QOlmMessage(messageBuf, QOlmMessage::Type(messageType)); +} + +QOlmExpected<QByteArray> QOlmSession::decrypt(const QOlmMessage &message) const +{ + const auto ciphertext = message.toCiphertext(); + const auto messageTypeValue = message.type(); + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(ciphertext.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length( + m_session, messageTypeValue, messageBuf.data(), messageBuf.length()); + if (plaintextMaxLen == olm_error()) { + qWarning(E2EE) << "Couldn't calculate decrypted message length:" + << lastError(); + return lastErrorCode(); + } + + QByteArray plaintextBuf(plaintextMaxLen, '\0'); + QByteArray messageBuf2(ciphertext.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf2.begin()); + + const auto plaintextResultLen = + olm_decrypt(m_session, messageTypeValue, messageBuf2.data(), + messageBuf2.length(), plaintextBuf.data(), plaintextMaxLen); + if (plaintextResultLen == olm_error()) { + QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to decrypt the message"); + return lastErrorCode(); + } + plaintextBuf.truncate(plaintextResultLen); + return plaintextBuf; +} + +QByteArray QOlmSession::sessionId() const +{ + const auto idMaxLength = olm_session_id_length(m_session); + QByteArray idBuffer(idMaxLength, '\0'); + if (olm_session_id(m_session, idBuffer.data(), idMaxLength) == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain Olm session id"); + + return idBuffer; +} + +bool QOlmSession::hasReceivedMessage() const +{ + return olm_session_has_received_message(m_session); +} + +bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); + QByteArray oneTimeKeyBuf(preKeyMessage.data()); + const auto maybeMatches = + olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), + oneTimeKeyBuf.length()); + if (maybeMatches == olm_error()) + qWarning(E2EE) << "Error matching an inbound session:" << lastError(); + + return maybeMatches == 1; // Any errors are treated as non-match +} + +bool QOlmSession::matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const +{ + const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + const auto maybeMatches = olm_matches_inbound_session_from( + m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + + if (maybeMatches == olm_error()) + qCWarning(E2EE) << "Error matching an inbound session:" << lastError(); + + return maybeMatches == 1; +} + +QOlmSession::QOlmSession(OlmSession *session) + : m_session(session) +{} diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h new file mode 100644 index 00000000..400fb854 --- /dev/null +++ b/lib/e2ee/qolmsession.h @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" +#include "e2ee/qolmaccount.h" + +struct OlmSession; + +namespace Quotient { + +//! Either an outbound or inbound session for secure communication. +class QUOTIENT_API QOlmSession +{ +public: + ~QOlmSession(); + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + static QOlmExpected<QOlmSessionPtr> createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage); + + static QOlmExpected<QOlmSessionPtr> createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage); + + static QOlmExpected<QOlmSessionPtr> createOutboundSession( + QOlmAccount* account, const QByteArray& theirIdentityKey, + const QByteArray& theirOneTimeKey); + + //! Serialises an `QOlmSession` to encrypted Base64. + QByteArray pickle(const PicklingMode &mode) const; + + //! Deserialises from encrypted Base64 previously made with pickle() + static QOlmExpected<QOlmSessionPtr> unpickle(QByteArray&& pickled, + const PicklingMode& mode); + + //! Encrypts a plaintext message using the session. + QOlmMessage encrypt(const QByteArray& plaintext); + + //! Decrypts a message using this session. Decoding is lossy, meaning if + //! the decrypted plaintext contains invalid UTF-8 symbols, they will + //! be returned as `U+FFFD` (�). + QOlmExpected<QByteArray> decrypt(const QOlmMessage &message) const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! Checker for any received messages for this session. + bool hasReceivedMessage() const; + + //! Checks if the 'prekey' message is for this in-bound session. + bool matchesInboundSession(const QOlmMessage& preKeyMessage) const; + + //! Checks if the 'prekey' message is for this in-bound session. + bool matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const; + + friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) + { + return lhs.sessionId() < rhs.sessionId(); + } + + friend bool operator<(const QOlmSessionPtr& lhs, const QOlmSessionPtr& rhs) + { + return *lhs < *rhs; + } + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + + OlmSession* raw() const { return m_session; } + + QOlmSession(OlmSession* session); +private: + //! Helper function for creating new sessions and handling errors. + static OlmSession* create(); + static QOlmExpected<QOlmSessionPtr> createInbound( + QOlmAccount* account, const QOlmMessage& preKeyMessage, + bool from = false, const QString& theirIdentityKey = ""); + OlmSession* m_session; +}; +} //namespace Quotient diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp new file mode 100644 index 00000000..46f7f4f3 --- /dev/null +++ b/lib/e2ee/qolmutility.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutility.h" + +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmUtility::lastErrorCode() const { + return olm_utility_last_error_code(m_utility); +} + +const char* QOlmUtility::lastError() const +{ + return olm_utility_last_error(m_utility); +} + +QOlmUtility::QOlmUtility() +{ + auto utility = new uint8_t[olm_utility_size()]; + m_utility = olm_utility(utility); +} + +QOlmUtility::~QOlmUtility() +{ + olm_clear_utility(m_utility); + delete[](reinterpret_cast<uint8_t *>(m_utility)); +} + +QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const +{ + const auto outputLen = olm_sha256_length(m_utility); + QByteArray outputBuf(outputLen, '\0'); + olm_sha256(m_utility, inputBuf.data(), inputBuf.length(), + outputBuf.data(), outputBuf.length()); + + return QString::fromUtf8(outputBuf); +} + +QString QOlmUtility::sha256Utf8Msg(const QString &message) const +{ + return sha256Bytes(message.toUtf8()); +} + +bool QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message, + QByteArray signature) +{ + return olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), + message.size(), signature.data(), signature.size()) + == 0; +} diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h new file mode 100644 index 00000000..508767bf --- /dev/null +++ b/lib/e2ee/qolmutility.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" + +struct OlmUtility; + +namespace Quotient { + +//! Allows you to make use of crytographic hashing via SHA-2 and +//! verifying ed25519 signatures. +class QUOTIENT_API QOlmUtility +{ +public: + QOlmUtility(); + ~QOlmUtility(); + + //! Returns a sha256 of the supplied byte slice. + QString sha256Bytes(const QByteArray &inputBuf) const; + + //! Convenience function that converts the UTF-8 message + //! to bytes and then calls `sha256Bytes()`, returning its output. + QString sha256Utf8Msg(const QString &message) const; + + //! Verify a ed25519 signature. + //! \param key QByteArray The public part of the ed25519 key that signed the message. + //! \param message QByteArray The message that was signed. + //! \param signature QByteArray The signature of the message. + bool ed25519Verify(const QByteArray &key, + const QByteArray &message, QByteArray signature); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + +private: + OlmUtility *m_utility; +}; +} diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp new file mode 100644 index 00000000..c6e51bcd --- /dev/null +++ b/lib/e2ee/qolmutils.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutils.h" +#include <QtCore/QRandomGenerator> + +using namespace Quotient; + +QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) +{ + if (std::holds_alternative<Quotient::Unencrypted>(mode)) { + return {}; + } + return std::get<Quotient::Encrypted>(mode).key; +} + +RandomBuffer::RandomBuffer(size_t size) + : QByteArray(static_cast<int>(size), '\0') +{ + QRandomGenerator::system()->generate(begin(), end()); +} diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h new file mode 100644 index 00000000..17eee7a3 --- /dev/null +++ b/lib/e2ee/qolmutils.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QByteArray> + +#include "e2ee/e2ee.h" + +namespace Quotient { + +// Convert PicklingMode to key +QUOTIENT_API QByteArray toKey(const PicklingMode &mode); + +class QUOTIENT_API RandomBuffer : public QByteArray { +public: + explicit RandomBuffer(size_t size); + ~RandomBuffer() { clear(); } + + // NOLINTNEXTLINE(google-explicit-constructor) + QUO_IMPLICIT operator void*() { return data(); } + char* chars() { return data(); } + uint8_t* bytes() { return reinterpret_cast<uint8_t*>(data()); } + + Q_DISABLE_COPY(RandomBuffer) + RandomBuffer(RandomBuffer&&) = default; + void operator=(RandomBuffer&&) = delete; +}; + +[[deprecated("Create RandomBuffer directly")]] inline auto getRandom( + size_t bufferSize) +{ + return RandomBuffer(bufferSize); +} + +#define QOLM_INTERNAL_ERROR_X(Message_, LastError_) \ + qFatal("%s, internal error: %s", Message_, LastError_) + +#define QOLM_INTERNAL_ERROR(Message_) \ + QOLM_INTERNAL_ERROR_X(Message_, lastError()) + +#define QOLM_FAIL_OR_LOG_X(InternalCondition_, Message_, LastErrorText_) \ + do { \ + const QString errorMsg{ (Message_) }; \ + if (InternalCondition_) \ + QOLM_INTERNAL_ERROR_X(qPrintable(errorMsg), (LastErrorText_)); \ + qWarning(E2EE).nospace() << errorMsg << ": " << (LastErrorText_); \ + } while (false) /* End of macro */ + +#define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_) \ + QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), \ + lastError()) + +} // namespace Quotient diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 79ef769c..a2e2a156 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -1,19 +1,32 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "eventitem.h" + +#include "events/roomavatarevent.h" +#include "events/roommessageevent.h" + +using namespace Quotient; + +void PendingEventItem::setFileUploaded(const FileSourceInfo& uploadedFileData) +{ + // TODO: eventually we might introduce hasFileContent to RoomEvent, + // and unify the code below. + if (auto* rme = getAs<RoomMessageEvent>()) { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([&uploadedFileData](EventContent::TypedBase& ec) { + ec.fileInfo()->source = uploadedFileData; + }); + } + if (auto* rae = getAs<RoomAvatarEvent>()) { + Q_ASSERT(rae->content().fileInfo()); + rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) { + fi.source = uploadedFileData; + }); + } + setStatus(EventStatus::FileUploaded); +} + +// Not exactly sure why but this helps with the linker not finding +// Quotient::EventStatus::staticMetaObject when building Quaternion +#include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index 5f1d10c9..96e45b38 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -1,151 +1,150 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "quotient_common.h" + +#include "events/callevents.h" +#include "events/filesourceinfo.h" #include "events/stateevent.h" +#include <any> #include <utility> -namespace QMatrixClient -{ - class StateEventBase; +namespace Quotient { + +namespace EventStatus { + Q_NAMESPACE_EXPORT(QUOTIENT_API) + + /** Special marks an event can assume + * + * This is used to hint at a special status of some events in UI. + * All values except Redacted and Hidden are mutually exclusive. + */ + enum Code { + Normal = 0x0, ///< No special designation + Submitted = 0x01, ///< The event has just been submitted for sending + FileUploaded = 0x02, ///< The file attached to the event has been + /// uploaded to the server + Departed = 0x03, ///< The event has left the client + ReachedServer = 0x04, ///< The server has received the event + SendingFailed = 0x05, ///< The server could not receive the event + Redacted = 0x08, ///< The event has been redacted + Replaced = 0x10, ///< The event has been replaced + Hidden = 0x100, ///< The event should not be shown in the timeline + }; + Q_ENUM_NS(Code) +} // namespace EventStatus - class EventStatus +class QUOTIENT_API EventItemBase { +public: + explicit EventItemBase(RoomEventPtr&& e) : evt(std::move(e)) { - Q_GADGET - public: - /** Special marks an event can assume - * - * This is used to hint at a special status of some events in UI. - * Most status values are mutually exclusive. - */ - enum Code { - Normal = 0x0, //< No special designation - Submitted = 0x01, //< The event has just been submitted for sending - Departed = 0x02, //< The event has left the client - ReachedServer = 0x03, //< The server has received the event - SendingFailed = 0x04, //< The server could not receive the event - Redacted = 0x08, //< The event has been redacted - Hidden = 0x10, //< The event should be hidden - }; - Q_DECLARE_FLAGS(Status, Code) - Q_FLAG(Status) - }; + Q_ASSERT(evt); + } - class EventItemBase + const RoomEvent* event() const { return rawPtr(evt); } + const RoomEvent* get() const { return event(); } + template <EventClass<RoomEvent> EventT> + const EventT* viewAs() const { - public: - explicit EventItemBase(RoomEventPtr&& e) - : evt(std::move(e)) - { - Q_ASSERT(evt); - } - - const RoomEvent* event() const { return rawPtr(evt); } - const RoomEvent* get() const { return event(); } - template <typename EventT> - const EventT* viewAs() const { return eventCast<const EventT>(evt); } - const RoomEventPtr& operator->() const { return evt; } - const RoomEvent& operator*() const { return *evt; } - - // Used for event redaction - RoomEventPtr replaceEvent(RoomEventPtr&& other) - { - return std::exchange(evt, move(other)); - } - - private: - RoomEventPtr evt; - }; + return eventCast<const EventT>(evt); + } + const RoomEventPtr& operator->() const { return evt; } + const RoomEvent& operator*() const { return *evt; } - class TimelineItem : public EventItemBase + // Used for event redaction + RoomEventPtr replaceEvent(RoomEventPtr&& other) { - public: - // For compatibility with Qt containers, even though we use - // a std:: container now for the room timeline - using index_t = int; + return std::exchange(evt, move(other)); + } - TimelineItem(RoomEventPtr&& e, index_t number) - : EventItemBase(std::move(e)), idx(number) - { } + /// Store arbitrary data with the event item + void setUserData(std::any userData) { data = std::move(userData); } + /// Obtain custom data previously stored with the event item + const std::any& userdata() const { return data; } + std::any& userData() { return data; } - index_t index() const { return idx; } +protected: + template <EventClass<RoomEvent> EventT> + EventT* getAs() + { + return eventCast<EventT>(evt); + } - private: - index_t idx; - }; +private: + RoomEventPtr evt; + std::any data; +}; + +class QUOTIENT_API TimelineItem : public EventItemBase { +public: + // For compatibility with Qt containers, even though we use + // a std:: container now for the room timeline + using index_t = int; + + TimelineItem(RoomEventPtr&& e, index_t number) + : EventItemBase(std::move(e)), idx(number) + {} + + index_t index() const { return idx; } - template<> - inline const StateEventBase* EventItemBase::viewAs<StateEventBase>() const +private: + index_t idx; +}; + +template <> +inline const StateEvent* EventItemBase::viewAs<StateEvent>() const +{ + return evt->isStateEvent() ? weakPtrCast<const StateEvent>(evt) : nullptr; +} + +template <> +inline const CallEvent* EventItemBase::viewAs<CallEvent>() const +{ + return evt->is<CallEvent>() ? weakPtrCast<const CallEvent>(evt) : nullptr; +} + +class QUOTIENT_API PendingEventItem : public EventItemBase { +public: + using EventItemBase::EventItemBase; + + EventStatus::Code deliveryStatus() const { return _status; } + QDateTime lastUpdated() const { return _lastUpdated; } + QString annotation() const { return _annotation; } + + void setDeparted() { setStatus(EventStatus::Departed); } + void setFileUploaded(const FileSourceInfo &uploadedFileData); + void setReachedServer(const QString& eventId) { - return evt->isStateEvent() ? weakPtrCast<const StateEventBase>(evt) - : nullptr; + setStatus(EventStatus::ReachedServer); + (*this)->addId(eventId); } - - template<> - inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const + void setSendingFailed(QString errorText) { - return evt->isCallEvent() ? weakPtrCast<const CallEventBase>(evt) - : nullptr; + setStatus(EventStatus::SendingFailed); + _annotation = std::move(errorText); } + void resetStatus() { setStatus(EventStatus::Submitted); } - class PendingEventItem : public EventItemBase - { - Q_GADGET - public: - using EventItemBase::EventItemBase; - - EventStatus::Code deliveryStatus() const { return _status; } - QDateTime lastUpdated() const { return _lastUpdated; } - QString annotation() const { return _annotation; } - - void setDeparted() { setStatus(EventStatus::Departed); } - void setReachedServer(const QString& eventId) - { - setStatus(EventStatus::ReachedServer); - (*this)->addId(eventId); - } - void setSendingFailed(QString errorText) - { - setStatus(EventStatus::SendingFailed); - _annotation = std::move(errorText); - } - void resetStatus() { setStatus(EventStatus::Submitted); } - - private: - EventStatus::Code _status = EventStatus::Submitted; - QDateTime _lastUpdated = QDateTime::currentDateTimeUtc(); - QString _annotation; - - void setStatus(EventStatus::Code status) - { - _status = status; - _lastUpdated = QDateTime::currentDateTimeUtc(); - _annotation.clear(); - } - }; +private: + EventStatus::Code _status = EventStatus::Submitted; + QDateTime _lastUpdated = QDateTime::currentDateTimeUtc(); + QString _annotation; - inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) + void setStatus(EventStatus::Code status) { - QDebugStateSaver dss(d); - d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; - return d; + _status = status; + _lastUpdated = QDateTime::currentDateTimeUtc(); + _annotation.clear(); } +}; + +inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) +{ + QDebugStateSaver dss(d); + d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; + return d; } -Q_DECLARE_METATYPE(QMatrixClient::EventStatus) +} // namespace Quotient diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index d1c1abc8..324ce449 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -1,99 +1,65 @@ -#include <utility> - -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "event.h" -#include "eventcontent.h" -#include "converters.h" - -namespace QMatrixClient -{ - constexpr const char* FavouriteTag = "m.favourite"; - constexpr const char* LowPriorityTag = "m.lowpriority"; - struct TagRecord - { - using order_type = Omittable<float>; +namespace Quotient { +constexpr auto FavouriteTag [[maybe_unused]] = "m.favourite"_ls; +constexpr auto LowPriorityTag [[maybe_unused]] = "m.lowpriority"_ls; +constexpr auto ServerNoticeTag [[maybe_unused]] = "m.server_notice"_ls; - order_type order; +struct TagRecord { + Omittable<float> order = none; +}; - TagRecord (order_type order = none) : order(order) { } - explicit TagRecord(const QJsonObject& jo) - { - // Parse a float both from JSON double and JSON string because - // libqmatrixclient previously used to use strings to store order. - const auto orderJv = jo.value("order"_ls); - if (orderJv.isDouble()) - order = fromJson<float>(orderJv); - else if (orderJv.isString()) - { - bool ok; - order = orderJv.toString().toFloat(&ok); - if (!ok) - order = none; - } - } +inline bool operator<(TagRecord lhs, TagRecord rhs) +{ + // Per The Spec, rooms with no order should be after those with order, + // against std::optional<>::operator<() convention. + return lhs.order && (!rhs.order || *lhs.order < *rhs.order); +} - bool operator<(const TagRecord& other) const - { - // Per The Spec, rooms with no order should be after those with order - return !order.omitted() && - (other.order.omitted() || order.value() < other.order.value()); +template <> +struct JsonObjectConverter<TagRecord> { + static void fillFrom(const QJsonObject& jo, TagRecord& rec) + { + // Parse a float both from JSON double and JSON string because + // the library previously used to use strings to store order. + const auto orderJv = jo.value("order"_ls); + if (orderJv.isDouble()) + rec.order = fromJson<float>(orderJv); + if (orderJv.isString()) { + bool ok = false; + rec.order = orderJv.toString().toFloat(&ok); + if (!ok) + rec.order = none; } - }; - - inline QJsonValue toJson(const TagRecord& rec) + } + static void dumpTo(QJsonObject& jo, TagRecord rec) { - QJsonObject o; - addParam<IfNotEmpty>(o, QStringLiteral("order"), rec.order); - return o; + addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); } +}; - using TagsMap = QHash<QString, TagRecord>; +using TagsMap = QHash<QString, TagRecord>; -#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public Event \ - { \ - public: \ - using content_type = _ContentType; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(QJsonObject obj) \ - : Event(typeId(), std::move(obj)) \ - { } \ - explicit _Name(_ContentType content) \ - : Event(typeId(), matrixTypeId(), \ - QJsonObject { { QStringLiteral(#_ContentKey), \ - toJson(std::move(content)) } }) \ - { } \ - auto _ContentKey() const \ - { return fromJson<content_type>(contentJson()[#_ContentKey##_ls]); } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ - // End of macro - - DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", TagsMap, tags) - DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", QString, event_id) - DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, "m.ignored_user_list", - QSet<QString>, ignored_users) - - DEFINE_EVENTTYPE_ALIAS(Tag, TagEvent) - DEFINE_EVENTTYPE_ALIAS(ReadMarker, ReadMarkerEvent) -} +DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags, "tags") +DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString, + eventId, "event_id") +class ReadMarkerEvent : public ReadMarkerEventImpl { +public: + using ReadMarkerEventImpl::ReadMarkerEventImpl; + [[deprecated("Use ReadMarkerEvent::eventId() instead")]] + auto event_id() const { return eventId(); } +}; +DEFINE_SIMPLE_EVENT(IgnoredUsersEventImpl, Event, "m.ignored_user_list", + QSet<QString>, ignoredUsers, "ignored_users") +class IgnoredUsersEvent : public IgnoredUsersEventImpl { +public: + using IgnoredUsersEventImpl::IgnoredUsersEventImpl; + [[deprecated("Use IgnoredUsersEvent::ignoredUsers() instead")]] + auto ignored_users() const { return ignoredUsers(); } +}; +} // namespace Quotient diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp deleted file mode 100644 index d2862241..00000000 --- a/lib/events/callanswerevent.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "callanswerevent.h" - -#include "event.h" - -#include "logging.h" - -#include <QtCore/QJsonDocument> - -/* -m.call.answer -{ - "age": 242352, - "content": { - "answer": { - "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", - "type": "answer" - }, - "call_id": "12345", - "lifetime": 60000, - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.answer" -} -*/ - -using namespace QMatrixClient; - -CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Answer event"; -} - -CallAnswerEvent::CallAnswerEvent(const QString& callId, const int lifetime, - const QString& sdp) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("lifetime"), lifetime } - , { QStringLiteral("answer"), QJsonObject { - { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } - }) -{ } - -CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("answer"), QJsonObject { - { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } - }) -{ } diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h deleted file mode 100644 index 2d9e5bb0..00000000 --- a/lib/events/callanswerevent.h +++ /dev/null @@ -1,45 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace QMatrixClient -{ - class CallAnswerEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) - - explicit CallAnswerEvent(const QJsonObject& obj); - - explicit CallAnswerEvent(const QString& callId, const int lifetime, - const QString& sdp); - explicit CallAnswerEvent(const QString& callId, const QString& sdp); - - int lifetime() const { return content<int>("lifetime"_ls); } // FIXME: Omittable<>? - QString sdp() const { - return contentJson()["answer"_ls].toObject() - .value("sdp"_ls).toString(); - } - }; - - REGISTER_EVENT_TYPE(CallAnswerEvent) - DEFINE_EVENTTYPE_ALIAS(CallAnswer, CallAnswerEvent) -} // namespace QMatrixClient diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp deleted file mode 100644 index 52cd1856..00000000 --- a/lib/events/callcandidatesevent.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "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 deleted file mode 100644 index 4618832c..00000000 --- a/lib/events/callcandidatesevent.h +++ /dev/null @@ -1,48 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace QMatrixClient -{ - class CallCandidatesEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.candidates", CallCandidatesEvent) - - explicit CallCandidatesEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) - { } - - explicit CallCandidatesEvent(const QString& callId, - const QJsonArray& candidates) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - {{ QStringLiteral("candidates"), candidates }}) - { } - - QJsonArray candidates() const - { - return content<QJsonArray>("candidates"_ls); - } - }; - - REGISTER_EVENT_TYPE(CallCandidatesEvent) - DEFINE_EVENTTYPE_ALIAS(CallCandidates, CallCandidatesEvent) -} diff --git a/lib/events/callevents.cpp b/lib/events/callevents.cpp new file mode 100644 index 00000000..3873614d --- /dev/null +++ b/lib/events/callevents.cpp @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "callevents.h" + +#include "logging.h" + +using namespace Quotient; + +QJsonObject CallEvent::basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson) +{ + contentJson.insert(QStringLiteral("call_id"), callId); + contentJson.insert(QStringLiteral("version"), version); + return RoomEvent::basicJson(matrixType, contentJson); +} + +CallEvent::CallEvent(const QJsonObject& json) + : RoomEvent(json) +{ + if (callId().isEmpty()) + qCWarning(EVENTS) << id() << "is a call event with an empty call id"; +} + +/* +m.call.invite +{ + "age": 242352, + "content": { + "call_id": "12345", + "lifetime": 60000, + "offer": { + "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", + "type": "offer" + }, + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.invite" +} +*/ + +CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime, + const QString& sdp) + : EventTemplate( + callId, + { { QStringLiteral("lifetime"), lifetime }, + { QStringLiteral("offer"), + QJsonObject{ { QStringLiteral("type"), QStringLiteral("offer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} + +/* +m.call.answer +{ + "age": 242352, + "content": { + "answer": { + "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", + "type": "answer" + }, + "call_id": "12345", + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.answer" +} +*/ + +CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) + : EventTemplate(callId, { { QStringLiteral("answer"), + QJsonObject { { QStringLiteral("type"), + QStringLiteral("answer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} diff --git a/lib/events/callevents.h b/lib/events/callevents.h new file mode 100644 index 00000000..752e331d --- /dev/null +++ b/lib/events/callevents.h @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" + +namespace Quotient { + +class QUOTIENT_API CallEvent : public RoomEvent { +public: + QUO_BASE_EVENT(CallEvent, "m.call.*"_ls, RoomEvent::BaseMetaType) + static bool matches(const QJsonObject&, const QString& mType) + { + return mType.startsWith("m.call."); + } + + QUO_CONTENT_GETTER(QString, callId) + QUO_CONTENT_GETTER(int, version) + +protected: + explicit CallEvent(const QJsonObject& json); + + static QJsonObject basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson = {}); +}; +using CallEventBase + [[deprecated("CallEventBase is CallEvent now")]] = CallEvent; + +template <typename EventT> +class EventTemplate<EventT, CallEvent> : public CallEvent { +public: + using CallEvent::CallEvent; + explicit EventTemplate(const QString& callId, + const QJsonObject& contentJson = {}) + : EventTemplate(basicJson(EventT::TypeId, callId, 0, contentJson)) + {} +}; + +template <typename EventT, typename ContentT> +class EventTemplate<EventT, CallEvent, ContentT> + : public EventTemplate<EventT, CallEvent> { +public: + using EventTemplate<EventT, CallEvent>::EventTemplate; + template <typename... ContentParamTs> + explicit EventTemplate(const QString& callId, + ContentParamTs&&... contentParams) + : EventTemplate<EventT, CallEvent>( + callId, + toJson(ContentT{ std::forward<ContentParamTs>(contentParams)... })) + {} +}; + +class QUOTIENT_API CallInviteEvent + : public EventTemplate<CallInviteEvent, CallEvent> { +public: + QUO_EVENT(CallInviteEvent, "m.call.invite") + + using EventTemplate::EventTemplate; + + explicit CallInviteEvent(const QString& callId, int lifetime, + const QString& sdp); + + QUO_CONTENT_GETTER(int, lifetime) + QString sdp() const + { + return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString(); + } +}; + +DEFINE_SIMPLE_EVENT(CallCandidatesEvent, CallEvent, "m.call.candidates", + QJsonArray, candidates, "candidates") + +class QUOTIENT_API CallAnswerEvent + : public EventTemplate<CallAnswerEvent, CallEvent> { +public: + QUO_EVENT(CallAnswerEvent, "m.call.answer") + + using EventTemplate::EventTemplate; + + explicit CallAnswerEvent(const QString& callId, const QString& sdp); + + QString sdp() const + { + return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString(); + } +}; + +class QUOTIENT_API CallHangupEvent + : public EventTemplate<CallHangupEvent, CallEvent> { +public: + QUO_EVENT(CallHangupEvent, "m.call.hangup") + using EventTemplate::EventTemplate; +}; + +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::CallEvent*) +Q_DECLARE_METATYPE(const Quotient::CallEvent*) diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp deleted file mode 100644 index b1154806..00000000 --- a/lib/events/callhangupevent.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "callhangupevent.h" - -#include "event.h" - -#include "logging.h" - -#include <QtCore/QJsonDocument> - -/* -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 QMatrixClient; - - -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 deleted file mode 100644 index c74e20d5..00000000 --- a/lib/events/callhangupevent.h +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace QMatrixClient -{ - class CallHangupEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) - - explicit CallHangupEvent(const QJsonObject& obj); - explicit CallHangupEvent(const QString& callId); - }; - - REGISTER_EVENT_TYPE(CallHangupEvent) - DEFINE_EVENTTYPE_ALIAS(CallHangup, CallHangupEvent) -} diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp deleted file mode 100644 index bca3f296..00000000 --- a/lib/events/callinviteevent.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "callinviteevent.h" - -#include "event.h" - -#include "logging.h" - -#include <QtCore/QJsonDocument> - -/* -m.call.invite -{ - "age": 242352, - "content": { - "call_id": "12345", - "lifetime": 60000, - "offer": { - "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", - "type": "offer" - }, - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.invite" -} -*/ - -using namespace QMatrixClient; - -CallInviteEvent::CallInviteEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Invite event"; -} - -CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime, - const QString& sdp) - : CallEventBase(typeId(), matrixTypeId(), callId, lifetime, - { { QStringLiteral("lifetime"), lifetime } - , { QStringLiteral("offer"), QJsonObject { - { QStringLiteral("type"), QStringLiteral("offer") }, - { QStringLiteral("sdp"), sdp } } - }}) -{ } diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h deleted file mode 100644 index d5315309..00000000 --- a/lib/events/callinviteevent.h +++ /dev/null @@ -1,44 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace QMatrixClient -{ - class CallInviteEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) - - explicit CallInviteEvent(const QJsonObject& obj); - - explicit CallInviteEvent(const QString& callId, const int lifetime, - const QString& sdp); - - int lifetime() const { return content<int>("lifetime"_ls); } // FIXME: Omittable<>? - QString sdp() const { - return contentJson()["offer"_ls].toObject() - .value("sdp"_ls).toString(); - } - }; - - REGISTER_EVENT_TYPE(CallInviteEvent) - DEFINE_EVENTTYPE_ALIAS(CallInvite, CallInviteEvent) -} diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index 266d60d8..83bb1e32 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -1,38 +1,20 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "directchatevent.h" -#include <QtCore/QJsonArray> - -using namespace QMatrixClient; +using namespace Quotient; QMultiHash<QString, QString> DirectChatEvent::usersToDirectChats() const { QMultiHash<QString, QString> result; const auto& json = contentJson(); - for (auto it = json.begin(); it != json.end(); ++it) - { + for (auto it = json.begin(); it != json.end(); ++it) { // Beware of range-for's over temporary returned from temporary // (see the bottom of // http://en.cppreference.com/w/cpp/language/range-for#Explanation) const auto roomIds = it.value().toArray(); - for (const auto& roomIdValue: roomIds) + for (const auto& roomIdValue : roomIds) result.insert(it.key(), roomIdValue.toString()); } return result; diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index 7559796b..0756d816 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -1,38 +1,17 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "event.h" -namespace QMatrixClient -{ - class DirectChatEvent : public Event - { - public: - DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) +namespace Quotient { +class QUOTIENT_API DirectChatEvent : public Event { +public: + QUO_EVENT(DirectChatEvent, "m.direct") - explicit DirectChatEvent(const QJsonObject& obj) - : Event(typeId(), obj) - { } + using Event::Event; - QMultiHash<QString, QString> usersToDirectChats() const; - }; - REGISTER_EVENT_TYPE(DirectChatEvent) - DEFINE_EVENTTYPE_ALIAS(DirectChat, DirectChatEvent) -} + QMultiHash<QString, QString> usersToDirectChats() const; +}; +} // namespace Quotient diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp new file mode 100644 index 00000000..540594d1 --- /dev/null +++ b/lib/events/encryptedevent.cpp @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "encryptedevent.h" +#include "e2ee/e2ee.h" +#include "logging.h" + +using namespace Quotient; + +EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertexts, + const QString& senderKey) + : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, + { CiphertextKeyL, ciphertexts }, + { SenderKeyKeyL, senderKey } })) +{} + +EncryptedEvent::EncryptedEvent(const QByteArray& ciphertext, + const QString& senderKey, + const QString& deviceId, const QString& sessionId) + : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, + { CiphertextKeyL, QString(ciphertext) }, + { DeviceIdKeyL, deviceId }, + { SenderKeyKeyL, senderKey }, + { SessionIdKeyL, sessionId } })) +{} + +EncryptedEvent::EncryptedEvent(const QJsonObject& obj) + : RoomEvent(obj) +{ + qCDebug(E2EE) << "Encrypted event from" << senderId(); +} + +QString EncryptedEvent::algorithm() const +{ + const auto algo = contentPart<QString>(AlgorithmKeyL); + if (!isSupportedAlgorithm(algo)) + qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + + return algo; +} + +RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const +{ + auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); + eventObject["event_id"] = id(); + eventObject["sender"] = senderId(); + eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch(); + if (const auto relatesToJson = contentPart<QJsonObject>("m.relates_to"_ls); + !relatesToJson.isEmpty()) { + auto content = eventObject["content"].toObject(); + content["m.relates_to"] = relatesToJson; + eventObject["content"] = content; + } + if (const auto redactsJson = unsignedPart<QString>("redacts"_ls); + !redactsJson.isEmpty()) { + auto unsign = eventObject["unsigned"].toObject(); + unsign["redacts"] = redactsJson; + eventObject["unsigned"] = unsign; + } + return loadEvent<RoomEvent>(eventObject); +} + +void EncryptedEvent::setRelation(const QJsonObject& relation) +{ + auto content = contentJson(); + content["m.relates_to"] = relation; + editJson()["content"] = content; +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h new file mode 100644 index 00000000..e24e5745 --- /dev/null +++ b/lib/events/encryptedevent.h @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" + +namespace Quotient { + +constexpr auto CiphertextKeyL = "ciphertext"_ls; +constexpr auto SenderKeyKeyL = "sender_key"_ls; +constexpr auto DeviceIdKeyL = "device_id"_ls; +constexpr auto SessionIdKeyL = "session_id"_ls; + +/* + * While the specification states: + * + * "This event type is used when sending encrypted events. + * It can be used either within a room + * (in which case it will have all of the Room Event fields), + * or as a to-device event." + * "The encrypted payload can contain any message event." + * https://matrix.org/docs/spec/client_server/latest#id493 + * + * -- for most of the cases the message event is the room message event. + * And even for the to-device events the context is for the room. + * + * So, to simplify integration to the timeline, EncryptedEvent is a RoomEvent + * inheritor. Strictly speaking though, it's not always a RoomEvent, but an Event + * in general. It's possible, because RoomEvent interface is similar to Event's + * one and doesn't add new restrictions, just provides additional features. + */ +class QUOTIENT_API EncryptedEvent : public RoomEvent { +public: + QUO_EVENT(EncryptedEvent, "m.room.encrypted") + + /* In case with Olm, the encrypted content of the event is + * a map from the recipient Curve25519 identity key to ciphertext + * information */ + explicit EncryptedEvent(const QJsonObject& ciphertexts, + const QString& senderKey); + /* In case with Megolm, device_id and session_id are required */ + explicit EncryptedEvent(const QByteArray& ciphertext, + const QString& senderKey, const QString& deviceId, + const QString& sessionId); + explicit EncryptedEvent(const QJsonObject& obj); + + QString algorithm() const; + QByteArray ciphertext() const + { + return contentPart<QString>(CiphertextKeyL).toLatin1(); + } + QJsonObject ciphertext(const QString& identityKey) const + { + return contentPart<QJsonObject>(CiphertextKeyL) + .value(identityKey) + .toObject(); + } + QString senderKey() const { return contentPart<QString>(SenderKeyKeyL); } + + /* device_id and session_id are required with Megolm */ + QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); } + QString sessionId() const { return contentPart<QString>(SessionIdKeyL); } + RoomEventPtr createDecrypted(const QString &decrypted) const; + + void setRelation(const QJsonObject& relation); +}; +} // namespace Quotient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp new file mode 100644 index 00000000..b1b04984 --- /dev/null +++ b/lib/events/encryptionevent.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "encryptionevent.h" +#include "logging.h" + +#include "e2ee/e2ee.h" + +using namespace Quotient; + +static constexpr std::array encryptionStrings { MegolmV1AesSha2AlgoKey }; + +template <> +EncryptionType Quotient::fromJson(const QJsonValue& jv) +{ + const auto& encryptionString = jv.toString(); + for (auto it = encryptionStrings.begin(); it != encryptionStrings.end(); + ++it) + if (encryptionString == *it) + return EncryptionType(it - encryptionStrings.begin()); + + if (!encryptionString.isEmpty()) + qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; + return EncryptionType::Undefined; +} + +EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) + : encryption(fromJson<Quotient::EncryptionType>(json[AlgorithmKeyL])) + , algorithm(sanitized(json[AlgorithmKeyL].toString())) +{ + // NB: fillFromJson only fills the variable if the JSON key exists + fillFromJson<int>(json[RotationPeriodMsKeyL], rotationPeriodMs); + fillFromJson<int>(json[RotationPeriodMsgsKeyL], rotationPeriodMsgs); +} + +EncryptionEventContent::EncryptionEventContent(Quotient::EncryptionType et) + : encryption(et) +{ + if(encryption != Quotient::EncryptionType::Undefined) { + algorithm = encryptionStrings[static_cast<size_t>(encryption)]; + } +} + +QJsonObject EncryptionEventContent::toJson() const +{ + QJsonObject o; + if (encryption != Quotient::EncryptionType::Undefined) + o.insert(AlgorithmKey, algorithm); + o.insert(RotationPeriodMsKey, rotationPeriodMs); + o.insert(RotationPeriodMsgsKey, rotationPeriodMsgs); + return o; +} diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h new file mode 100644 index 00000000..4bf7459c --- /dev/null +++ b/lib/events/encryptionevent.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "quotient_common.h" +#include "stateevent.h" + +namespace Quotient { +class QUOTIENT_API EncryptionEventContent { +public: + using EncryptionType + [[deprecated("Use Quotient::EncryptionType instead")]] = + Quotient::EncryptionType; + + // NOLINTNEXTLINE(google-explicit-constructor) + QUO_IMPLICIT EncryptionEventContent(Quotient::EncryptionType et); + explicit EncryptionEventContent(const QJsonObject& json); + + QJsonObject toJson() const; + + Quotient::EncryptionType encryption; + QString algorithm {}; + int rotationPeriodMs = 604'800'000; + int rotationPeriodMsgs = 100; +}; + +class QUOTIENT_API EncryptionEvent + : public KeylessStateEventBase<EncryptionEvent, EncryptionEventContent> { +public: + QUO_EVENT(EncryptionEvent, "m.room.encryption") + + using EncryptionType + [[deprecated("Use Quotient::EncryptionType instead")]] = + Quotient::EncryptionType; + + using KeylessStateEventBase::KeylessStateEventBase; + + Quotient::EncryptionType encryption() const { return content().encryption; } + QString algorithm() const { return content().algorithm; } + int rotationPeriodMs() const { return content().rotationPeriodMs; } + int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } + + bool useEncryption() const { return !algorithm().isEmpty(); } +}; +} // namespace Quotient diff --git a/lib/events/event.cpp b/lib/events/event.cpp index fd6e3939..da7de919 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -1,72 +1,67 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "event.h" +#include "callevents.h" #include "logging.h" +#include "stateevent.h" #include <QtCore/QJsonDocument> -using namespace QMatrixClient; +using namespace Quotient; -event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) -{ - const auto id = get().eventTypes.size(); - get().eventTypes.push_back(matrixTypeId); - if (strncmp(matrixTypeId, "", 1) == 0) - qDebug(EVENTS) << "Initialized unknown event type with id" << id; - else - qDebug(EVENTS) << "Initialized event type" << matrixTypeId - << "with id" << id; - return id; -} +QString EventTypeRegistry::getMatrixType(event_type_t typeId) { return typeId; } -QString EventTypeRegistry::getMatrixType(event_type_t typeId) +void AbstractEventMetaType::addDerived(AbstractEventMetaType* newType) { - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : ""; + if (const auto existing = + std::find_if(derivedTypes.cbegin(), derivedTypes.cend(), + [&newType](const AbstractEventMetaType* t) { + return t->matrixId == newType->matrixId; + }); + existing != derivedTypes.cend()) + { + if (*existing == newType) + return; + // Two different metatype objects claim the same Matrix type id; this + // is not normal, so give as much information as possible to diagnose + if ((*existing)->className == newType->className) { + qCritical(EVENTS) + << newType->className << "claims" << newType->matrixId + << "repeatedly; check that it's exported across translation " + "units or shared objects"; + Q_ASSERT(false); // That situation is plain wrong + return; // So maybe std::terminate() even? + } + qWarning(EVENTS).nospace() + << newType->matrixId << " is already mapped to " + << (*existing)->className << " before " << newType->className + << "; unless the two have different isValid() conditions, the " + "latter class will never be used"; + } + derivedTypes.emplace_back(newType); + qDebug(EVENTS).nospace() + << newType->matrixId << " -> " << newType->className << "; " + << derivedTypes.size() << " derived type(s) registered for " + << className; } -Event::Event(Type type, const QJsonObject& json) - : _type(type), _json(json) +Event::Event(const QJsonObject& json) + : _json(json) { - if (!json.contains(ContentKeyL) && - !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) - { + if (!json.contains(ContentKeyL) + && !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) { qCWarning(EVENTS) << "Event without 'content' node"; qCWarning(EVENTS) << formatJson << json; } } -Event::Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) - : Event(type, basicEventJson(matrixType, contentJson)) -{ } - Event::~Event() = default; -QString Event::matrixType() const -{ - return fullJson()[TypeKeyL].toString(); -} +QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } -QByteArray Event::originalJson() const -{ - return QJsonDocument(_json).toJson(); -} +QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); } const QJsonObject Event::contentJson() const { @@ -77,3 +72,12 @@ const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); } + +bool Event::isStateEvent() const { return is<StateEvent>(); } + +bool Event::isCallEvent() const { return is<CallEvent>(); } + +void Event::dumpTo(QDebug dbg) const +{ + dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); +} diff --git a/lib/events/event.h b/lib/events/event.h index e0d83976..0abef1f0 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -1,396 +1,637 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "converters.h" -#include "logging.h" +#include "function_traits.h" +#include "single_key_value.h" -#ifdef ENABLE_EVENTTYPE_ALIAS -#define USE_EVENTTYPE_ALIAS 1 -#endif +namespace Quotient { +// === event_ptr_tt<> and basic type casting facilities === -namespace QMatrixClient -{ - // === event_ptr_tt<> and type casting facilities === +template <typename EventT> +using event_ptr_tt = std::unique_ptr<EventT>; - template <typename EventT> - using event_ptr_tt = std::unique_ptr<EventT>; +/// Unwrap a plain pointer from a smart pointer +template <typename EventT> +inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) +{ + return ptr.get(); +} - template <typename EventT> - inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) // unwrap +/// Unwrap a plain pointer and downcast it to the specified type +template <typename TargetEventT, typename EventT> +inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) +{ + return static_cast<TargetEventT*>(rawPtr(ptr)); +} + +// === Standard Matrix key names and basicEventJson() === + +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 }; + +using event_type_t = QLatin1String; + +// TODO: Remove in 0.8 +struct QUOTIENT_API EventTypeRegistry { + [[deprecated("event_type_t is a string since libQuotient 0.7, use it directly instead")]] + static QString getMatrixType(event_type_t typeId); + + EventTypeRegistry() = delete; + ~EventTypeRegistry() = default; + Q_DISABLE_COPY_MOVE(EventTypeRegistry) +}; + +// === EventMetaType === + +class Event; + +// TODO: move over to std::derived_from<Event> once it's available everywhere +template <typename EventT, typename BaseEventT = Event> +concept EventClass = std::is_base_of_v<BaseEventT, EventT>; + +template <EventClass EventT> +bool is(const Event& e); + +//! \brief The base class for event metatypes +//! +//! You should not normally have to use this directly, unless you need to devise +//! a whole new kind of event metatypes. +class QUOTIENT_API AbstractEventMetaType { +public: + // The public fields here are const and are not to be changeable anyway. + // NOLINTBEGIN(misc-non-private-member-variables-in-classes) + const char* const className; + const event_type_t matrixId; + const AbstractEventMetaType* const baseType = nullptr; + // NOLINTEND(misc-non-private-member-variables-in-classes) + + explicit AbstractEventMetaType(const char* className) + : className(className) + {} + explicit AbstractEventMetaType(const char* className, event_type_t matrixId, + AbstractEventMetaType& nearestBase) + : className(className), matrixId(matrixId), baseType(&nearestBase) { - return ptr.get(); + nearestBase.addDerived(this); } - template <typename TargetEventT, typename EventT> - inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) - { - return static_cast<TargetEventT*>(rawPtr(ptr)); - } + void addDerived(AbstractEventMetaType *newType); - template <typename TargetT, typename SourceT> - inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr) - { - return unique_ptr_cast<TargetT>(ptr); - } + virtual ~AbstractEventMetaType() = default; - // === Standard Matrix key names and basicEventJson() === - - static const auto TypeKey = QStringLiteral("type"); - static const auto ContentKey = QStringLiteral("content"); - static const auto EventIdKey = QStringLiteral("event_id"); - static const auto UnsignedKey = QStringLiteral("unsigned"); - static const auto TypeKeyL = "type"_ls; - static const auto ContentKeyL = "content"_ls; - static const auto EventIdKeyL = "event_id"_ls; - static const auto UnsignedKeyL = "unsigned"_ls; - static const auto RedactedCauseKeyL = "redacted_because"_ls; - static const auto PrevContentKeyL = "prev_content"_ls; - - // Minimal correct Matrix event JSON - template <typename StrT> - inline QJsonObject basicEventJson(StrT matrixType, - const QJsonObject& content) - { - return { { TypeKey, std::forward<StrT>(matrixType) }, - { ContentKey, content } }; - } +protected: + // Allow template specialisations to call into one another + template <class EventT> + friend class EventMetaType; - // === Event types and event types registry === + // The returned value indicates whether a generic object has to be created + // on the top level when `event` is empty, instead of returning nullptr + virtual bool doLoadFrom(const QJsonObject& fullJson, const QString& type, + Event*& event) const = 0; - using event_type_t = size_t; - using event_mtype_t = const char*; +private: + std::vector<const AbstractEventMetaType*> derivedTypes{}; + Q_DISABLE_COPY_MOVE(AbstractEventMetaType) +}; - class EventTypeRegistry +// Any event metatype is unique (note Q_DISABLE_COPY_MOVE above) so can be +// identified by its address +inline bool operator==(const AbstractEventMetaType& lhs, + const AbstractEventMetaType& rhs) +{ + return &lhs == &rhs; +} + +//! \brief A family of event meta-types to load and match events +//! +//! TL;DR for the loadFrom() story: +//! - for base event types, use QUO_BASE_EVENT and, if you have additional +//! validation (e.g., JSON has to contain a certain key - see StateEvent +//! for a real example), define it in the static EventT::isValid() member +//! function accepting QJsonObject and returning bool. +//! - for leaf (specific) event types - simply use QUO_EVENT and it will do +//! everything necessary, including the TypeId definition. +//! \sa QUO_EVENT, QUO_BASE_EVENT +template <class EventT> +class QUOTIENT_API EventMetaType : public AbstractEventMetaType { + // Above: can't constrain EventT to be EventClass because it's incomplete + // at the point of EventMetaType<EventT> instantiation. +public: + using AbstractEventMetaType::AbstractEventMetaType; + + //! \brief Try to load an event from JSON, with dynamic type resolution + //! + //! The generic logic defined in this class template and invoked applies to + //! all event types defined in the library and boils down to the following: + //! 1. + //! a. If EventT has TypeId defined (which normally is a case of + //! all leaf - specific - event types, via QUO_EVENT macro) and + //! \p type doesn't exactly match it, nullptr is immediately returned. + //! b. In absence of TypeId, an event type is assumed to be a base; + //! its derivedTypes are examined, and this algorithm is applied + //! recursively on each. + //! 2. Optional validation: if EventT (or, due to the way inheritance works, + //! any of its base event types) has a static isValid() predicate and + //! the event JSON does not satisfy it, nullptr is immediately returned + //! to the upper level or to the loadFrom() caller. This is how existence + //! of `state_key` is checked in any type derived from StateEvent. + //! 3. If step 1b above returned non-nullptr, immediately return it. + //! 4. + //! a. If EventT::isValid() or EventT::TypeId (either, or both) exist and + //! are satisfied (see steps 1a and 2 above), an object of this type + //! is created from the passed JSON and returned. In case of a base + //! event type, this will be a generic (aka "unknown") event. + //! b. If neither exists, a generic event is only created and returned + //! when on the top level (i.e., outside of recursion into + //! derivedTypes); lower levels return nullptr instead and the type + //! lookup continues. The latter is a case of a derived base event + //! metatype (e.g. RoomEvent) called from its base event metatype + //! (i.e., Event). If no matching type derived from RoomEvent is found, + //! the nested lookup returns nullptr rather than a generic RoomEvent, + //! so that other types derived from Event could be examined. + event_ptr_tt<EventT> loadFrom(const QJsonObject& fullJson, + const QString& type) const { - public: - ~EventTypeRegistry() = default; - - static event_type_t initializeTypeId(event_mtype_t matrixTypeId); + Event* event = nullptr; + const bool goodEnough = doLoadFrom(fullJson, type, event); + if (!event && goodEnough) + return event_ptr_tt<EventT>{ new EventT(fullJson) }; + return event_ptr_tt<EventT>{ static_cast<EventT*>(event) }; + } - template <typename EventT> - static inline event_type_t initializeTypeId() - { - return initializeTypeId(EventT::matrixTypeId()); +private: + bool doLoadFrom(const QJsonObject& fullJson, const QString& type, + Event*& event) const override + { + if constexpr (requires { EventT::TypeId; }) { + if (EventT::TypeId != type) + return false; + } else { + for (const auto& p : derivedTypes) { + p->doLoadFrom(fullJson, type, event); + if (event) { + Q_ASSERT(is<EventT>(*event)); + return false; + } } + } + if constexpr (requires { EventT::isValid; }) { + if (!EventT::isValid(fullJson)) + return false; + } else if constexpr (!requires { EventT::TypeId; }) + return true; // Create a generic event object if on the top level + event = new EventT(fullJson); + return false; + } +}; - static QString getMatrixType(event_type_t typeId); - - private: - EventTypeRegistry() = default; - Q_DISABLE_COPY(EventTypeRegistry) - DISABLE_MOVE(EventTypeRegistry) - - static EventTypeRegistry& get() - { - static EventTypeRegistry etr; - return etr; - } +// === Event creation facilities === - std::vector<event_mtype_t> eventTypes; - }; +//! \brief Create an event of arbitrary type from its arguments +//! +//! This should not be used to load events from JSON - use loadEvent() for that. +//! \sa loadEvent +template <EventClass EventT, typename... ArgTs> +inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args) +{ + return std::make_unique<EventT>(std::forward<ArgTs>(args)...); +} - template <> - inline event_type_t EventTypeRegistry::initializeTypeId<void>() +template <EventClass EventT> +constexpr const auto& mostSpecificMetaType() +{ + if constexpr (requires { EventT::MetaType; }) + return EventT::MetaType; + else + return EventT::BaseMetaType; +} + +//! \brief Create an event with proper type from a JSON object +//! +//! Use this factory template to detect the type from the JSON object +//! contents (the detected event type should derive from the template +//! parameter type) and create an event object of that type. +template <EventClass EventT> +inline event_ptr_tt<EventT> loadEvent(const QJsonObject& fullJson) +{ + return mostSpecificMetaType<EventT>().loadFrom( + fullJson, fullJson[TypeKeyL].toString()); +} + +//! \brief Create an event from a type string and content JSON +//! +//! Use this template to resolve the C++ type from the Matrix type string in +//! \p matrixType and create an event of that type by passing all parameters +//! to BaseEventT::basicJson(). +template <EventClass EventT> +inline event_ptr_tt<EventT> loadEvent(const QString& matrixType, + const auto&... otherBasicJsonParams) +{ + return mostSpecificMetaType<EventT>().loadFrom( + EventT::basicJson(matrixType, otherBasicJsonParams...), matrixType); +} + +template <EventClass EventT> +struct JsonConverter<event_ptr_tt<EventT>> + : JsonObjectUnpacker<event_ptr_tt<EventT>> { + // No dump() to avoid any ambiguity on whether a given export to JSON uses + // fullJson() or only contentJson() + using JsonObjectUnpacker<event_ptr_tt<EventT>>::load; + static auto load(const QJsonObject& jo) { - return initializeTypeId(""); + return loadEvent<EventT>(jo); } +}; - template <typename EventT> - struct EventTypeTraits - { - static event_type_t id() - { - static const auto id = EventTypeRegistry::initializeTypeId<EventT>(); - return id; - } - }; +// === Event === - template <typename EventT> - inline event_type_t typeId() +class QUOTIENT_API Event { +public: + using Type = event_type_t; + static inline EventMetaType<Event> BaseMetaType { "Event" }; + virtual const AbstractEventMetaType& metaType() const { - return EventTypeTraits<std::decay_t<EventT>>::id(); + return BaseMetaType; } - inline event_type_t unknownEventTypeId() { return typeId<void>(); } + Q_DISABLE_COPY(Event) + Event(Event&&) noexcept = default; + Event& operator=(Event&&) = delete; + virtual ~Event(); - // === EventFactory === - - /** Create an event of arbitrary type from its arguments */ - template <typename EventT, typename... ArgTs> - inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args) + /// Make a minimal correct Matrix event JSON + static QJsonObject basicJson(const QString& matrixType, + const QJsonObject& content) { - return std::make_unique<EventT>(std::forward<ArgTs>(args)...); + return { { TypeKey, matrixType }, { ContentKey, content } }; } - template <typename BaseEventT> - class EventFactory + //! \brief Event Matrix type, as identified by its metatype object + //! + //! For generic/unknown events it will contain a descriptive/generic string + //! defined by the respective base event type (that can be empty). + //! \sa matrixType + Type type() const { return metaType().matrixId; } + + //! \brief Exact Matrix type stored in JSON + //! + //! Coincides with the result of type() (but is slower) for events defined + //! in C++ (not necessarily in the library); for generic/unknown events + //! the returned value will be different. + QString matrixType() const; + + template <EventClass EventT> + bool is() const { - public: - template <typename FnT> - static auto addMethod(FnT&& method) - { - factories().emplace_back(std::forward<FnT>(method)); - return 0; - } + return Quotient::is<EventT>(*this); + } - /** 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); - } + [[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(); } - 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; - } + const QJsonObject& fullJson() const { return _json; } - private: - static auto& factories() - { - using inner_factory_tt = - std::function<event_ptr_tt<BaseEventT>(const QJsonObject&, - const QString&)>; - static std::vector<inner_factory_tt> _factories {}; - return _factories; - } - }; + // According to the CS API spec, every event also has + // a "content" object; but since its structure is different for + // different types, we're implementing it per-event type. - /** 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() + // NB: const return types below are meant to catch accidental attempts + // to change event JSON (e.g., consider contentJson()["inexistentKey"]). + + const QJsonObject contentJson() const; + + //! \brief Get a part of the content object, assuming a given type + //! + //! This retrieves the value under `content.<key>` from the event JSON and + //! then converts it to \p T using fromJson(). + //! \sa contentJson, fromJson + template <typename T, typename KeyT> + const T contentPart(KeyT&& key) const { - 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; - }); + return fromJson<T>(contentJson()[std::forward<KeyT>(key)]); } - template <typename EventT> - inline auto registerEventType() + template <typename T> + [[deprecated("Use contentPart() to get a part of the event content")]] // + T content(const QString& key) const { - static const auto _ = setupFactory<EventT>(); - return _; // Only to facilitate usage in static initialisation + return contentPart<T>(key); } - // === Event === + const QJsonObject unsignedJson() const; - class Event + //! \brief Get a part of the unsigned object, assuming a given type + //! + //! This retrieves the value under `unsigned.<key>` from the event JSON and + //! then converts it to \p T using fromJson(). + //! \sa unsignedJson, fromJson + template <typename T, typename KeyT> + const T unsignedPart(KeyT&& key) const { - 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>; - - explicit Event(Type type, const QJsonObject& json); - explicit Event(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); - Q_DISABLE_COPY(Event) - Event(Event&&) = default; - Event& operator=(Event&&) = delete; - virtual ~Event(); - - Type type() const { return _type; } - QString matrixType() const; - QByteArray originalJson() const; - QJsonObject originalJsonObject() const { return fullJson(); } - - const QJsonObject& fullJson() const { return _json; } - - // According to the CS API spec, every event also has - // a "content" object; but since its structure is different for - // different types, we're implementing it per-event type. - - const QJsonObject contentJson() const; - const QJsonObject unsignedJson() const; - - template <typename T> - T content(const QString& key) const - { - return fromJson<T>(contentJson()[key]); - } - - template <typename T> - T content(const QLatin1String& key) const - { - return fromJson<T>(contentJson()[key]); - } - - virtual bool isStateEvent() const { return false; } - virtual bool isCallEvent() const { return false; } - - protected: - QJsonObject& editJson() { return _json; } - - private: - Type _type; - QJsonObject _json; - }; - using EventPtr = event_ptr_tt<Event>; - - template <typename EventT> - using EventsArray = std::vector<event_ptr_tt<EventT>>; - using Events = EventsArray<Event>; - - // === Macros used with event class definitions === - - // This macro should be used in a public section of an event class to - // provide matrixTypeId() and typeId(). -#define DEFINE_EVENT_TYPEID(_Id, _Type) \ - static constexpr event_mtype_t matrixTypeId() { return _Id; } \ - static auto typeId() { return QMatrixClient::typeId<_Type>(); } \ - // End of macro - - // This macro should be put after an event class definition (in .h or .cpp) - // to enable its deserialisation from a /sync and other - // polymorphic event arrays -#define REGISTER_EVENT_TYPE(_Type) \ - namespace { \ - [[gnu::unused]] \ - static const auto _factoryAdded##_Type = registerEventType<_Type>(); \ - } \ - // End of macro + return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]); + } -#ifdef USE_EVENTTYPE_ALIAS - namespace EventType + friend QUOTIENT_API QDebug operator<<(QDebug dbg, const Event& e) { - inline event_type_t logEventType(event_type_t id, const char* idName) - { - qDebug(EVENTS) << "Using id" << id << "for" << idName; - return id; - } + QDebugStateSaver _dss { dbg }; + dbg.noquote().nospace() + << e.matrixType() << '(' << e.metaType().className << "): "; + e.dumpTo(dbg); + return dbg; } - // This macro provides constants in EventType:: namespace for - // back-compatibility with libQMatrixClient 0.3 event type system. -#define DEFINE_EVENTTYPE_ALIAS(_Id, _Type) \ - namespace EventType \ - { \ - [[deprecated("Use is<>(), eventCast<>() or visit<>()")]] \ - static const auto _Id = logEventType(typeId<_Type>(), #_Id); \ - } \ + // State events are quite special in Matrix; so isStateEvent() is here, + // as an exception. For other base events, Event::is<>() and + // Quotient::is<>() should be used; don't add is* methods here + bool isStateEvent() const; + [[deprecated("Use is<CallEvent>() instead")]] bool isCallEvent() const; + +protected: + friend class EventMetaType<Event>; // To access the below constructor + + explicit Event(const QJsonObject& json); + + QJsonObject& editJson() { return _json; } + virtual void dumpTo(QDebug dbg) const; + +private: + QJsonObject _json; +}; +using EventPtr = event_ptr_tt<Event>; + +template <EventClass EventT> +using EventsArray = std::vector<event_ptr_tt<EventT>>; +using Events = EventsArray<Event>; + +// === Facilities for event class definitions === + +//! \brief A template base class to derive your event type from +//! +//! This simple class template generates commonly used event constructor +//! signatures and the content() method with the appropriate return type. +//! The generic version here is only used with non-trivial \p ContentT (if you +//! don't need to create an event from its content structure, just go and derive +//! straight from the respective \p EventBaseT instead of using EventTemplate); +//! specialisations may override that and provide useful semantics even without +//! \p ContentT (see EventTemplate<CallEvent>, e.g.). +//! +//! The template uses CRTP to pick the event type id from the actual class; +//! it will fail to compile if \p EventT doesn't provide TypeId. It also uses +//! the base event type's basicJson(); if you need extra keys to be inserted +//! you may want to bypass this template as writing the code to that effect in +//! your class will likely be clearer and more concise. +//! \sa https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern +//! \sa DEFINE_SIMPLE_EVENT +template <typename EventT, EventClass BaseEventT, typename ContentT = void> +class EventTemplate : public BaseEventT { + // Above: can't constrain EventT to be EventClass because it's incomplete + // by CRTP definition. +public: + static_assert( + !std::is_same_v<ContentT, void>, + "If you see this, you tried to use EventTemplate with the default" + " ContentT type, which is void. This default is only used with explicit" + " specialisations (see CallEvent, e.g.). Otherwise, if you don't intend" + " to use the content part of EventTemplate then you don't need" + " EventTemplate; just use the base event class directly"); + using content_type = ContentT; + + explicit EventTemplate(const QJsonObject& json) + : BaseEventT(json) + {} + explicit EventTemplate(const ContentT& c) + : BaseEventT(EventT::basicJson(EventT::TypeId, toJson(c))) + {} + + ContentT content() const { return fromJson<ContentT>(this->contentJson()); } +}; + +//! \brief Supply event metatype information in base event types +//! +//! Use this macro in a public section of your base event class to provide +//! type identity and enable dynamic loading of generic events of that type. +//! Do _not_ add this macro if your class is an intermediate wrapper and is not +//! supposed to be instantiated on its own. Provides BaseMetaType static field +//! initialised by parameters passed to the macro, and a metaType() override +//! pointing to that BaseMetaType. +//! \sa EventMetaType, EventMetaType::SuppressLoadDerived +#define QUO_BASE_EVENT(CppType_, ...) \ + friend class EventMetaType<CppType_>; \ + static inline EventMetaType<CppType_> BaseMetaType{ \ + #CppType_ __VA_OPT__(,) __VA_ARGS__ }; \ + const AbstractEventMetaType& metaType() const override \ + { \ + return BaseMetaType; \ + } \ // End of macro -#else -#define DEFINE_EVENTTYPE_ALIAS(_Id, _Type) // Nothing -#endif - - // === is<>(), eventCast<>() and visit<>() === - template <typename EventT> - inline bool is(const Event& e) { return e.type() == typeId<EventT>(); } +//! Supply event metatype information in (specific) event types +//! +//! Use this macro in a public section of your event class to provide type +//! identity and enable dynamic loading of generic events of that type. +//! Do _not_ use this macro if your class is an intermediate wrapper and is not +//! supposed to be instantiated on its own. Provides MetaType static field +//! initialised as described below; a metaType() override pointing to it; and +//! the TypeId static field that is equal to MetaType.matrixId. +//! +//! The first two macro parameters are used as the first two EventMetaType +//! constructor parameters; the third EventMetaType parameter is always +//! BaseMetaType; and additional base types can be passed in extra macro +//! parameters if you need to include the same event type in more than one +//! event factory hierarchy (e.g., EncryptedEvent). +//! \sa EventMetaType +#define QUO_EVENT(CppType_, MatrixType_, ...) \ + static inline const auto& TypeId = MatrixType_##_ls; \ + friend class EventMetaType<CppType_>; \ + static inline const EventMetaType<CppType_> MetaType{ \ + #CppType_, TypeId, BaseMetaType __VA_OPT__(,) __VA_ARGS__ \ + }; \ + const AbstractEventMetaType& metaType() const override \ + { \ + return MetaType; \ + } \ + [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \ + static constexpr const char* matrixTypeId() { return MatrixType_; } \ + [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \ + static event_type_t typeId() { return TypeId; } \ + // End of macro - inline bool isUnknown(const Event& e) { return e.type() == unknownEventTypeId(); } +//! \deprecated This is the old name for what is now known as QUO_EVENT +#define DEFINE_EVENT_TYPEID(Type_, Id_) QUO_EVENT(Type_, Id_) - template <typename EventT, typename BasePtrT> - inline auto eventCast(const BasePtrT& eptr) - -> decltype(static_cast<EventT*>(&*eptr)) - { - Q_ASSERT(eptr); - return is<EventT>(*eptr) ? static_cast<EventT*>(&*eptr) : nullptr; +#define QUO_CONTENT_GETTER_X(PartType_, PartName_, JsonKey_) \ + PartType_ PartName_() const \ + { \ + static const auto PartName_##JsonKey = JsonKey_; \ + return contentPart<PartType_>(PartName_##JsonKey); \ } - // A single generic catch-all visitor - template <typename BaseEventT, typename FnT> - inline auto visit(const BaseEventT& event, FnT&& visitor) - -> decltype(visitor(event)) - { - return visitor(event); - } +//! \brief Define an inline method obtaining a content part +//! +//! This macro adds a const method that extracts a JSON value at the key +//! <tt>toSnakeCase(PartName_)</tt> (sic) and converts it to the type +//! \p PartType_. Effectively, the generated method is an equivalent of +//! \code +//! contentPart<PartType_>(Quotient::toSnakeCase(#PartName_##_ls)); +//! \endcode +#define QUO_CONTENT_GETTER(PartType_, PartName_) \ + QUO_CONTENT_GETTER_X(PartType_, PartName_, toSnakeCase(#PartName_##_ls)) + +//! \deprecated This macro was used after an event class definition +//! to enable its dynamic loading; it is completely superseded by QUO_EVENT +#define REGISTER_EVENT_TYPE(Type_) + +/// \brief Define a new event class with a single key-value pair in the content +/// +/// This macro defines a new event class \p Name_ derived from \p Base_, +/// with Matrix event type \p TypeId_, providing a getter named \p GetterName_ +/// for a single value of type \p ValueType_ inside the event content. +/// To retrieve the value the getter uses a JSON key name that corresponds to +/// its own (getter's) name but written in snake_case. \p GetterName_ must be +/// in camelCase, no quotes (an identifier, not a literal). +#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \ + JsonKey_) \ + constexpr auto Name_##ContentKey = JsonKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public EventTemplate< \ + Name_, Base_, \ + EventContent::SingleKeyValue<ValueType_, Name_##ContentKey>> { \ + public: \ + QUO_EVENT(Name_, TypeId_) \ + using value_type = ValueType_; \ + using EventTemplate::EventTemplate; \ + QUO_CONTENT_GETTER_X(ValueType_, GetterName_, Name_##ContentKey) \ + }; \ + // End of macro - template <typename T> - constexpr auto is_event() - { - return std::is_base_of<Event, std::decay_t<T>>::value; - } +// === is<>(), eventCast<>() and switchOnType<>() === - template <typename T, typename FnT> - constexpr auto needs_cast() - { - return !std::is_convertible<T, fn_arg_t<FnT>>::value; +template <EventClass EventT> +inline bool is(const Event& e) +{ + if constexpr (requires { EventT::MetaType; }) { + return &e.metaType() == &EventT::MetaType; + } else { + const auto* p = &e.metaType(); + do { + if (p == &EventT::BaseMetaType) + return true; + } while ((p = p->baseType) != nullptr); + return false; } - - // A single type-specific void visitor - template <typename BaseEventT, typename FnT> - inline - std::enable_if_t< - is_event<BaseEventT>() && needs_cast<BaseEventT, FnT>() && - std::is_void<fn_return_t<FnT>>::value> - visit(const BaseEventT& event, FnT&& visitor) +} + +//! \brief Cast the event pointer down in a type-safe way +//! +//! Checks that the event \p eptr points to actually is of the requested type +//! and returns a (plain) pointer to the event downcast to that type. \p eptr +//! can be either "dumb" (BaseEventT*) or "smart" (`event_ptr_tt<>`). This +//! overload doesn't affect the event ownership - if the original pointer owns +//! the event it must outlive the downcast pointer to keep it from dangling. +template <EventClass EventT, typename BasePtrT> +inline auto eventCast(const BasePtrT& eptr) + -> decltype(static_cast<EventT*>(&*eptr)) +{ + return eptr && is<std::decay_t<EventT>>(*eptr) + ? static_cast<EventT*>(&*eptr) + : nullptr; +} + +//! \brief Cast the event pointer down in a type-safe way, with moving +//! +//! Checks that the event \p eptr points to actually is of the requested type; +//! if (and only if) it is, releases the pointer, downcasts it to the requested +//! event type and returns a new smart pointer wrapping the downcast one. +//! Unlike the non-moving eventCast() overload, this one only accepts a smart +//! pointer, and that smart pointer should be an rvalue (either a temporary, +//! or as a result of std::move()). The ownership, respectively, is transferred +//! to the new pointer; the original smart pointer is reset to nullptr, as is +//! normal for `unique_ptr<>::release()`. +//! \note If \p eptr's event type does not match \p EventT it retains ownership +//! after calling this overload; if it is a temporary, this normally +//! leads to the event getting deleted along with the end of +//! the temporary's lifetime. +template <EventClass EventT, typename BaseEventT> +inline auto eventCast(event_ptr_tt<BaseEventT>&& eptr) +{ + return eptr && is<std::decay_t<EventT>>(*eptr) + ? event_ptr_tt<EventT>(static_cast<EventT*>(eptr.release())) + : nullptr; +} + +namespace _impl { + template <typename FnT, typename BaseT> + concept Invocable_With_Downcast = requires { - using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) - visitor(static_cast<event_type>(event)); - } + requires EventClass<BaseT>; + std::is_base_of_v<BaseT, std::remove_cvref_t<fn_arg_t<FnT>>>; + }; +} - // A single type-specific non-void visitor with an optional default value - template <typename BaseEventT, typename FnT> - inline - std::enable_if_t< - is_event<BaseEventT>() && needs_cast<BaseEventT, FnT>(), - fn_return_t<FnT>> // non-voidness is guarded by defaultValue type - visit(const BaseEventT& event, FnT&& visitor, - fn_return_t<FnT>&& defaultValue = {}) - { - using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) - return visitor(static_cast<event_type>(event)); - return std::forward<fn_return_t<FnT>>(defaultValue); +template <EventClass BaseT, typename TailT> +inline auto switchOnType(const BaseT& event, TailT&& tail) +{ + if constexpr (std::is_invocable_v<TailT, BaseT>) { + return tail(event); + } else if constexpr (_impl::Invocable_With_Downcast<TailT, BaseT>) { + using event_type = fn_arg_t<TailT>; + if (is<std::decay_t<event_type>>(event)) + return tail(static_cast<event_type>(event)); + return std::invoke_result_t<TailT, event_type>(); // Default-constructed + } else { // Treat it as a value to return + return std::forward<TailT>(tail); } +} - // A chain of 2 or more visitors - template <typename BaseEventT, typename FnT1, typename FnT2, typename... FnTs> - inline - std::enable_if_t<is_event<BaseEventT>(), fn_return_t<FnT1>> - visit(const BaseEventT& event, FnT1&& visitor1, FnT2&& visitor2, - FnTs&&... visitors) - { - using event_type1 = fn_arg_t<FnT1>; - if (is<event_type1>(event)) - return visitor1(static_cast<event_type1&>(event)); - return visit(event, std::forward<FnT2>(visitor2), - std::forward<FnTs>(visitors)...); - } -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::Event*) -Q_DECLARE_METATYPE(const QMatrixClient::Event*) +template <EventClass BaseT, typename FnT1, typename... FnTs> +inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns) +{ + using event_type1 = fn_arg_t<FnT1>; + if (is<std::decay_t<event_type1>>(event)) + return fn1(static_cast<event_type1>(event)); + return switchOnType(event, std::forward<FnTs>(fns)...); +} + +template <EventClass BaseT, typename... FnTs> +[[deprecated("The new name for visit() is switchOnType()")]] // +inline auto visit(const BaseT& event, FnTs&&... fns) +{ + return switchOnType(event, std::forward<FnTs>(fns)...); +} + + // A facility overload that calls void-returning 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&&... fns) + requires std::is_void_v< + decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))> +{ + for (auto&& evtPtr: events) + switchOnType(*evtPtr, std::forward<FnTs>(fns)...); +} +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::Event*) +Q_DECLARE_METATYPE(const Quotient::Event*) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index a6b1c763..8db3b7e3 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -1,86 +1,122 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "eventcontent.h" -#include "util.h" + +#include "converters.h" +#include "logging.h" #include <QtCore/QMimeDatabase> +#include <QtCore/QFileInfo> -using namespace QMatrixClient::EventContent; +using namespace Quotient::EventContent; +using std::move; QJsonObject Base::toJson() const { QJsonObject o; - fillJson(&o); + fillJson(o); return o; } -FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, - const QString& originalFilename) - : mimeType(mimeType), url(u), payloadSize(payloadSize) - , originalName(originalFilename) -{ } - -FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename) - : originalInfoJson(infoJson) - , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) - , url(u) - , payloadSize(infoJson["size"_ls].toInt()) - , originalName(originalFilename) +FileInfo::FileInfo(const QFileInfo& fi) + : source(QUrl::fromLocalFile(fi.filePath())), + mimeType(QMimeDatabase().mimeTypeForFile(fi)), + payloadSize(fi.size()), + originalName(fi.fileName()) +{ + Q_ASSERT(fi.isFile()); +} + +FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize, + const QMimeType& mimeType, QString originalFilename) + : source(move(sourceInfo)) + , mimeType(mimeType) + , payloadSize(payloadSize) + , originalName(move(originalFilename)) +{ + 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(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + QString originalFilename) + : source(move(sourceInfo)) + , originalInfoJson(infoJson) + , mimeType( + QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) + , payloadSize(fromJson<qint64>(infoJson["size"_ls])) + , originalName(move(originalFilename)) { if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } -void FileInfo::fillInfoJson(QJsonObject* infoJson) const +bool FileInfo::isValid() const { - Q_ASSERT(infoJson); - infoJson->insert(QStringLiteral("size"), payloadSize); - infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + const auto& u = url(); + return u.scheme() == "mxc" && (u.authority() + u.path()).count('/') == 1; } -ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType, - const QSize& imageSize) - : FileInfo(u, fileSize, mimeType), imageSize(imageSize) -{ } +QUrl FileInfo::url() const +{ + return getUrlFromSourceInfo(source); +} -ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, +QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info) +{ + QJsonObject infoJson; + if (info.payloadSize != -1) + infoJson.insert(QStringLiteral("size"), info.payloadSize); + if (info.mimeType.isValid()) + infoJson.insert(QStringLiteral("mimetype"), info.mimeType.name()); + return infoJson; +} + +ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) + : FileInfo(fi), imageSize(imageSize) +{} + +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize, + const QMimeType& type, QSize imageSize, const QString& originalFilename) - : FileInfo(u, infoJson, originalFilename) + : FileInfo(move(sourceInfo), fileSize, type, originalFilename) + , imageSize(imageSize) +{} + +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + const QString& originalFilename) + : FileInfo(move(sourceInfo), infoJson, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) -{ } +{} -void ImageInfo::fillInfoJson(QJsonObject* infoJson) const +QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info) { - FileInfo::fillInfoJson(infoJson); - infoJson->insert(QStringLiteral("w"), imageSize.width()); - infoJson->insert(QStringLiteral("h"), imageSize.height()); + auto infoJson = toInfoJson(static_cast<const FileInfo&>(info)); + if (info.imageSize.width() != -1) + infoJson.insert(QStringLiteral("w"), info.imageSize.width()); + if (info.imageSize.height() != -1) + infoJson.insert(QStringLiteral("h"), info.imageSize.height()); + return infoJson; } -Thumbnail::Thumbnail(const QJsonObject& infoJson) - : ImageInfo(infoJson["thumbnail_url"_ls].toString(), +Thumbnail::Thumbnail(const QJsonObject& infoJson, + const Omittable<EncryptedFileMetadata>& efm) + : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), infoJson["thumbnail_info"_ls].toObject()) -{ } +{ + if (efm) + source = *efm; +} -void Thumbnail::fillInfoJson(QJsonObject* infoJson) const +void Thumbnail::dumpTo(QJsonObject& infoJson) const { - infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); - infoJson->insert(QStringLiteral("thumbnail_info"), - toInfoJson<ImageInfo>(*this)); + if (url().isValid()) + fillJson(infoJson, { "thumbnail_url"_ls, "thumbnail_file"_ls }, source); + if (!imageSize.isEmpty()) + infoJson.insert(QStringLiteral("thumbnail_info"), + toInfoJson(*this)); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 91d7a8c8..af26c0a4 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -1,280 +1,254 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). +#include "filesourceinfo.h" +#include "quotient_export.h" + #include <QtCore/QJsonObject> +#include <QtCore/QMetaType> #include <QtCore/QMimeType> -#include <QtCore/QUrl> #include <QtCore/QSize> +#include <QtCore/QUrl> -namespace QMatrixClient -{ - namespace EventContent +class QFileInfo; + +namespace Quotient::EventContent { +//! \brief Base for all content types that can be stored in RoomMessageEvent +//! +//! Each content type class should have a constructor taking +//! a QJsonObject and override fillJson() with an implementation +//! that will fill the target QJsonObject with stored values. It is +//! assumed but not required that a content object can also be created +//! from plain data. +class QUOTIENT_API Base { +public: + explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} + virtual ~Base() = default; + + QJsonObject toJson() const; + +public: + QJsonObject originalJson; + + // You can't assign those classes + Base& operator=(const Base&) = delete; + Base& operator=(Base&&) = delete; + +protected: + Base(const Base&) = default; + Base(Base&&) noexcept = default; + + virtual void fillJson(QJsonObject&) const = 0; +}; + +// The below structures fairly follow CS spec 11.2.1.6. The overall +// set of attributes for each content types is a superset of the spec +// but specific aggregation structure is altered. See doc comments to +// each type for the list of available attributes. + +// A quick classes inheritance structure follows (the definitions are +// spread across eventcontent.h and roommessageevent.h): +// UrlBasedContent<InfoT> : InfoT + thumbnail data +// PlayableContent<InfoT> : + duration attribute +// FileInfo +// FileContent = UrlBasedContent<FileInfo> +// AudioContent = PlayableContent<FileInfo> +// ImageInfo : FileInfo + imageSize attribute +// ImageContent = UrlBasedContent<ImageInfo> +// VideoContent = PlayableContent<ImageInfo> + +//! \brief Mix-in class representing `info` subobject in content JSON +//! +//! This is one of base classes for content types that deal with files or +//! URLs. It stores the file metadata attributes, such as size, MIME type +//! etc. found in the `content/info` subobject of event JSON payloads. +//! Actual content classes derive from this class _and_ TypedBase that +//! provides a polymorphic interface to access data in the mix-in. FileInfo +//! (as well as ImageInfo, that adds image size to the metadata) is NOT +//! polymorphic and is used in a non-polymorphic way to store thumbnail +//! metadata (in a separate instance), next to the metadata on the file +//! itself. +//! +//! If you need to make a new _content_ (not info) class based on files/URLs +//! take UrlBasedContent as the example, i.e.: +//! 1. Double-inherit from this class (or ImageInfo) and TypedBase. +//! 2. Provide a constructor from QJsonObject that will pass the `info` +//! subobject (not the whole content JSON) down to FileInfo/ImageInfo. +//! 3. Override fillJson() to customise the JSON export logic. Make sure +//! to call toInfoJson() from it to produce the payload for the `info` +//! subobject in the JSON payload. +//! +//! \sa ImageInfo, FileContent, ImageContent, AudioContent, VideoContent, +//! UrlBasedContent +class QUOTIENT_API FileInfo { +public: + FileInfo() = default; + //! \brief Construct from a QFileInfo object + //! + //! \param fi a QFileInfo object referring to an existing file + explicit FileInfo(const QFileInfo& fi); + explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1, + const QMimeType& mimeType = {}, + QString originalFilename = {}); + //! \brief Construct from a JSON `info` payload + //! + //! Make sure to pass the `info` subobject of content JSON, not the + //! whole JSON content. + FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + QString originalFilename = {}); + + bool isValid() const; + QUrl url() const; + + //! \brief Extract media id from the URL + //! + //! This can be used, e.g., to construct a QML-facing image:// + //! URI as follows: + //! \code "image://provider/" + info.mediaId() \endcode + QString mediaId() const { return url().authority() + url().path(); } + +public: + FileSourceInfo source; + QJsonObject originalInfoJson; + QMimeType mimeType; + qint64 payloadSize = 0; + QString originalName; +}; + +QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); + +//! \brief A content info class for image/video content types and thumbnails +class QUOTIENT_API ImageInfo : public FileInfo { +public: + ImageInfo() = default; + explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); + explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1, + const QMimeType& type = {}, QSize imageSize = {}, + const QString& originalFilename = {}); + ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + const QString& originalFilename = {}); + +public: + QSize imageSize; +}; + +QUOTIENT_API QJsonObject toInfoJson(const ImageInfo& info); + +//! \brief An auxiliary class for an info type that carries a thumbnail +//! +//! This class saves/loads a thumbnail to/from `info` subobject of +//! the JSON representation of event content; namely, `info/thumbnail_url` +//! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and +//! `info/thumbnail_info` fields are used. +class QUOTIENT_API Thumbnail : public ImageInfo { +public: + using ImageInfo::ImageInfo; + explicit Thumbnail(const QJsonObject& infoJson, + const Omittable<EncryptedFileMetadata>& efm = none); + + //! \brief Add thumbnail information to the passed `info` JSON object + void dumpTo(QJsonObject& infoJson) const; +}; + +class QUOTIENT_API TypedBase : public Base { +public: + 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; +}; + +//! \brief A template class for content types with a URL and additional info +//! +//! Types that derive from this class template take `url` (or, if the file +//! is encrypted, `file`) and, optionally, `filename` values from +//! the top-level JSON object and the rest of information from the `info` +//! subobject, as defined by the parameter type. +//! \tparam InfoT base info class - FileInfo or ImageInfo +template <class InfoT> +class UrlBasedContent : public TypedBase, public InfoT { +public: + using InfoT::InfoT; + explicit UrlBasedContent(const QJsonObject& json) + : TypedBase(json) + , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), + json["filename"].toString()) + , thumbnail(FileInfo::originalInfoJson) + { + if (const auto efmJson = json.value("file"_ls).toObject(); + !efmJson.isEmpty()) + InfoT::source = fromJson<EncryptedFileMetadata>(efmJson); + // Two small hacks on originalJson to expose mediaIds to QML + originalJson.insert("mediaId", InfoT::mediaId()); + originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); + } + + QMimeType type() const override { return InfoT::mimeType; } + const FileInfo* fileInfo() const override { return this; } + FileInfo* fileInfo() override { return this; } + const Thumbnail* thumbnailInfo() const override { return &thumbnail; } + +public: + Thumbnail thumbnail; + +protected: + virtual void fillInfoJson(QJsonObject& infoJson [[maybe_unused]]) const + {} + + void fillJson(QJsonObject& json) const override { - /** - * A base class for all content types that can be stored - * in a RoomMessageEvent - * - * Each content type class should have a constructor taking - * a QJsonObject and override fillJson() with an implementation - * that will fill the target QJsonObject with stored values. It is - * assumed but not required that a content object can also be created - * from plain data. - */ - class Base - { - public: - explicit Base (const QJsonObject& o = {}) : originalJson(o) { } - virtual ~Base() = default; - - QJsonObject toJson() const; - - public: - QJsonObject originalJson; - - protected: - virtual void fillJson(QJsonObject* o) const = 0; - }; - - // The below structures fairly follow CS spec 11.2.1.6. The overall - // set of attributes for each content types is a superset of the spec - // but specific aggregation structure is altered. See doc comments to - // each type for the list of available attributes. - - // A quick classes inheritance structure follows: - // FileInfo - // FileContent : UrlBasedContent<FileInfo, Thumbnail> - // AudioContent : UrlBasedContent<FileInfo, Duration> - // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlBasedContent<ImageInfo, Thumbnail> - // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration> - - /** - * A base/mixin class for structures representing an "info" object for - * some content types. These include most attachment types currently in - * the CS API spec. - * - * In order to use it in a content class, derive both from TypedBase - * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) - * and call fillInfoJson() to fill the "info" subobject. Make sure - * to pass an "info" part of JSON to FileInfo constructor, not the whole - * JSON content, as well as contents of "url" (or a similar key) and - * optionally "filename" node from the main JSON content. Assuming you - * don't do unusual things, you should use \p UrlBasedContent<> instead - * of doing multiple inheritance and overriding Base::fillJson() by hand. - * - * This class is not polymorphic. - */ - class FileInfo - { - public: - explicit FileInfo(const QUrl& u, int payloadSize = -1, - const QMimeType& mimeType = {}, - const QString& originalFilename = {}); - FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - void fillInfoJson(QJsonObject* infoJson) const; - - /** - * \brief Extract media id from the URL - * - * This can be used, e.g., to construct a QML-facing image:// - * URI as follows: - * \code "image://provider/" + info.mediaId() \endcode - */ - QString mediaId() const { return url.authority() + url.path(); } - - public: - QJsonObject originalInfoJson; - QMimeType mimeType; - QUrl url; - int payloadSize; - QString originalName; - }; - - template <typename InfoT> - QJsonObject toInfoJson(const InfoT& info) - { - QJsonObject infoJson; - info.fillInfoJson(&infoJson); - return infoJson; - } - - /** - * A content info class for image content types: image, thumbnail, video - */ - class ImageInfo : public FileInfo - { - public: - explicit ImageInfo(const QUrl& u, int fileSize = -1, - QMimeType mimeType = {}, - const QSize& imageSize = {}); - ImageInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - void fillInfoJson(QJsonObject* infoJson) const; - - public: - QSize imageSize; - }; - - /** - * An auxiliary class for an info type that carries a thumbnail - * - * This class saves/loads a thumbnail to/from "info" subobject of - * the JSON representation of event content; namely, - * "info/thumbnail_url" and "info/thumbnail_info" fields are used. - */ - class Thumbnail : public ImageInfo - { - public: - Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) - : ImageInfo(info) - { } - - /** - * Writes thumbnail information to "thumbnail_info" subobject - * and thumbnail URL to "thumbnail_url" node inside "info". - */ - void fillInfoJson(QJsonObject* infoJson) const; - }; - - class TypedBase: public Base - { - public: - explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } - virtual QMimeType type() const = 0; - virtual const FileInfo* fileInfo() const { return nullptr; } - virtual const Thumbnail* thumbnailInfo() const { return nullptr; } - }; - - /** - * A base class for content types that have a URL and additional info - * - * Types that derive from this class template take "url" and, - * optionally, "filename" values from the top-level JSON object and - * the rest of information from the "info" subobject, as defined by - * the parameter type. - * - * \tparam InfoT base info class - */ - template <class InfoT> - class UrlBasedContent : public TypedBase, public InfoT - { - public: - UrlBasedContent(QUrl url, InfoT&& info, QString filename = {}) - : InfoT(url, std::forward<InfoT>(info), filename) - { } - explicit UrlBasedContent(const QJsonObject& json) - : TypedBase(json) - , InfoT(json["url"].toString(), json["info"].toObject(), - json["filename"].toString()) - { - // A small hack to facilitate links creation in QML. - originalJson.insert("mediaId", InfoT::mediaId()); - } - - QMimeType type() const override { return InfoT::mimeType; } - const FileInfo* fileInfo() const override { return this; } - - protected: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert("url", InfoT::url.toString()); - if (!InfoT::originalName.isEmpty()) - json->insert("filename", InfoT::originalName); - json->insert("info", toInfoJson<InfoT>(*this)); - } - }; - - template <typename InfoT> - class UrlWithThumbnailContent : public UrlBasedContent<InfoT> - { - public: - // TODO: POD constructor - explicit UrlWithThumbnailContent(const QJsonObject& json) - : UrlBasedContent<InfoT>(json) - , thumbnail(InfoT::originalInfoJson) - { - // Another small hack, to simplify making a thumbnail link - UrlBasedContent<InfoT>::originalJson.insert( - "thumbnailMediaId", thumbnail.mediaId()); - } - - const Thumbnail* thumbnailInfo() const override - { return &thumbnail; } - - public: - Thumbnail thumbnail; - - protected: - void fillJson(QJsonObject* json) const override - { - UrlBasedContent<InfoT>::fillJson(json); - auto infoJson = json->take("info").toObject(); - thumbnail.fillInfoJson(&infoJson); - json->insert("info", infoJson); - } - }; - - /** - * Content class for m.image - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for the main image: - * - payloadSize - * - mimeType - * - imageSize - */ - using ImageContent = UrlWithThumbnailContent<ImageInfo>; - - /** - * Content class for m.file - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize (QSize for "h" and "w" in JSON) - */ - using FileContent = UrlWithThumbnailContent<FileInfo>; - } // namespace EventContent -} // namespace QMatrixClient + Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source); + if (!InfoT::originalName.isEmpty()) + json.insert("filename", InfoT::originalName); + auto infoJson = toInfoJson(*this); + if (thumbnail.isValid()) + thumbnail.dumpTo(infoJson); + fillInfoJson(infoJson); + json.insert("info", infoJson); + } +}; + +//! \brief Content class for m.image +//! +//! Available fields: +//! - corresponding to the top-level JSON: +//! - source (corresponding to `url` or `file` in JSON) +//! - filename (extension to the spec) +//! - corresponding to the `info` subobject: +//! - payloadSize (`size` in JSON) +//! - mimeType (`mimetype` in JSON) +//! - imageSize (QSize for a combination of `h` and `w` in JSON) +//! - thumbnail.url (`thumbnail_url` in JSON) +//! - corresponding to the `info/thumbnail_info` subobject: contents of +//! thumbnail field, in the same vein as for the main image: +//! - payloadSize +//! - mimeType +//! - imageSize +using ImageContent = UrlBasedContent<ImageInfo>; + +//! \brief Content class for m.file +//! +//! Available fields: +//! - corresponding to the top-level JSON: +//! - source (corresponding to `url` or `file` in JSON) +//! - filename +//! - corresponding to the `info` subobject: +//! - payloadSize (`size` in JSON) +//! - mimeType (`mimetype` in JSON) +//! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON) +//! - corresponding to the `info/thumbnail_info` subobject: +//! - thumbnail.payloadSize +//! - thumbnail.mimeType +//! - thumbnail.imageSize (QSize for `h` and `w` in JSON) +using FileContent = UrlBasedContent<FileInfo>; +} // namespace Quotient::EventContent +Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*) diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 3ee9a181..b4ac154c 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -1,68 +1,13 @@ -/****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "stateevent.h" -#include "converters.h" -namespace QMatrixClient { - 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); - } - } - - /** Create an event with proper type from a JSON object - * Use this factory template to detect the type from the JSON object - * contents (the detected event type should derive from the template - * parameter type) and create an event object of that type. - */ - template <typename BaseEventT> - inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) - { - return _impl::loadEvent<BaseEventT>(fullJson, - fullJson[TypeKeyL].toString()); - } - - /** Create an event from a type string and content JSON - * Use this factory template to resolve the C++ type from the Matrix - * type string in \p matrixType and create an event of that type that has - * its content part set to \p content. - */ - template <typename BaseEventT> - inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, - const QJsonObject& content) - { - return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content), - matrixType); - } - - template <typename EventT> struct FromJsonObject<event_ptr_tt<EventT>> - { - auto operator()(const QJsonObject& jo) const - { - return loadEvent<EventT>(jo); - } - }; -} // namespace QMatrixClient +namespace Quotient { +struct [[deprecated( + "This header is obsolete since libQuotient 0.7; include a header with" + " the respective event type definition instead")]] EventLoaderH; +StateEventPtr eventLoaderH(EventLoaderH&); +} diff --git a/lib/events/eventrelation.cpp b/lib/events/eventrelation.cpp new file mode 100644 index 00000000..04972f45 --- /dev/null +++ b/lib/events/eventrelation.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "eventrelation.h" + +#include "../logging.h" +#include "event.h" + +using namespace Quotient; + +void JsonObjectConverter<EventRelation>::dumpTo(QJsonObject& jo, + const EventRelation& pod) +{ + if (pod.type.isEmpty()) { + qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; + return; + } + jo.insert(RelTypeKey, pod.type); + jo.insert(EventIdKey, pod.eventId); + if (pod.type == EventRelation::AnnotationType) + jo.insert(QStringLiteral("key"), pod.key); +} + +void JsonObjectConverter<EventRelation>::fillFrom(const QJsonObject& jo, + EventRelation& pod) +{ + if (const auto replyJson = jo.value(EventRelation::ReplyType).toObject(); + !replyJson.isEmpty()) { + pod.type = EventRelation::ReplyType; + fromJson(replyJson[EventIdKeyL], pod.eventId); + } else { + // The experimental logic for generic relationships (MSC1849) + fromJson(jo[RelTypeKey], pod.type); + fromJson(jo[EventIdKeyL], pod.eventId); + if (pod.type == EventRelation::AnnotationType) + fromJson(jo["key"_ls], pod.key); + } +} diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h new file mode 100644 index 00000000..2a841cf1 --- /dev/null +++ b/lib/events/eventrelation.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +namespace Quotient { + +[[maybe_unused]] constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto RelTypeKey = "rel_type"_ls; + +struct QUOTIENT_API EventRelation { + using reltypeid_t = QLatin1String; + + QString type; + QString eventId; + QString key = {}; // Only used for m.annotation for now + + static constexpr auto ReplyType = "m.in_reply_to"_ls; + static constexpr auto AnnotationType = "m.annotation"_ls; + static constexpr auto ReplacementType = "m.replace"_ls; + + static EventRelation replyTo(QString eventId) + { + return { ReplyType, std::move(eventId) }; + } + static EventRelation annotate(QString eventId, QString key) + { + return { AnnotationType, std::move(eventId), std::move(key) }; + } + static EventRelation replace(QString eventId) + { + return { ReplacementType, std::move(eventId) }; + } + + [[deprecated("Use ReplyType variable instead")]] + static constexpr auto Reply() { return ReplyType; } + [[deprecated("Use AnnotationType variable instead")]] // + static constexpr auto Annotation() { return AnnotationType; } + [[deprecated("Use ReplacementType variable instead")]] // + static constexpr auto Replacement() { return ReplacementType; } +}; + +template <> +struct QUOTIENT_API JsonObjectConverter<EventRelation> { + static void dumpTo(QJsonObject& jo, const EventRelation& pod); + static void fillFrom(const QJsonObject& jo, EventRelation& pod); +}; + +} + diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp new file mode 100644 index 00000000..a60d86d2 --- /dev/null +++ b/lib/events/filesourceinfo.cpp @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "filesourceinfo.h" + +#include "logging.h" +#include "util.h" + +#ifdef Quotient_E2EE_ENABLED +# include "e2ee/qolmutils.h" + +# include <QtCore/QCryptographicHash> + +# include <openssl/evp.h> +#endif + +using namespace Quotient; + +QByteArray Quotient::decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata) +{ +#ifdef Quotient_E2EE_ENABLED + if (QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()) + != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return {}; + } + + auto _key = metadata.key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); + EVP_DecryptInit_ex( + ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast<const unsigned char*>(keyBytes.data()), + reinterpret_cast<const unsigned char*>( + QByteArray::fromBase64(metadata.iv.toLatin1()).data())); + EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(plaintext.data()), + &length, + reinterpret_cast<const unsigned char*>(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast<unsigned char*>(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext.left(ciphertext.size()); +#else + qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " + "cannot decrypt the file"; + return ciphertext; +#endif +} + +std::pair<EncryptedFileMetadata, QByteArray> Quotient::encryptFile( + const QByteArray& plainText) +{ +#ifdef Quotient_E2EE_ENABLED + auto k = RandomBuffer(32); + auto kBase64 = k.toBase64(QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals); + auto iv = RandomBuffer(16); + JWK key = { + "oct"_ls, { "encrypt"_ls, "decrypt"_ls }, "A256CTR"_ls, kBase64, true + }; + + int length = -1; + auto* ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, k.bytes(), iv.bytes()); + const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); + QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); + EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()), + &length, + reinterpret_cast<const unsigned char*>(plainText.data()), + plainText.size()); + EVP_EncryptFinal_ex(ctx, + reinterpret_cast<unsigned char*>(cipherText.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + + auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256) + .toBase64(QByteArray::OmitTrailingEquals); + auto ivBase64 = iv.toBase64(QByteArray::OmitTrailingEquals); + EncryptedFileMetadata efm = { + {}, key, ivBase64, { { QStringLiteral("sha256"), hash } }, "v2"_ls + }; + return { efm, cipherText }; +#else + return {}; +#endif +} + +void JsonObjectConverter<EncryptedFileMetadata>::dumpTo(QJsonObject& jo, + const EncryptedFileMetadata& 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); +} + +void JsonObjectConverter<EncryptedFileMetadata>::fillFrom(const QJsonObject& jo, + EncryptedFileMetadata& 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); +} + +void JsonObjectConverter<JWK>::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); +} + +void JsonObjectConverter<JWK>::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); +} + +QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi) +{ + return std::visit(Overloads { [](const QUrl& url) { return url; }, + [](const EncryptedFileMetadata& efm) { + return efm.url; + } }, + fsi); +} + +void Quotient::setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl) +{ + std::visit(Overloads { [&newUrl](QUrl& url) { url = newUrl; }, + [&newUrl](EncryptedFileMetadata& efm) { + efm.url = newUrl; + } }, + fsi); +} + +void Quotient::fillJson(QJsonObject& jo, + const std::array<QLatin1String, 2>& jsonKeys, + const FileSourceInfo& fsi) +{ + // NB: Keeping variant_size_v out of the function signature for readability. + // NB2: Can't use jsonKeys directly inside static_assert as its value is + // unknown so the compiler cannot ensure size() is constexpr (go figure...) + static_assert( + std::variant_size_v<FileSourceInfo> == decltype(jsonKeys) {}.size()); + jo.insert(jsonKeys[fsi.index()], toJson(fsi)); +} diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h new file mode 100644 index 00000000..8f7e3cbe --- /dev/null +++ b/lib/events/filesourceinfo.h @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +#include <array> + +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 QUOTIENT_API EncryptedFileMetadata { + 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; +}; + +QUOTIENT_API std::pair<EncryptedFileMetadata, QByteArray> encryptFile( + const QByteArray& plainText); +QUOTIENT_API QByteArray decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata); + +template <> +struct QUOTIENT_API JsonObjectConverter<EncryptedFileMetadata> { + static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod); + static void fillFrom(const QJsonObject& jo, EncryptedFileMetadata& pod); +}; + +template <> +struct QUOTIENT_API JsonObjectConverter<JWK> { + static void dumpTo(QJsonObject& jo, const JWK& pod); + static void fillFrom(const QJsonObject& jo, JWK& pod); +}; + +using FileSourceInfo = std::variant<QUrl, EncryptedFileMetadata>; + +QUOTIENT_API QUrl getUrlFromSourceInfo(const FileSourceInfo& fsi); + +QUOTIENT_API void setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl); + +// The way FileSourceInfo is stored in JSON requires an extra parameter so +// the original template is not applicable +template <> +void fillJson(QJsonObject&, const FileSourceInfo&) = delete; + +//! \brief Export FileSourceInfo to a JSON object +//! +//! Depending on what is stored inside FileSourceInfo, this function will insert +//! - a key-to-string pair where key is taken from jsonKeys[0] and the string +//! is the URL, if FileSourceInfo stores a QUrl; +//! - a key-to-object mapping where key is taken from jsonKeys[1] and the object +//! is the result of converting EncryptedFileMetadata to JSON, +//! if FileSourceInfo stores EncryptedFileMetadata +QUOTIENT_API void fillJson(QJsonObject& jo, + const std::array<QLatin1String, 2>& jsonKeys, + const FileSourceInfo& fsi); + +} // namespace Quotient diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h new file mode 100644 index 00000000..80aebcf3 --- /dev/null +++ b/lib/events/keyverificationevent.h @@ -0,0 +1,258 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "event.h" + +namespace Quotient { + +static constexpr auto SasV1Method = "m.sas.v1"_ls; + +class QUOTIENT_API KeyVerificationEvent : public Event { +public: + QUO_BASE_EVENT(KeyVerificationEvent, "m.key.*"_ls, Event::BaseMetaType) + + using Event::Event; + + /// An opaque identifier for the verification request. Must + /// be unique with respect to the devices involved. + QUO_CONTENT_GETTER(QString, transactionId) +}; + +/// Requests a key verification with another user's devices. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationRequestEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationRequestEvent, "m.key.verification.request") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationRequestEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods, + const QDateTime& timestamp) + : KeyVerificationRequestEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) }, + { "timestamp"_ls, toJson(timestamp) } })) + {} + + /// The device ID which is initiating the request. + QUO_CONTENT_GETTER(QString, fromDevice) + + /// The verification methods supported by the sender. + QUO_CONTENT_GETTER(QStringList, methods) + + /// The POSIX timestamp in milliseconds for when the request was + /// made. If the request is in the future by more than 5 minutes or + /// more than 10 minutes in the past, the message should be ignored + /// by the receiver. + QUO_CONTENT_GETTER(QDateTime, timestamp) +}; + +class QUOTIENT_API KeyVerificationReadyEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationReadyEvent, "m.key.verification.ready") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationReadyEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods) + : KeyVerificationReadyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) } })) + {} + + /// The device ID which is accepting the request. + QUO_CONTENT_GETTER(QString, fromDevice) + + /// The verification methods supported by the sender. + QUO_CONTENT_GETTER(QStringList, methods) +}; + +/// Begins a key verification process. +class QUOTIENT_API KeyVerificationStartEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationStartEvent, "m.key.verification.start") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationStartEvent(const QString& transactionId, + const QString& fromDevice) + : KeyVerificationStartEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "method"_ls, SasV1Method }, + { "hashes"_ls, QJsonArray{ "sha256"_ls } }, + { "key_agreement_protocols"_ls, + QJsonArray{ "curve25519-hkdf-sha256"_ls } }, + { "message_authentication_codes"_ls, + QJsonArray{ "hkdf-hmac-sha256"_ls } }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls } } })) + {} + + /// The device ID which is initiating the process. + QUO_CONTENT_GETTER(QString, fromDevice) + + /// The verification method to use. + QUO_CONTENT_GETTER(QString, method) + + /// Optional method to use to verify the other user's key with. + QUO_CONTENT_GETTER(Omittable<QString>, nextMethod) + + // SAS.V1 methods + + /// The key agreement protocols the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList keyAgreementProtocols() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QStringList>("key_agreement_protocols"_ls); + } + + /// The hash methods the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList hashes() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QStringList>("hashes"_ls); + } + + /// The message authentication codes that the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList messageAuthenticationCodes() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QStringList>("message_authentication_codes"_ls); + } + + /// The SAS methods the sending device (and the sending device's + /// user) understands. + /// \note Only exist if method is m.sas.v1 + QString shortAuthenticationString() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QString>("short_authentification_string"_ls); + } +}; + +/// Accepts a previously sent m.key.verification.start message. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationAcceptEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationAcceptEvent, "m.key.verification.accept") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationAcceptEvent(const QString& transactionId, + const QString& commitment) + : KeyVerificationAcceptEvent(basicJson( + TypeId, { { "transaction_id"_ls, transactionId }, + { "method"_ls, SasV1Method }, + { "key_agreement_protocol"_ls, "curve25519-hkdf-sha256" }, + { "hash"_ls, "sha256" }, + { "message_authentication_code"_ls, "hkdf-hmac-sha256" }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls, } }, + { "commitment"_ls, commitment } })) + {} + + /// The verification method to use. Must be 'm.sas.v1'. + QUO_CONTENT_GETTER(QString, method) + + /// The key agreement protocol the device is choosing to use, out of + /// the options in the m.key.verification.start message. + QUO_CONTENT_GETTER(QString, keyAgreementProtocol) + + /// The hash method the device is choosing to use, out of the + /// options in the m.key.verification.start message. + QUO_CONTENT_GETTER_X(QString, hashData, "hash"_ls) + + /// The message authentication code the device is choosing to use, out + /// of the options in the m.key.verification.start message. + QUO_CONTENT_GETTER(QString, messageAuthenticationCode) + + /// The SAS methods both devices involved in the verification process understand. + QUO_CONTENT_GETTER(QStringList, shortAuthenticationString) + + /// The hash (encoded as unpadded base64) of the concatenation of the + /// device's ephemeral public key (encoded as unpadded base64) and the + /// canonical JSON representation of the m.key.verification.start message. + QUO_CONTENT_GETTER(QString, commitment) +}; + +class QUOTIENT_API KeyVerificationCancelEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationCancelEvent, "m.key.verification.cancel") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationCancelEvent(const QString& transactionId, + const QString& reason) + : KeyVerificationCancelEvent( + basicJson(TypeId, { + { "transaction_id"_ls, transactionId }, + { "reason"_ls, reason }, + { "code"_ls, reason } // Not a typo + })) + {} + + /// A human readable description of the code. The client should only + /// rely on this string if it does not understand the code. + QUO_CONTENT_GETTER(QString, reason) + + /// The error code for why the process/request was cancelled by the user. + QUO_CONTENT_GETTER(QString, code) +}; + +/// Sends the ephemeral public key for a device to the partner device. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationKeyEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationKeyEvent, "m.key.verification.key") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationKeyEvent(const QString& transactionId, const QString& key) + : KeyVerificationKeyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "key"_ls, key } })) + {} + + /// The device's ephemeral public key, encoded as unpadded base64. + QUO_CONTENT_GETTER(QString, key) +}; + +/// Sends the MAC of a device's key to the partner device. +class QUOTIENT_API KeyVerificationMacEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationMacEvent, "m.key.verification.mac") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationMacEvent(const QString& transactionId, const QString& keys, + const QJsonObject& mac) + : KeyVerificationMacEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "keys"_ls, keys }, + { "mac"_ls, mac } })) + {} + + /// The device's ephemeral public key, encoded as unpadded base64. + QUO_CONTENT_GETTER(QString, keys) + + QHash<QString, QString> mac() const + { + return contentPart<QHash<QString, QString>>("mac"_ls); + } +}; + +class QUOTIENT_API KeyVerificationDoneEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationDoneEvent, "m.key.verification.done") + + using KeyVerificationEvent::KeyVerificationEvent; + explicit KeyVerificationDoneEvent(const QString& transactionId) + : KeyVerificationDoneEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId } })) + {} +}; +} // namespace Quotient diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h new file mode 100644 index 00000000..8d873441 --- /dev/null +++ b/lib/events/reactionevent.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" +#include "eventrelation.h" + +namespace Quotient { + +DEFINE_SIMPLE_EVENT(ReactionEvent, RoomEvent, "m.reaction", EventRelation, + relation, "m.relates_to") + +} // namespace Quotient diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index 47e1398c..d8f9fa0b 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later /* Example of a Receipt Event: @@ -35,35 +20,49 @@ Example of a Receipt Event: #include "receiptevent.h" -#include "converters.h" #include "logging.h" -using namespace QMatrixClient; +using namespace Quotient; -ReceiptEvent::ReceiptEvent(const QJsonObject& obj) - : Event(typeId(), obj) +// 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 Quotient::toJson(const EventsWithReceipts& ewrs) { - const auto& contents = contentJson(); - _eventsWithReceipts.reserve(contents.size()); - for( auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt ) - { - if (eventIt.key().isEmpty()) - { - qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping"; - qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; + 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; +} + +template<> +EventsWithReceipts Quotient::fromJson(const QJsonObject& json) +{ + EventsWithReceipts result; + result.reserve(json.size()); + for (auto eventIt = json.begin(); eventIt != json.end(); ++eventIt) { + if (eventIt.key().isEmpty()) { + qCWarning(EPHEMERAL) + << "ReceiptEvent has an empty event id, skipping"; + qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << json; continue; } - const QJsonObject reads = eventIt.value().toObject() - .value("m.read"_ls).toObject(); - QVector<Receipt> receipts; - receipts.reserve(reads.size()); - for( auto userIt = reads.begin(); userIt != reads.end(); ++userIt ) - { - const QJsonObject user = userIt.value().toObject(); - receipts.push_back({userIt.key(), - fromJson<QDateTime>(user["ts"_ls])}); + const auto reads = + eventIt.value().toObject().value("m.read"_ls).toObject(); + QVector<UserTimestamp> usersAtEvent; + usersAtEvent.reserve(reads.size()); + for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) { + const auto user = userIt.value().toObject(); + usersAtEvent.push_back( + { userIt.key(), fromJson<QDateTime>(user["ts"_ls]) }); } - _eventsWithReceipts.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 c15a01c2..b87e00f6 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -1,54 +1,35 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "event.h" -#include <QtCore/QVector> #include <QtCore/QDateTime> +#include <QtCore/QVector> -namespace QMatrixClient -{ - struct Receipt - { - QString userId; - QDateTime timestamp; - }; - struct ReceiptsForEvent - { - QString evtId; - QVector<Receipt> receipts; - }; - using EventsWithReceipts = QVector<ReceiptsForEvent>; +namespace Quotient { +struct UserTimestamp { + QString userId; + QDateTime timestamp; +}; +struct ReceiptsForEvent { + QString evtId; + QVector<UserTimestamp> receipts; +}; +using EventsWithReceipts = QVector<ReceiptsForEvent>; - class ReceiptEvent: public Event - { - public: - DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) - explicit ReceiptEvent(const QJsonObject& obj); +template <> +QUOTIENT_API EventsWithReceipts fromJson(const QJsonObject& json); +QUOTIENT_API QJsonObject toJson(const EventsWithReceipts& ewrs); - const EventsWithReceipts& eventsWithReceipts() const - { return _eventsWithReceipts; } +class QUOTIENT_API ReceiptEvent + : public EventTemplate<ReceiptEvent, Event, EventsWithReceipts> { +public: + QUO_EVENT(ReceiptEvent, "m.receipt") + using EventTemplate::EventTemplate; - private: - EventsWithReceipts _eventsWithReceipts; - }; - REGISTER_EVENT_TYPE(ReceiptEvent) - DEFINE_EVENTTYPE_ALIAS(Receipt, ReceiptEvent) -} // namespace QMatrixClient + [[deprecated("Use content() instead")]] + EventsWithReceipts eventsWithReceipts() const { return content(); } +}; +} // namespace Quotient diff --git a/lib/events/redactionevent.cpp b/lib/events/redactionevent.cpp deleted file mode 100644 index bf467718..00000000 --- a/lib/events/redactionevent.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "redactionevent.h" diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index a72a8ff9..a2e0b73b 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -1,41 +1,21 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "roomevent.h" -namespace QMatrixClient -{ - class RedactionEvent : public RoomEvent - { - public: - DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) +namespace Quotient { +class QUOTIENT_API RedactionEvent : public RoomEvent { +public: + QUO_EVENT(RedactionEvent, "m.room.redaction") - explicit RedactionEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj) - { } + using RoomEvent::RoomEvent; - QString redactedEvent() const - { return fullJson()["redacts"_ls].toString(); } - QString reason() const - { return contentJson()["reason"_ls].toString(); } - }; - REGISTER_EVENT_TYPE(RedactionEvent) - DEFINE_EVENTTYPE_ALIAS(Redaction, RedactionEvent) -} // namespace QMatrixClient + QString redactedEvent() const + { + return fullJson()["redacts"_ls].toString(); + } + QUO_CONTENT_GETTER(QString, reason) +}; +} // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 491861b1..1986f852 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -1,42 +1,23 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "event.h" - #include "eventcontent.h" +#include "stateevent.h" + +namespace Quotient { +class QUOTIENT_API RoomAvatarEvent + : public KeylessStateEventBase<RoomAvatarEvent, + EventContent::ImageContent> { + // It's a bit of an overkill to use a full-fledged ImageContent + // because in reality m.room.avatar usually only has a single URL, + // without a thumbnail. But The Spec says there be thumbnails, and + // we follow The Spec (and ImageContent is very convenient to reuse here). +public: + QUO_EVENT(RoomAvatarEvent, "m.room.avatar") + using KeylessStateEventBase::KeylessStateEventBase; -namespace QMatrixClient -{ - class RoomAvatarEvent: public StateEvent<EventContent::ImageContent> - { - // It's a bit of an overkill to use a full-fledged ImageContent - // because in reality m.room.avatar usually only has a single URL, - // without a thumbnail. But The Spec says there be thumbnails, and - // we follow The Spec. - public: - DEFINE_EVENT_TYPEID("m.room.avatar", RoomAvatarEvent) - explicit RoomAvatarEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - { } - QUrl url() const { return content().url; } - }; - REGISTER_EVENT_TYPE(RoomAvatarEvent) - DEFINE_EVENTTYPE_ALIAS(RoomAvatar, RoomAvatarEvent) -} // namespace QMatrixClient + QUrl url() const { return content().url(); } +}; +} // namespace Quotient diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h new file mode 100644 index 00000000..c73bc92a --- /dev/null +++ b/lib/events/roomcanonicalaliasevent.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com> +// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "stateevent.h" + +namespace Quotient { +namespace EventContent { + struct AliasesEventContent { + QString canonicalAlias; + QStringList altAliases; + }; +} // namespace EventContent + +template<> +inline EventContent::AliasesEventContent fromJson(const QJsonObject& jo) +{ + return EventContent::AliasesEventContent { + fromJson<QString>(jo["alias"_ls]), + fromJson<QStringList>(jo["alt_aliases"_ls]) + }; +} +template<> +inline auto toJson(const EventContent::AliasesEventContent& c) +{ + QJsonObject jo; + addParam<IfNotEmpty>(jo, QStringLiteral("alias"), c.canonicalAlias); + addParam<IfNotEmpty>(jo, QStringLiteral("alt_aliases"), c.altAliases); + return jo; +} + +class QUOTIENT_API RoomCanonicalAliasEvent + : public KeylessStateEventBase<RoomCanonicalAliasEvent, + EventContent::AliasesEventContent> { +public: + QUO_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias") + using KeylessStateEventBase::KeylessStateEventBase; + + QString alias() const { return content().canonicalAlias; } + QStringList altAliases() const { return content().altAliases; } +}; +} // namespace Quotient diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp new file mode 100644 index 00000000..3b5024d5 --- /dev/null +++ b/lib/events/roomcreateevent.cpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roomcreateevent.h" + +using namespace Quotient; + +template <> +RoomType Quotient::fromJson(const QJsonValue& jv) +{ + return enumFromJsonString(jv.toString(), RoomTypeStrings, + RoomType::Undefined); +} + +bool RoomCreateEvent::isFederated() const +{ + return contentPart<bool>("m.federate"_ls); +} + +QString RoomCreateEvent::version() const +{ + return contentPart<QString>("room_version"_ls); +} + +RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const +{ + const auto predJson = contentPart<QJsonObject>("predecessor"_ls); + return { fromJson<QString>(predJson[RoomIdKeyL]), + fromJson<QString>(predJson[EventIdKeyL]) }; +} + +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 new file mode 100644 index 00000000..5968e187 --- /dev/null +++ b/lib/events/roomcreateevent.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "stateevent.h" +#include "quotient_common.h" + +namespace Quotient { +class QUOTIENT_API RoomCreateEvent : public StateEvent { +public: + QUO_EVENT(RoomCreateEvent, "m.room.create") + + using StateEvent::StateEvent; + + struct Predecessor { + QString roomId; + QString eventId; + }; + + bool isFederated() const; + QString version() const; + Predecessor predecessor() const; + bool isUpgrade() const; + RoomType roomType() const; +}; +} // namespace Quotient diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 80d121de..e98cb591 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -1,88 +1,76 @@ -/****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roomevent.h" -#include "redactionevent.h" -#include "converters.h" #include "logging.h" +#include "redactionevent.h" -using namespace QMatrixClient; - -[[gnu::unused]] static auto roomEventTypeInitialised = - Event::factory_t::chainFactory<RoomEvent>(); - -RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson) - : Event(type, matrixType, contentJson) -{ } +using namespace Quotient; -RoomEvent::RoomEvent(Type type, const QJsonObject& json) - : Event(type, json) +RoomEvent::RoomEvent(const QJsonObject& json) : Event(json) { - const auto unsignedData = json[UnsignedKeyL].toObject(); - const auto redaction = unsignedData[RedactedCauseKeyL]; - if (redaction.isObject()) - { - _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); - return; - } - - const auto& txnId = transactionId(); - if (!txnId.isEmpty()) - qCDebug(EVENTS) << "Event transactionId:" << txnId; + if (const auto redaction = unsignedPart<QJsonObject>(RedactedCauseKeyL); + !redaction.isEmpty()) + _redactedBecause = loadEvent<RedactionEvent>(redaction); } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job -QString RoomEvent::id() const -{ - return fullJson()[EventIdKeyL].toString(); -} +QString RoomEvent::id() const { return fullJson()[EventIdKeyL].toString(); } -QDateTime RoomEvent::timestamp() const +QDateTime RoomEvent::originTimestamp() const { - return QMatrixClient::fromJson<QDateTime>(fullJson()["origin_server_ts"_ls]); + return Quotient::fromJson<QDateTime>(fullJson()["origin_server_ts"_ls]); } QString RoomEvent::roomId() const { - return fullJson()["room_id"_ls].toString(); + return fullJson()[RoomIdKeyL].toString(); } QString RoomEvent::senderId() const { - return fullJson()["sender"_ls].toString(); + return fullJson()[SenderKeyL].toString(); +} + +bool RoomEvent::isReplaced() const +{ + return unsignedPart<QJsonObject>("m.relations"_ls).contains("m.replace"); +} + +QString RoomEvent::replacedBy() const +{ + // clang-format off + return unsignedPart<QJsonObject>("m.relations"_ls) + .value("m.replace"_ls).toObject() + .value(EventIdKeyL).toString(); + // clang-format on } QString RoomEvent::redactionReason() const { - return isRedacted() ? _redactedBecause->reason() : QString{}; + return isRedacted() ? _redactedBecause->reason() : QString {}; } QString RoomEvent::transactionId() const { - return unsignedJson()["transaction_id"_ls].toString(); + return unsignedPart<QString>("transaction_id"_ls); } QString RoomEvent::stateKey() const { - return fullJson()["state_key"_ls].toString(); + return fullJson()[StateKeyKeyL].toString(); +} + +void RoomEvent::setRoomId(const QString& roomId) +{ + editJson().insert(RoomIdKey, roomId); +} + +void RoomEvent::setSender(const QString& senderId) +{ + editJson().insert(SenderKey, senderId); } void RoomEvent::setTransactionId(const QString& txnId) @@ -90,36 +78,35 @@ void RoomEvent::setTransactionId(const QString& txnId) auto unsignedData = fullJson()[UnsignedKeyL].toObject(); unsignedData.insert(QStringLiteral("transaction_id"), txnId); editJson().insert(UnsignedKey, unsignedData); - qCDebug(EVENTS) << "New event transactionId:" << txnId; Q_ASSERT(transactionId() == txnId); } void RoomEvent::addId(const QString& newId) { - Q_ASSERT(id().isEmpty()); Q_ASSERT(!newId.isEmpty()); + Q_ASSERT(id().isEmpty()); + Q_ASSERT(!newId.isEmpty()); editJson().insert(EventIdKey, newId); qCDebug(EVENTS) << "Event txnId -> id:" << transactionId() << "->" << id(); Q_ASSERT(id() == newId); } -QJsonObject makeCallContentJson(const QString& callId, int version, - QJsonObject content) +void RoomEvent::dumpTo(QDebug dbg) const { - content.insert(QStringLiteral("call_id"), callId); - content.insert(QStringLiteral("version"), version); - return content; + Event::dumpTo(dbg); + dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')'; } -CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, - const QString& callId, int version, - const QJsonObject& contentJson) - : RoomEvent(type, matrixType, - makeCallContentJson(callId, version, contentJson)) -{ } +#ifdef Quotient_E2EE_ENABLED +void RoomEvent::setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent) +{ + _originalEvent = std::move(originalEvent); +} -CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) - : RoomEvent(type, json) +const QJsonObject RoomEvent::encryptedJson() const { - if (callId().isEmpty()) - qCWarning(EVENTS) << id() << "is a call event with an empty call id"; + if(!_originalEvent) { + return {}; + } + return _originalEvent->fullJson(); } +#endif diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index ce96174e..203434f6 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -1,20 +1,5 @@ -/****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -22,85 +7,78 @@ #include <QtCore/QDateTime> -namespace QMatrixClient -{ - class RedactionEvent; +namespace Quotient { +class RedactionEvent; - /** This class corresponds to m.room.* events */ - class RoomEvent : public Event +// That check could look into Event and find most stuff already deleted... +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) +class QUOTIENT_API RoomEvent : public Event { +public: + QUO_BASE_EVENT(RoomEvent, {}, Event::BaseMetaType) + + ~RoomEvent() override; // Don't inline this - see the private section + + QString id() const; + QDateTime originTimestamp() const; + 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); } + const event_ptr_tt<RedactionEvent>& redactedBecause() const { - 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>; + return _redactedBecause; + } + QString redactionReason() const; + QString transactionId() const; + QString stateKey() const; - // RedactionEvent is an incomplete type here so we cannot inline - // constructors and destructors and we cannot use 'using'. - RoomEvent(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); - RoomEvent(Type type, const QJsonObject& json); - ~RoomEvent() override; + //! \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); + //! \brief Fill the pending event object with the transaction id + //! \param txnId - transaction id, normally obtained from + //! Connection::generateTxnId() + void setTransactionId(const QString& txnId); - QString id() const; - QDateTime timestamp() const; - QString roomId() const; - QString senderId() const; - bool isRedacted() const { return bool(_redactedBecause); } - const event_ptr_tt<RedactionEvent>& redactedBecause() const - { - return _redactedBecause; - } - QString redactionReason() const; - QString transactionId() const; - QString stateKey() const; + //! \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); - /** - * 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() - */ - void setTransactionId(const QString& txnId); +#ifdef Quotient_E2EE_ENABLED + void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent); + const RoomEvent* originalEvent() const { return _originalEvent.get(); } + const QJsonObject encryptedJson() const; +#endif - /** - * 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 - */ - void addId(const QString& newId); +protected: + explicit RoomEvent(const QJsonObject& json); + void dumpTo(QDebug dbg) const override; - private: - event_ptr_tt<RedactionEvent> _redactedBecause; - }; - using RoomEventPtr = event_ptr_tt<RoomEvent>; - using RoomEvents = EventsArray<RoomEvent>; - using RoomEventsRange = Range<RoomEvents>; +private: + // RedactionEvent is an incomplete type here so we cannot inline + // constructors using it and also destructors (with 'using', in particular). + event_ptr_tt<RedactionEvent> _redactedBecause; - class CallEventBase: public RoomEvent - { - public: - CallEventBase(Type type, event_mtype_t matrixType, - const QString& callId, int version, - const QJsonObject& contentJson = {}); - CallEventBase(Type type, const QJsonObject& json); - ~CallEventBase() override = default; - bool isCallEvent() const override { return true; } +#ifdef Quotient_E2EE_ENABLED + event_ptr_tt<RoomEvent> _originalEvent; +#endif +}; +using RoomEventPtr = event_ptr_tt<RoomEvent>; +using RoomEvents = EventsArray<RoomEvent>; +using RoomEventsRange = Range<RoomEvents>; - QString callId() const { return content<QString>("call_id"_ls); } - int version() const { return content<int>("version"_ls); } - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::RoomEvent*) -Q_DECLARE_METATYPE(const QMatrixClient::RoomEvent*) +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::RoomEvent*) +Q_DECLARE_METATYPE(const Quotient::RoomEvent*) diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h new file mode 100644 index 00000000..dad5df8b --- /dev/null +++ b/lib/events/roomkeyevent.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "event.h" + +namespace Quotient { +class QUOTIENT_API RoomKeyEvent : public Event +{ +public: + QUO_EVENT(RoomKeyEvent, "m.room_key") + + using Event::Event; + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, + const QString& sessionId, const QString& sessionKey) + : Event(basicJson(TypeId, { + { "algorithm", algorithm }, + { "room_id", roomId }, + { "session_id", sessionId }, + { "session_key", sessionKey }, + })) + {} + + QUO_CONTENT_GETTER(QString, algorithm) + QUO_CONTENT_GETTER(QString, roomId) + QUO_CONTENT_GETTER(QString, sessionId) + QByteArray sessionKey() const + { + return contentPart<QString>("session_key"_ls).toLatin1(); + } +}; +} // namespace Quotient diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index eaa3302c..4e7eae1b 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -1,102 +1,102 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Karol Kosek <krkkx@protonmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roommemberevent.h" - -#include "converters.h" #include "logging.h" -#include <array> - -using namespace QMatrixClient; - -static const std::array<QString, 5> membershipStrings = { { - QStringLiteral("invite"), QStringLiteral("join"), - QStringLiteral("knock"), QStringLiteral("leave"), - QStringLiteral("ban") -} }; - -namespace QMatrixClient -{ - template <> - struct FromJson<MembershipType> +namespace Quotient { +template <> +struct JsonConverter<Membership> { + static Membership load(const QJsonValue& jv) { - MembershipType operator()(const QJsonValue& jv) const - { - const auto& membershipString = jv.toString(); - for (auto it = membershipStrings.begin(); - it != membershipStrings.end(); ++it) - if (membershipString == *it) - return MembershipType(it - membershipStrings.begin()); + if (const auto& ms = jv.toString(); !ms.isEmpty()) + return flagFromJsonString<Membership>(ms, MembershipStrings); - qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; - return MembershipType::Undefined; - } - }; + qCWarning(EVENTS) << "Empty membership state"; + return Membership::Invalid; + } +}; +} // namespace Quotient -} +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(json["displayname"_ls].toString()) - , avatarUrl(json["avatar_url"_ls].toString()) -{ } + , displayName(fromJson<Omittable<QString>>(json["displayname"_ls])) + , avatarUrl(fromJson<Omittable<QString>>(json["avatar_url"_ls])) + , reason(json["reason"_ls].toString()) +{ + if (displayName) + displayName = sanitized(*displayName); +} + +QJsonObject MemberEventContent::toJson() const +{ + QJsonObject o; + if (membership != Membership::Invalid) + o.insert(QStringLiteral("membership"), + flagToJsonString(membership, MembershipStrings)); + if (displayName) + o.insert(QStringLiteral("displayname"), *displayName); + if (avatarUrl && avatarUrl->isValid()) + o.insert(QStringLiteral("avatar_url"), avatarUrl->toString()); + if (!reason.isEmpty()) + o.insert(QStringLiteral("reason"), reason); + return o; +} -void MemberEventContent::fillJson(QJsonObject* o) const +bool RoomMemberEvent::changesMembership() 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]); - o->insert(QStringLiteral("displayname"), displayName); - if (avatarUrl.isValid()) - o->insert(QStringLiteral("avatar_url"), avatarUrl.toString()); + return !prevContent() || prevContent()->membership != membership(); } bool RoomMemberEvent::isInvite() const { - return membership() == MembershipType::Invite && - (!prevContent() || prevContent()->membership != membership()); + return membership() == Membership::Invite && changesMembership(); +} + +bool RoomMemberEvent::isRejectedInvite() const +{ + return membership() == Membership::Leave && prevContent() + && prevContent()->membership == Membership::Invite; } bool RoomMemberEvent::isJoin() const { - return membership() == MembershipType::Join && - (!prevContent() || prevContent()->membership != membership()); + return membership() == Membership::Join && changesMembership(); } bool RoomMemberEvent::isLeave() const { - return membership() == MembershipType::Leave && - prevContent() && prevContent()->membership != membership() && - prevContent()->membership != MembershipType::Ban; + return membership() == Membership::Leave && prevContent() + && prevContent()->membership != membership() + && prevContent()->membership != Membership::Ban + && prevContent()->membership != Membership::Invite; +} + +bool RoomMemberEvent::isBan() const +{ + return membership() == Membership::Ban && changesMembership(); +} + +bool RoomMemberEvent::isUnban() const +{ + return membership() == Membership::Leave && prevContent() + && prevContent()->membership == Membership::Ban; } bool RoomMemberEvent::isRename() const { - auto prevName = prevContent() ? prevContent()->displayName : QString(); - return displayName() != prevName; + return prevContent() && prevContent()->displayName + ? newDisplayName() != *prevContent()->displayName + : newDisplayName().has_value(); } bool RoomMemberEvent::isAvatarUpdate() const { - auto prevAvatarUrl = prevContent() ? prevContent()->avatarUrl : QUrl(); - return avatarUrl() != prevAvatarUrl; + return prevContent() && prevContent()->avatarUrl + ? newAvatarUrl() != *prevContent()->avatarUrl + : newAvatarUrl().has_value(); } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index db25d026..9f063136 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -1,89 +1,66 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Karol Kosek <krkkx@protonmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "stateevent.h" -#include "eventcontent.h" +#include "quotient_common.h" -namespace QMatrixClient -{ - class MemberEventContent: public EventContent::Base - { - public: - enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, - Undefined }; - - explicit MemberEventContent(MembershipType mt = MembershipType::Join) - : membership(mt) - { } - explicit MemberEventContent(const QJsonObject& json); - explicit MemberEventContent(const QJsonValue& jv) - : MemberEventContent(jv.toObject()) - { } +namespace Quotient { +class QUOTIENT_API MemberEventContent { +public: + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; - MembershipType membership; - bool isDirect = false; - QString displayName; - QUrl avatarUrl; + QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {} + explicit MemberEventContent(const QJsonObject& json); + QJsonObject toJson() const; - protected: - void fillJson(QJsonObject* o) const override; - }; + Membership membership; + /// (Only for invites) Whether the invite is to a direct chat + bool isDirect = false; + Omittable<QString> displayName; + Omittable<QUrl> avatarUrl; + QString reason; +}; - using MembershipType = MemberEventContent::MembershipType; - - class RoomMemberEvent: public StateEvent<MemberEventContent> - { - Q_GADGET - public: - DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) +using MembershipType [[deprecated("Use Membership instead")]] = Membership; - using MembershipType = MemberEventContent::MembershipType; +class QUOTIENT_API RoomMemberEvent + : public KeyedStateEventBase<RoomMemberEvent, MemberEventContent> { + Q_GADGET +public: + QUO_EVENT(RoomMemberEvent, "m.room.member") - explicit RoomMemberEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - { } - RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c.toJson()) - { } + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; - // This is a special constructor enabling RoomMemberEvent to be - // a base class for more specific member events. - RoomMemberEvent(Type type, const QJsonObject& fullJson) - : StateEvent(type, fullJson) - { } + using KeyedStateEventBase::KeyedStateEventBase; - MembershipType membership() const { return content().membership; } - QString userId() const - { return fullJson()["state_key"_ls].toString(); } - bool isDirect() const { return content().isDirect; } - QString displayName() const { return content().displayName; } - QUrl avatarUrl() const { return content().avatarUrl; } - bool isInvite() const; - bool isJoin() const; - bool isLeave() const; - bool isRename() const; - bool isAvatarUpdate() const; - - private: - REGISTER_ENUM(MembershipType) - }; - REGISTER_EVENT_TYPE(RoomMemberEvent) - DEFINE_EVENTTYPE_ALIAS(RoomMember, RoomMemberEvent) -} // namespace QMatrixClient + Membership membership() const { return content().membership; } + QString userId() const { return stateKey(); } + bool isDirect() const { return content().isDirect; } + Omittable<QString> newDisplayName() const { return content().displayName; } + Omittable<QUrl> newAvatarUrl() const { return content().avatarUrl; } + [[deprecated("Use newDisplayName() instead")]] QString displayName() const + { + return newDisplayName().value_or(QString()); + } + [[deprecated("Use newAvatarUrl() instead")]] QUrl avatarUrl() const + { + return newAvatarUrl().value_or(QUrl()); + } + QString reason() const { return content().reason; } + bool changesMembership() const; + bool isBan() const; + bool isUnban() const; + bool isInvite() const; + bool isRejectedInvite() const; + bool isJoin() const; + bool isLeave() const; + bool isRename() const; + bool isAvatarUpdate() const; +}; +} // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 1c5cf058..df4840b3 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -1,60 +1,71 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roommessageevent.h" #include "logging.h" +#include "events/eventrelation.h" +#include <QtCore/QFileInfo> #include <QtCore/QMimeDatabase> +#include <QtGui/QImageReader> +#if QT_VERSION_MAJOR < 6 +# include <QtMultimedia/QMediaResource> +#endif -using namespace QMatrixClient; +using namespace Quotient; using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; +namespace { // Supporting internal definitions +constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto MsgTypeKey = "msgtype"_ls; +constexpr auto FormattedBodyKey = "formatted_body"_ls; +constexpr auto TextTypeKey = "m.text"_ls; +constexpr auto EmoteTypeKey = "m.emote"_ls; +constexpr auto NoticeTypeKey = "m.notice"_ls; +constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls; + template <typename ContentT> TypedBase* make(const QJsonObject& json) { return new ContentT(json); } -struct MsgTypeDesc +template <> +TypedBase* make<TextContent>(const QJsonObject& json) { - QString matrixType; + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + ? new TextContent(json) + : nullptr; +} + +struct MsgTypeDesc { + QLatin1String matrixType; MsgType enumType; TypedBase* (*maker)(const QJsonObject&); }; -const std::vector<MsgTypeDesc> msgTypes = - { { QStringLiteral("m.text"), MsgType::Text, make<TextContent> } - , { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> } - , { QStringLiteral("m.notice"), MsgType::Notice, make<TextContent> } - , { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> } - , { QStringLiteral("m.file"), MsgType::File, make<FileContent> } - , { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> } - , { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> } - , { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> } - }; +const std::vector<MsgTypeDesc> msgTypes = { + { TextTypeKey, MsgType::Text, make<TextContent> }, + { EmoteTypeKey, MsgType::Emote, make<TextContent> }, + { NoticeTypeKey, MsgType::Notice, make<TextContent> }, + { "m.image"_ls, MsgType::Image, make<ImageContent> }, + { "m.file"_ls, MsgType::File, make<FileContent> }, + { "m.location"_ls, MsgType::Location, make<LocationContent> }, + { "m.video"_ls, MsgType::Video, make<VideoContent> }, + { "m.audio"_ls, MsgType::Audio, make<AudioContent> } +}; QString msgTypeToJson(MsgType enumType) { auto it = std::find_if(msgTypes.begin(), msgTypes.end(), - [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; }); + [=](const MsgTypeDesc& mtd) { + return mtd.enumType == enumType; + }); if (it != msgTypes.end()) return it->matrixType; @@ -64,59 +75,126 @@ QString msgTypeToJson(MsgType enumType) MsgType jsonToMsgType(const QString& matrixType) { auto it = std::find_if(msgTypes.begin(), msgTypes.end(), - [=](const MsgTypeDesc& mtd) { return mtd.matrixType == matrixType; }); + [=](const MsgTypeDesc& mtd) { + return mtd.matrixType == matrixType; + }); if (it != msgTypes.end()) return it->enumType; return MsgType::Unknown; } -inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType, - TypedBase* content) +inline bool isReplacement(const Omittable<EventRelation>& rel) { - auto json = content ? content->toJson() : QJsonObject(); - json.insert(QStringLiteral("msgtype"), jsonMsgType); - json.insert(QStringLiteral("body"), plainBody); - return json; + return rel && rel->type == EventRelation::ReplacementType; } -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; +} // anonymous namespace + +QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, + TypedBase* content) +{ + QJsonObject json; + if (content) { + // TODO: replace with content->fillJson(json) when it starts working + json = content->toJson(); + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey + && jsonMsgType != EmoteTypeKey) { + if (json.contains(RelatesToKey)) { + json.remove(RelatesToKey); + qCWarning(EVENTS) + << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; + } + } else if (auto* textContent = static_cast<const TextContent*>(content); + textContent->relatesTo + && textContent->relatesTo->type + == EventRelation::ReplacementType) { + auto newContentJson = json.take("m.new_content"_ls).toObject(); + newContentJson.insert(BodyKey, plainBody); + newContentJson.insert(MsgTypeKey, jsonMsgType); + json.insert(QStringLiteral("m.new_content"), newContentJson); + json[MsgTypeKey] = jsonMsgType; + json[BodyKeyL] = "* " + plainBody; + return json; + } + } + json.insert(MsgTypeKey, jsonMsgType); + json.insert(BodyKey, plainBody); + return json; +} RoomMessageEvent::RoomMessageEvent(const QString& plainBody, - const QString& jsonMsgType, TypedBase* content) - : RoomEvent(typeId(), matrixTypeId(), - toMsgJson(plainBody, jsonMsgType, content)) + const QString& jsonMsgType, + TypedBase* content) + : RoomEvent( + basicJson(TypeId, assembleContentJson(plainBody, jsonMsgType, content))) , _content(content) -{ } +{} -RoomMessageEvent::RoomMessageEvent(const QString& plainBody, - MsgType msgType, TypedBase* content) +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType, + TypedBase* content) : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) -{ } +{} + +#if QT_VERSION_MAJOR < 6 +TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) +{ + auto filePath = file.absoluteFilePath(); + auto localUrl = QUrl::fromLocalFile(filePath); + auto mimeType = QMimeDatabase().mimeTypeForFile(file); + if (!asGenericFile) { + auto mimeTypeName = mimeType.name(); + if (mimeTypeName.startsWith("image/")) + return new ImageContent(localUrl, file.size(), mimeType, + QImageReader(filePath).size(), + file.fileName()); + + // duration can only be obtained asynchronously and can only be reliably + // done by starting to play the file. Left for a future implementation. + if (mimeTypeName.startsWith("video/")) + return new VideoContent(localUrl, file.size(), mimeType, + QMediaResource(localUrl).resolution(), + file.fileName()); + + if (mimeTypeName.startsWith("audio/")) + return new AudioContent(localUrl, file.size(), mimeType, + file.fileName()); + } + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); +} + +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, bool asGenericFile) + : RoomMessageEvent(plainBody, + asGenericFile ? QStringLiteral("m.file") + : rawMsgTypeForFile(file), + contentFromFile(file, asGenericFile)) +{} +#endif RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj), _content(nullptr) + : RoomEvent(obj), _content(nullptr) { if (isRedacted()) return; const QJsonObject content = contentJson(); - if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) - { + if (content.contains(MsgTypeKey) && content.contains(BodyKeyL)) { auto msgtype = content[MsgTypeKey].toString(); - for (const auto& mt: msgTypes) - if (mt.matrixType == msgtype) + bool msgTypeFound = false; + for (const auto& mt : msgTypes) + if (mt.matrixType == msgtype) { _content.reset(mt.maker(content)); + msgTypeFound = true; + } - if (!_content) - { - qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," + if (!msgTypeFound) { + qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } - } - else - { + } else { qCWarning(EVENTS) << "No body or msgtype in room message event"; qCWarning(EVENTS) << formatJson << obj; } @@ -129,27 +207,26 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentJson()[MsgTypeKey].toString(); + return contentPart<QString>(MsgTypeKey); } QString RoomMessageEvent::plainBody() const { - return contentJson()[BodyKey].toString(); + return contentPart<QString>(BodyKeyL); } QMimeType RoomMessageEvent::mimeType() const { static const auto PlainTextMimeType = - QMimeDatabase().mimeTypeForName("text/plain"); + QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; - ; } bool RoomMessageEvent::hasTextContent() const { - return content() && - (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes + return !content() + || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote + || msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const @@ -162,62 +239,115 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } -TextContent::TextContent(const QString& text, const QString& contentType) - : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) +QString RoomMessageEvent::replacedEvent() const +{ + if (!content() || !hasTextContent()) + return {}; + + const auto& rel = static_cast<const TextContent*>(content())->relatesTo; + return isReplacement(rel) ? rel->eventId : QString(); +} + +QString rawMsgTypeForMimeType(const QMimeType& mimeType) +{ + auto name = mimeType.name(); + return name.startsWith("image/") + ? QStringLiteral("m.image") + : name.startsWith("video/") + ? QStringLiteral("m.video") + : name.startsWith("audio/") ? QStringLiteral("m.audio") + : QStringLiteral("m.file"); +} + +QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url) { - if (contentType == "org.matrix.custom.html") + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url)); +} + +QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); +} + +TextContent::TextContent(QString text, const QString& contentType, + Omittable<EventRelation> relatesTo) + : mimeType(QMimeDatabase().mimeTypeForName(contentType)) + , body(std::move(text)) + , relatesTo(std::move(relatesTo)) +{ + if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } TextContent::TextContent(const QJsonObject& json) + : relatesTo(fromJson<Omittable<EventRelation>>(json[RelatesToKey])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); static const auto HtmlMimeType = db.mimeTypeForName("text/html"); - // Special-casing the custom matrix.org's (actually, Riot's) way + const auto actualJson = isReplacement(relatesTo) + ? json.value("m.new_content"_ls).toObject() + : json; + // Special-casing the custom matrix.org's (actually, Element's) way // of sending HTML messages. - if (json["format"_ls].toString() == "org.matrix.custom.html") - { + if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json["formatted_body"_ls].toString(); + body = actualJson[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; - body = json[BodyKey].toString(); + body = actualJson[BodyKeyL].toString(); } } -void TextContent::fillJson(QJsonObject* json) const +void TextContent::fillJson(QJsonObject &json) const { - Q_ASSERT(json); - if (mimeType.inherits("text/html")) - { - json->insert(QStringLiteral("format"), - QStringLiteral("org.matrix.custom.html")); - json->insert(QStringLiteral("formatted_body"), body); + static const auto FormatKey = QStringLiteral("format"); + + if (mimeType.inherits("text/html")) { + json.insert(FormatKey, HtmlContentTypeId); + json.insert(FormattedBodyKey, body); + } + if (relatesTo) { + json.insert( + QStringLiteral("m.relates_to"), + relatesTo->type == EventRelation::ReplyType + ? QJsonObject { { relatesTo->type, + QJsonObject { + { EventIdKey, relatesTo->eventId } } } } + : QJsonObject { { RelTypeKey, relatesTo->type }, + { EventIdKey, relatesTo->eventId } }); + if (relatesTo->type == EventRelation::ReplacementType) { + QJsonObject newContentJson; + if (mimeType.inherits("text/html")) { + newContentJson.insert(FormatKey, HtmlContentTypeId); + newContentJson.insert(FormattedBodyKey, body); + } + json.insert(QStringLiteral("m.new_content"), newContentJson); + } } } -LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail) +LocationContent::LocationContent(const QString& geoUri, + const Thumbnail& thumbnail) : geoUri(geoUri), thumbnail(thumbnail) -{ } +{} LocationContent::LocationContent(const QJsonObject& json) : TypedBase(json) , geoUri(json["geo_uri"_ls].toString()) , thumbnail(json["info"_ls].toObject()) -{ } +{} QMimeType LocationContent::type() const { return QMimeDatabase().mimeTypeForData(geoUri.toLatin1()); } -void LocationContent::fillJson(QJsonObject* o) const +void LocationContent::fillJson(QJsonObject& o) const { - Q_ASSERT(o); - o->insert(QStringLiteral("geo_uri"), geoUri); - o->insert(QStringLiteral("info"), toInfoJson(thumbnail)); + o.insert(QStringLiteral("geo_uri"), geoUri); + o.insert(QStringLiteral("info"), toInfoJson(thumbnail)); } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 4c29a93e..889fc4dc 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -1,188 +1,233 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "roomevent.h" #include "eventcontent.h" +#include "eventrelation.h" +#include "roomevent.h" + +class QFileInfo; + +namespace Quotient { +namespace MessageEventContent = EventContent; // Back-compatibility -namespace QMatrixClient -{ - namespace MessageEventContent = EventContent; // Back-compatibility +/** + * The event class corresponding to m.room.message events + */ +class QUOTIENT_API RoomMessageEvent : public RoomEvent { + Q_GADGET +public: + QUO_EVENT(RoomMessageEvent, "m.room.message") + + enum class MsgType { + Text, + Emote, + Notice, + Image, + File, + Location, + Video, + Audio, + Unknown + }; + + RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, + EventContent::TypedBase* content = nullptr); + 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; + QString rawMsgtype() const; + QString plainBody() const; + const EventContent::TypedBase* content() const { return _content.data(); } + template <typename VisitorT> + void editContent(VisitorT&& visitor) + { + visitor(*_content); + editJson()[ContentKeyL] = assembleContentJson(plainBody(), rawMsgtype(), + _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); + static QString rawMsgTypeForFile(const QFileInfo& fi); + +private: + QScopedPointer<EventContent::TypedBase> _content; + + // FIXME: should it really be static? + static QJsonObject assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, + EventContent::TypedBase* content); + + Q_ENUM(MsgType) +}; + +using MessageEventType = RoomMessageEvent::MsgType; + +namespace EventContent { + + struct [[deprecated("Use Quotient::EventRelation instead")]] RelatesTo + : EventRelation { + static constexpr auto ReplyTypeId() { return ReplyType; } + static constexpr auto ReplacementTypeId() { return ReplacementType; } + }; + [[deprecated("Use EventRelation::replyTo() instead")]] + inline auto replyTo(QString eventId) + { + return EventRelation::replyTo(std::move(eventId)); + } + [[deprecated("Use EventRelation::replace() instead")]] + inline auto replacementOf(QString eventId) + { + return EventRelation::replace(std::move(eventId)); + } + + // Additional event content types /** - * The event class corresponding to m.room.message events + * Rich text content for m.text, m.emote, m.notice + * + * Available fields: mimeType, body. The body can be either rich text + * or plain text, depending on what mimeType specifies. */ - class 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(EventContent::TypedBase* content READ content CONSTANT) - public: - DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) - - enum class MsgType - { - Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown - }; - - RoomMessageEvent(const QString& plainBody, - const QString& jsonMsgType, - EventContent::TypedBase* content = nullptr); - explicit RoomMessageEvent(const QString& plainBody, - MsgType msgType = MsgType::Text, - EventContent::TypedBase* content = nullptr); - explicit RoomMessageEvent(const QJsonObject& obj); - - MsgType msgtype() const; - QString rawMsgtype() const; - QString plainBody() const; - EventContent::TypedBase* content() const - { return _content.data(); } - QMimeType mimeType() const; - bool hasTextContent() const; - bool hasFileContent() const; - bool hasThumbnail() const; - - private: - QScopedPointer<EventContent::TypedBase> _content; - - REGISTER_ENUM(MsgType) + class QUOTIENT_API TextContent : public TypedBase { + public: + TextContent(QString text, const QString& contentType, + Omittable<EventRelation> relatesTo = none); + explicit TextContent(const QJsonObject& json); + + QMimeType type() const override { return mimeType; } + + QMimeType mimeType; + QString body; + Omittable<EventRelation> relatesTo; + + protected: + void fillJson(QJsonObject& json) const override; }; - REGISTER_EVENT_TYPE(RoomMessageEvent) - DEFINE_EVENTTYPE_ALIAS(RoomMessage, RoomMessageEvent) - using MessageEventType = RoomMessageEvent::MsgType; - namespace EventContent - { - // Additional event content types - - /** - * Rich text content for m.text, m.emote, m.notice - * - * Available fields: mimeType, body. The body can be either rich text - * or plain text, depending on what mimeType specifies. - */ - class TextContent: public TypedBase - { - public: - TextContent(const QString& text, const QString& contentType); - explicit TextContent(const QJsonObject& json); - - QMimeType type() const override { return mimeType; } - - QMimeType mimeType; - QString body; - - protected: - void fillJson(QJsonObject* json) const override; - }; - - /** - * Content class for m.location - * - * Available fields: - * - corresponding to the top-level JSON: - * - geoUri ("geo_uri" in JSON) - * - corresponding to the "info" subobject: - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize - */ - class LocationContent: public TypedBase - { - public: - LocationContent(const QString& geoUri, - const ImageInfo& thumbnail); - explicit LocationContent(const QJsonObject& json); - - QMimeType type() const override; - - public: - QString geoUri; - Thumbnail thumbnail; - - protected: - void fillJson(QJsonObject* o) const override; - }; - - /** - * A base class for info types that include duration: audio and video - */ - template <typename ContentT> - class PlayableContent : public ContentT + /** + * Content class for m.location + * + * Available fields: + * - corresponding to the top-level JSON: + * - geoUri ("geo_uri" in JSON) + * - corresponding to the "info" subobject: + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: + * - thumbnail.payloadSize + * - thumbnail.mimeType + * - thumbnail.imageSize + */ + class QUOTIENT_API LocationContent : public TypedBase { + public: + LocationContent(const QString& geoUri, const Thumbnail& thumbnail = {}); + explicit LocationContent(const QJsonObject& json); + + QMimeType type() const override; + + public: + QString geoUri; + Thumbnail thumbnail; + + protected: + void fillJson(QJsonObject& o) const override; + }; + + /** + * A base class for info types that include duration: audio and video + */ + template <typename InfoT> + class PlayableContent : public UrlBasedContent<InfoT> { + public: + using UrlBasedContent<InfoT>::UrlBasedContent; + PlayableContent(const QJsonObject& json) + : UrlBasedContent<InfoT>(json) + , duration(FileInfo::originalInfoJson["duration"_ls].toInt()) + {} + + protected: + void fillInfoJson(QJsonObject& infoJson) const override { - public: - PlayableContent(const QJsonObject& json) - : ContentT(json) - , duration(ContentT::originalInfoJson["duration"_ls].toInt()) - { } - - protected: - void fillJson(QJsonObject* json) const override - { - ContentT::fillJson(json); - auto infoJson = json->take("info"_ls).toObject(); - infoJson.insert(QStringLiteral("duration"), duration); - json->insert(QStringLiteral("info"), infoJson); - } - - public: - int duration; - }; - - /** - * Content class for m.video - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the CS API spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - duration - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for "info": - * - payloadSize - * - mimeType - * - imageSize - */ - using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>; - - /** - * Content class for m.audio - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the CS API spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - duration - */ - using AudioContent = PlayableContent<UrlBasedContent<FileInfo>>; - } // namespace EventContent -} // namespace QMatrixClient + infoJson.insert(QStringLiteral("duration"), duration); + } + + public: + int duration; + }; + + /** + * Content class for m.video + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the CS API spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - duration + * - imageSize (QSize for a combination of "h" and "w" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field, in the same vein as for "info": + * - payloadSize + * - mimeType + * - imageSize + */ + using VideoContent = PlayableContent<ImageInfo>; + + /** + * Content class for m.audio + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the CS API spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - duration + * - thumbnail.url ("thumbnail_url" in JSON - extension to the spec) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field (extension to the spec): + * - payloadSize + * - mimeType + * - imageSize + */ + using AudioContent = PlayableContent<FileInfo>; +} // namespace EventContent +} // namespace Quotient diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp new file mode 100644 index 00000000..d9bd010b --- /dev/null +++ b/lib/events/roompowerlevelsevent.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roompowerlevelsevent.h" + +using namespace Quotient; + +// The default values used below are defined in +// https://spec.matrix.org/v1.3/client-server-api/#mroompower_levels +PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : + invite(json["invite"_ls].toInt(50)), + kick(json["kick"_ls].toInt(50)), + ban(json["ban"_ls].toInt(50)), + redact(json["redact"_ls].toInt(50)), + events(fromJson<QHash<QString, int>>(json["events"_ls])), + eventsDefault(json["events_default"_ls].toInt(0)), + stateDefault(json["state_default"_ls].toInt(0)), + users(fromJson<QHash<QString, int>>(json["users"_ls])), + usersDefault(json["users_default"_ls].toInt(0)), + notifications(Notifications{json["notifications"_ls].toObject()["room"_ls].toInt(50)}) +{} + +QJsonObject PowerLevelsEventContent::toJson() const +{ + QJsonObject o; + o.insert(QStringLiteral("invite"), invite); + o.insert(QStringLiteral("kick"), kick); + o.insert(QStringLiteral("ban"), ban); + o.insert(QStringLiteral("redact"), redact); + o.insert(QStringLiteral("events"), Quotient::toJson(events)); + o.insert(QStringLiteral("events_default"), eventsDefault); + o.insert(QStringLiteral("state_default"), stateDefault); + o.insert(QStringLiteral("users"), Quotient::toJson(users)); + o.insert(QStringLiteral("users_default"), usersDefault); + o.insert(QStringLiteral("notifications"), + QJsonObject { { "room", notifications.room } }); + return o; +} + +int RoomPowerLevelsEvent::powerLevelForEvent(const QString& eventId) const +{ + return events().value(eventId, eventsDefault()); +} + +int RoomPowerLevelsEvent::powerLevelForState(const QString& eventId) const +{ + return events().value(eventId, stateDefault()); +} + +int RoomPowerLevelsEvent::powerLevelForUser(const QString& userId) const +{ + return users().value(userId, usersDefault()); +} diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h new file mode 100644 index 00000000..6150980a --- /dev/null +++ b/lib/events/roompowerlevelsevent.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "stateevent.h" + +namespace Quotient { +struct QUOTIENT_API PowerLevelsEventContent { + struct Notifications { + int room; + }; + + explicit PowerLevelsEventContent(const QJsonObject& json); + QJsonObject toJson() const; + + int invite; + int kick; + int ban; + + int redact; + + QHash<QString, int> events; + int eventsDefault; + int stateDefault; + + QHash<QString, int> users; + int usersDefault; + + Notifications notifications; +}; + +class QUOTIENT_API RoomPowerLevelsEvent + : public KeylessStateEventBase<RoomPowerLevelsEvent, PowerLevelsEventContent> { +public: + QUO_EVENT(RoomPowerLevelsEvent, "m.room.power_levels") + + using KeylessStateEventBase::KeylessStateEventBase; + + int invite() const { return content().invite; } + int kick() const { return content().kick; } + int ban() const { return content().ban; } + + int redact() const { return content().redact; } + + QHash<QString, int> events() const { return content().events; } + int eventsDefault() const { return content().eventsDefault; } + int stateDefault() const { return content().stateDefault; } + + QHash<QString, int> users() const { return content().users; } + int usersDefault() const { return content().usersDefault; } + + int roomNotification() const { return content().notifications.room; } + + int powerLevelForEvent(const QString& eventId) const; + int powerLevelForState(const QString& eventId) const; + int powerLevelForUser(const QString& userId) const; +}; +} // namespace Quotient diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp new file mode 100644 index 00000000..2c3492d6 --- /dev/null +++ b/lib/events/roomtombstoneevent.cpp @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roomtombstoneevent.h" + +using namespace Quotient; + +QString RoomTombstoneEvent::serverMessage() const +{ + return contentPart<QString>("body"_ls); +} + +QString RoomTombstoneEvent::successorRoomId() const +{ + return contentPart<QString>("replacement_room"_ls); +} diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h new file mode 100644 index 00000000..c85b4dfd --- /dev/null +++ b/lib/events/roomtombstoneevent.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "stateevent.h" + +namespace Quotient { +class QUOTIENT_API RoomTombstoneEvent : public StateEvent { +public: + QUO_EVENT(RoomTombstoneEvent, "m.room.tombstone") + + using StateEvent::StateEvent; + + QString serverMessage() const; + QString successorRoomId() const; +}; +} // namespace Quotient diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 56be947c..2a0d3817 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -1,95 +1,47 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "stateevent.h" -#include "eventcontent.h" - -#include "converters.h" - -namespace QMatrixClient +#include "single_key_value.h" + +namespace Quotient { +#define DEFINE_SIMPLE_STATE_EVENT(Name_, TypeId_, ValueType_, ContentKey_) \ + constexpr auto Name_##Key = #ContentKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public KeylessStateEventBase< \ + Name_, EventContent::SingleKeyValue<ValueType_, Name_##Key>> { \ + public: \ + using value_type = ValueType_; \ + QUO_EVENT(Name_, TypeId_) \ + using KeylessStateEventBase::KeylessStateEventBase; \ + auto ContentKey_() const { return content().value; } \ + }; \ +// End of macro + +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) + +constexpr auto RoomAliasesEventKey = "aliases"_ls; +class QUOTIENT_API RoomAliasesEvent + : public KeyedStateEventBase< + RoomAliasesEvent, + EventContent::SingleKeyValue<QStringList, RoomAliasesEventKey>> { - namespace EventContent - { - template <typename T> - class SimpleContent: public Base - { - public: - using value_type = T; - - // The constructor is templated to enable perfect forwarding - template <typename TT> - SimpleContent(QString keyName, TT&& value) - : value(std::forward<TT>(value)), key(std::move(keyName)) - { } - SimpleContent(const QJsonObject& json, QString keyName) - : Base(json) - , value(QMatrixClient::fromJson<T>(json[keyName])) - , key(std::move(keyName)) - { } - - public: - T value; - - protected: - QString key; - - private: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert(key, QMatrixClient::toJson(value)); - } - }; - } // namespace EventContent - -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ContentType>> \ - { \ - public: \ - using content_type = _ContentType; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) \ - : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \ - { } \ - template <typename T> \ - explicit _Name(T&& value) \ - : StateEvent(typeId(), matrixTypeId(), \ - QStringLiteral(#_ContentKey), \ - std::forward<T>(value)) \ - { } \ - auto _ContentKey() const { return content().value; } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ - // End of macro - - DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) - DEFINE_EVENTTYPE_ALIAS(RoomName, RoomNameEvent) - DEFINE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases", - QStringList, aliases) - DEFINE_EVENTTYPE_ALIAS(RoomAliases, RoomAliasesEvent) - DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias", - QString, alias) - DEFINE_EVENTTYPE_ALIAS(RoomCanonicalAlias, RoomCanonicalAliasEvent) - DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) - DEFINE_EVENTTYPE_ALIAS(RoomTopic, RoomTopicEvent) - DEFINE_SIMPLE_STATE_EVENT(EncryptionEvent, "m.room.encryption", - QString, algorithm) - DEFINE_EVENTTYPE_ALIAS(RoomEncryption, EncryptionEvent) -} // namespace QMatrixClient +public: + QUO_EVENT(RoomAliasesEvent, "m.room.aliases") + using KeyedStateEventBase::KeyedStateEventBase; + + Q_DECL_DEPRECATED_X( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") + QString server() const { return stateKey(); } + Q_DECL_DEPRECATED_X( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") + QStringList aliases() const { return content().value; } +}; +} // namespace Quotient diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h new file mode 100644 index 00000000..ca2bd331 --- /dev/null +++ b/lib/events/single_key_value.h @@ -0,0 +1,36 @@ +#pragma once + +#include "converters.h" + +namespace Quotient { + +namespace EventContent { + template <typename T, const QLatin1String& KeyStr> + struct SingleKeyValue { + // NOLINTBEGIN(google-explicit-constructor): that check should learn + // about explicit(false) + QUO_IMPLICIT SingleKeyValue(const T& v = {}) + : value { v } + {} + QUO_IMPLICIT SingleKeyValue(T&& v) + : value { std::move(v) } + {} + // NOLINTEND(google-explicit-constructor) + T value; + }; +} // namespace EventContent + +template <typename ValueT, const QLatin1String& KeyStr> +struct JsonConverter<EventContent::SingleKeyValue<ValueT, KeyStr>> { + using content_type = EventContent::SingleKeyValue<ValueT, KeyStr>; + static content_type load(const QJsonValue& jv) + { + return { fromJson<ValueT>(jv.toObject().value(JsonKey)) }; + } + static QJsonObject dump(const content_type& c) + { + return { { JsonKey, toJson(c.value) } }; + } + static inline const auto JsonKey = toSnakeCase(KeyStr); +}; +} // namespace Quotient diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index fd5d2642..72ecd5ad 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -1,30 +1,40 @@ -/****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "stateevent.h" +#include "logging.h" -using namespace QMatrixClient; +using namespace Quotient; -[[gnu::unused]] static auto stateEventTypeInitialised = - RoomEvent::factory_t::chainFactory<StateEventBase>(); +StateEvent::StateEvent(const QJsonObject& json) + : RoomEvent(json) +{ + Q_ASSERT_X(json.contains(StateKeyKeyL), __FUNCTION__, + "Attempt to create a state event without state key"); +} + +StateEvent::StateEvent(Event::Type type, const QString& stateKey, + const QJsonObject& contentJson) + : RoomEvent(basicJson(type, stateKey, contentJson)) +{} + +bool StateEvent::repeatsState() const +{ + return contentJson() == unsignedPart<QJsonObject>(PrevContentKeyL); +} + +QString StateEvent::replacedState() const +{ + return unsignedPart<QString>("replaces_state"_ls); +} -bool StateEventBase::repeatsState() const +void StateEvent::dumpTo(QDebug dbg) const { - const auto prevContentJson = unsignedJson().value(PrevContentKeyL); - return fullJson().value(ContentKeyL) == prevContentJson; + if (!stateKey().isEmpty()) + dbg << '<' << stateKey() << "> "; + 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 6032132e..992ec2e2 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -1,92 +1,151 @@ -/****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "roomevent.h" -namespace QMatrixClient { - class StateEventBase: public RoomEvent +namespace Quotient { + +class QUOTIENT_API StateEvent : public RoomEvent { +public: + QUO_BASE_EVENT(StateEvent, "json.contains('state_key')"_ls, + RoomEvent::BaseMetaType) + static bool isValid(const QJsonObject& fullJson) { - public: - using factory_t = EventFactory<StateEventBase>; + return fullJson.contains(StateKeyKeyL); + } - using RoomEvent::RoomEvent; - ~StateEventBase() override = default; + //! \brief Static setting of whether a given even type uses state keys + //! + //! Most event types don't use a state key; overriding this to `true` + //! for a given type changes the calls across Quotient to include state key + //! in their signatures; otherwise, state key is still accessible but + //! constructors and calls in, e.g., RoomStateView don't include it. + static constexpr auto needsStateKey = false; - bool isStateEvent() const override { return true; } - virtual bool repeatsState() const; - }; - using StateEventPtr = event_ptr_tt<StateEventBase>; - using StateEvents = EventsArray<StateEventBase>; + explicit StateEvent(Type type, const QString& stateKey = {}, + const QJsonObject& contentJson = {}); - template <typename ContentT> - struct Prev + //! Make a minimal correct Matrix state event JSON + static QJsonObject basicJson(const QString& matrixTypeId, + const QString& stateKey = {}, + const QJsonObject& contentJson = {}) { - template <typename... ContentParamTs> - explicit Prev(const QJsonObject& unsignedJson, - ContentParamTs&&... contentParams) - : senderId(unsignedJson.value("prev_sender"_ls).toString()) - , content(unsignedJson.value(PrevContentKeyL).toObject(), - std::forward<ContentParamTs>(contentParams)...) - { } + return { { TypeKey, matrixTypeId }, + { StateKeyKey, stateKey }, + { ContentKey, contentJson } }; + } + + QString replacedState() const; + virtual bool repeatsState() const; + +protected: + explicit StateEvent(const QJsonObject& json); + void dumpTo(QDebug dbg) const override; +}; +using StateEventBase + [[deprecated("StateEventBase is StateEvent now")]] = StateEvent; +using StateEventPtr = event_ptr_tt<StateEvent>; +using StateEvents = EventsArray<StateEvent>; + +[[deprecated("Use StateEvent::basicJson() instead")]] +inline QJsonObject basicStateEventJson(const QString& matrixTypeId, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return StateEvent::basicJson(matrixTypeId, stateKey, content); +} + +/** + * A combination of event type and state key uniquely identifies a piece + * of state in Matrix. + * \sa + * https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events + */ +using StateEventKey = std::pair<QString, QString>; + +template <typename EventT, typename ContentT> +class EventTemplate<EventT, StateEvent, ContentT> + : public StateEvent { +public: + using content_type = ContentT; + + struct Prev { + explicit Prev() = default; + explicit Prev(const QJsonObject& unsignedJson) + : senderId(fromJson<QString>(unsignedJson["prev_sender"_ls])) + , content( + fromJson<Omittable<ContentT>>(unsignedJson[PrevContentKeyL])) + {} QString senderId; - ContentT content; + Omittable<ContentT> content; }; - template <typename ContentT> - class StateEvent: public StateEventBase + explicit EventTemplate(const QJsonObject& fullJson) + : StateEvent(fullJson) + , _content(fromJson<ContentT>(Event::contentJson())) + , _prev(unsignedJson()) + {} + template <typename... ContentParamTs> + explicit EventTemplate(const QString& stateKey, + ContentParamTs&&... contentParams) + : StateEvent(EventT::TypeId, stateKey) + , _content { std::forward<ContentParamTs>(contentParams)... } { - public: - using content_type = ContentT; - - template <typename... ContentParamTs> - explicit StateEvent(Type type, const QJsonObject& fullJson, - ContentParamTs&&... contentParams) - : StateEventBase(type, fullJson) - , _content(contentJson(), - std::forward<ContentParamTs>(contentParams)...) - { - const auto& unsignedData = unsignedJson(); - if (unsignedData.contains(PrevContentKeyL)) - _prev = std::make_unique<Prev<ContentT>>(unsignedData, - std::forward<ContentParamTs>(contentParams)...); - } - template <typename... ContentParamTs> - explicit StateEvent(Type type, event_mtype_t matrixType, - ContentParamTs&&... contentParams) - : StateEventBase(type, matrixType) - , _content(std::forward<ContentParamTs>(contentParams)...) - { - editJson().insert(ContentKey, _content.toJson()); - } - - const ContentT& content() const { return _content; } - [[deprecated("Use prevContent instead")]] - const ContentT* prev_content() const { return prevContent(); } - const ContentT* prevContent() const - { return _prev ? &_prev->content : nullptr; } - QString prevSenderId() const - { return _prev ? _prev->senderId : QString(); } - - protected: - ContentT _content; - std::unique_ptr<Prev<ContentT>> _prev; - }; -} // namespace QMatrixClient + editJson().insert(ContentKey, toJson(_content)); + } + + const ContentT& content() const { return _content; } + + template <typename VisitorT> + void editContent(VisitorT&& visitor) + { + visitor(_content); + editJson()[ContentKeyL] = toJson(_content); + } + const Omittable<ContentT>& prevContent() const { return _prev.content; } + QString prevSenderId() const { return _prev.senderId; } + +private: + ContentT _content; + Prev _prev; +}; + +template <typename EventT, typename ContentT> +class KeyedStateEventBase + : public EventTemplate<EventT, StateEvent, ContentT> { +public: + static constexpr auto needsStateKey = true; + + using EventTemplate<EventT, StateEvent, ContentT>::EventTemplate; +}; + +template <typename EvT> +concept Keyed_State_Event = EvT::needsStateKey; + +template <typename EventT, typename ContentT> +class KeylessStateEventBase + : public EventTemplate<EventT, StateEvent, ContentT> { +private: + using base_type = EventTemplate<EventT, StateEvent, ContentT>; + +public: + template <typename... ContentParamTs> + explicit KeylessStateEventBase(ContentParamTs&&... contentParams) + : base_type(QString(), std::forward<ContentParamTs>(contentParams)...) + {} + +protected: + explicit KeylessStateEventBase(const QJsonObject& fullJson) + : base_type(fullJson) + {} +}; + +template <typename EvT> +concept Keyless_State_Event = !EvT::needsStateKey; + +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::StateEvent*) +Q_DECLARE_METATYPE(const Quotient::StateEvent*) diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h new file mode 100644 index 00000000..67905481 --- /dev/null +++ b/lib/events/stickerevent.h @@ -0,0 +1,48 @@ +// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" +#include "eventcontent.h" + +namespace Quotient { + +/// Sticker messages are specialised image messages that are displayed without +/// controls (e.g. no "download" link, or light-box view on click, as would be +/// displayed for for m.image events). +class QUOTIENT_API StickerEvent : public RoomEvent +{ +public: + QUO_EVENT(StickerEvent, "m.sticker") + + explicit StickerEvent(const QJsonObject& obj) + : RoomEvent(TypeId, obj) + , m_imageContent( + EventContent::ImageContent(obj["content"_ls].toObject())) + {} + + /// \brief A textual representation or associated description of the + /// sticker image. + /// + /// This could be the alt text of the original image, or a message to + /// accompany and further describe the sticker. + QUO_CONTENT_GETTER(QString, body) + + /// \brief Metadata about the image referred to in url including a + /// thumbnail representation. + const EventContent::ImageContent& image() const + { + return m_imageContent; + } + + /// \brief The URL to the sticker image. This must be a valid mxc:// URI. + QUrl url() const + { + return m_imageContent.url(); + } + +private: + EventContent::ImageContent m_imageContent; +}; +} // namespace Quotient diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp deleted file mode 100644 index 0d39d1be..00000000 --- a/lib/events/typingevent.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "typingevent.h" - -#include <QtCore/QJsonArray> - -using namespace QMatrixClient; - -TypingEvent::TypingEvent(const QJsonObject& obj) - : Event(typeId(), obj) -{ - const auto& array = contentJson()["user_ids"_ls].toArray(); - for(const auto& user: array ) - _users.push_back(user.toString()); -} - diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 27b668b4..b56475af 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -1,39 +1,10 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "event.h" -namespace QMatrixClient -{ - class TypingEvent: public Event - { - public: - DEFINE_EVENT_TYPEID("m.typing", TypingEvent) - - TypingEvent(const QJsonObject& obj); - - const QStringList& users() const { return _users; } - - private: - QStringList _users; - }; - REGISTER_EVENT_TYPE(TypingEvent) - DEFINE_EVENTTYPE_ALIAS(Typing, TypingEvent) -} // namespace QMatrixClient +namespace Quotient { +DEFINE_SIMPLE_EVENT(TypingEvent, Event, "m.typing", QStringList, users, "user_ids") +} // namespace Quotient 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..a10c81fb --- /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 QUOTIENT_API 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; +}; + +QUOTIENT_API QDebug operator<<(QDebug dbg, const EventStats& es); + +} diff --git a/lib/expected.h b/lib/expected.h new file mode 100644 index 00000000..81e186ea --- /dev/null +++ b/lib/expected.h @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <variant> + +namespace Quotient { + +//! \brief A minimal subset of std::expected from C++23 +template <typename T, typename E, + std::enable_if_t<!std::is_same_v<T, E>, bool> = true> +class Expected { +private: + template <typename X> + using enable_if_constructible_t = std::enable_if_t< + std::is_constructible_v<T, X> || std::is_constructible_v<E, X>>; + +public: + using value_type = T; + using error_type = E; + + Expected() = default; + Expected(const Expected&) = default; + Expected(Expected&&) noexcept = default; + ~Expected() = default; + + template <typename X, typename = enable_if_constructible_t<X>> + QUO_IMPLICIT Expected(X&& x) // NOLINT(google-explicit-constructor) + : data(std::forward<X>(x)) + {} + + Expected& operator=(const Expected&) = default; + Expected& operator=(Expected&&) noexcept = default; + + template <typename X, typename = enable_if_constructible_t<X>> + Expected& operator=(X&& x) + { + data = std::forward<X>(x); + return *this; + } + + bool has_value() const { return std::holds_alternative<T>(data); } + explicit operator bool() const { return has_value(); } + + const value_type& value() const& { return std::get<T>(data); } + value_type& value() & { return std::get<T>(data); } + value_type value() && { return std::get<T>(std::move(data)); } + + const value_type& operator*() const& { return value(); } + value_type& operator*() & { return value(); } + + const value_type* operator->() const& { return std::get_if<T>(&data); } + value_type* operator->() & { return std::get_if<T>(&data); } + + template <class U> + T value_or(U&& fallback) const& + { + if (has_value()) + return value(); + return std::forward<U>(fallback); + } + template <class U> + T value_or(U&& fallback) && + { + if (has_value()) + return value(); + return std::forward<U>(fallback); + } + + const E& error() const& { return std::get<E>(data); } + E& error() & { return std::get<E>(data); } + +private: + std::variant<T, E> data; +}; + +} // namespace Quotient diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp new file mode 100644 index 00000000..e3d27122 --- /dev/null +++ b/lib/function_traits.cpp @@ -0,0 +1,56 @@ +// 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; + +template <typename FnT> +using fn_return_t = typename function_traits<FnT>::return_type; + +int f_(); +static_assert(std::is_same_v<fn_return_t<decltype(f_)>, int>, + "Test fn_return_t<>"); + +void f1_(int, float); +static_assert(std::is_same_v<fn_arg_t<decltype(f1_), 1>, float>, + "Test fn_arg_t<>"); + +struct Fo { + int operator()(); + static constexpr auto l = [] { return 0.0f; }; + bool memFn(); + void constMemFn() const&; + double field; + const double field2; +}; +static_assert(std::is_same_v<fn_return_t<Fo>, int>, + "Test return type of function object"); +static_assert(std::is_same_v<fn_return_t<decltype(Fo::l)>, float>, + "Test return type of lambda"); +static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::memFn)>, Fo>, + "Test first argument type of member function"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::memFn)>, bool>, + "Test return type of member function"); +static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::constMemFn)>, const Fo&>, + "Test first argument type of const member function"); +static_assert(std::is_void_v<fn_return_t<decltype(&Fo::constMemFn)>>, + "Test return type of const member function"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field)>, double&>, + "Test return type of a class member"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field2)>, const double&>, + "Test return type of a const class member"); + +struct Fo1 { + void operator()(int); +}; +static_assert(std::is_same_v<fn_arg_t<Fo1>, int>, + "Test fn_arg_t defaulting to first argument"); + +template <typename T> +[[maybe_unused]] static void ft(const std::vector<T>&); +static_assert( + std::is_same<fn_arg_t<decltype(ft<double>)>, const std::vector<double>&>(), + "Test function templates"); diff --git a/lib/function_traits.h b/lib/function_traits.h new file mode 100644 index 00000000..143ed162 --- /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> + 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<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...>; +}; + +namespace _impl { + template <typename> + struct fn_object_traits; + + // Specialisation for a lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for a const lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...) const> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for function objects with (non-overloaded) operator() + // (this includes non-generic lambdas) + template <typename T> + requires requires { &T::operator(); } + struct fn_traits<T> + : public fn_object_traits<decltype(&T::operator())> {}; + + // Specialisation for a member function in a non-functor class + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ClassT, ArgTs...)> {}; + + // Specialisation for a const member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<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<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<ReturnT (ClassT::*)(ArgTs...) &&> + : function_traits<ReturnT(ClassT&&, ArgTs...)> {}; + + // Specialisation for a pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<ReturnT ClassT::*> + : function_traits<ReturnT&(ClassT)> {}; + + // Specialisation for a const pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<const ReturnT ClassT::*> + : function_traits<const ReturnT&(ClassT)> {}; +} // namespace _impl + +template <typename FnT, int ArgN = 0> +using fn_arg_t = + std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; + +template <typename FnT> +constexpr auto fn_arg_count_v = + std::tuple_size_v<typename function_traits<FnT>::arg_types>; + +} // namespace Quotient diff --git a/lib/identity/definitions/request_email_validation.cpp b/lib/identity/definitions/request_email_validation.cpp deleted file mode 100644 index 95088bcb..00000000 --- a/lib/identity/definitions/request_email_validation.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "request_email_validation.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const RequestEmailValidation& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("email"), pod.email); - addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); - addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; -} - -RequestEmailValidation FromJsonObject<RequestEmailValidation>::operator()(const QJsonObject& jo) const -{ - RequestEmailValidation result; - result.clientSecret = - fromJson<QString>(jo.value("client_secret"_ls)); - result.email = - fromJson<QString>(jo.value("email"_ls)); - result.sendAttempt = - fromJson<int>(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson<QString>(jo.value("next_link"_ls)); - - return result; -} - diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h index 3e72275f..87549505 100644 --- a/lib/identity/definitions/request_email_validation.h +++ b/lib/identity/definitions/request_email_validation.h @@ -6,39 +6,50 @@ #include "converters.h" -#include "converters.h" - -namespace QMatrixClient -{ - // Data structures - - struct RequestEmailValidation +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 + /// must not be empty. + QString clientSecret; + + /// The email address to validate. + QString email; + + /// 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 + /// 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 + /// email (e.g. a reminder) to be sent. If they do not, the server + /// should respond with success but not resend the email. + int sendAttempt; + + /// Optional. When the validation is completed, the identity server will + /// redirect the user to this URL. This option is ignored when submitting + /// 3PID validation information through a POST request. + QString nextLink; +}; + +template <> +struct JsonObjectConverter<RequestEmailValidation> { + static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod) { - /// 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 - /// must not be empty. - QString clientSecret; - /// The email address to validate. - QString email; - /// 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 - /// 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 - /// email (e.g. a reminder) to be sent. - int sendAttempt; - /// Optional. When the validation is completed, the identity - /// server will redirect the user to this URL. - QString nextLink; - }; - - QJsonObject toJson(const RequestEmailValidation& pod); - - template <> struct FromJsonObject<RequestEmailValidation> + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("email"), pod.email); + addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); + addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); + } + static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod) { - RequestEmailValidation operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient + fromJson(jo.value("client_secret"_ls), pod.clientSecret); + fromJson(jo.value("email"_ls), pod.email); + fromJson(jo.value("send_attempt"_ls), pod.sendAttempt); + fromJson(jo.value("next_link"_ls), pod.nextLink); + } +}; + +} // namespace Quotient diff --git a/lib/identity/definitions/request_msisdn_validation.cpp b/lib/identity/definitions/request_msisdn_validation.cpp deleted file mode 100644 index 125baa9c..00000000 --- a/lib/identity/definitions/request_msisdn_validation.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "request_msisdn_validation.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const RequestMsisdnValidation& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("country"), pod.country); - addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); - addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); - addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; -} - -RequestMsisdnValidation FromJsonObject<RequestMsisdnValidation>::operator()(const QJsonObject& jo) const -{ - RequestMsisdnValidation result; - result.clientSecret = - fromJson<QString>(jo.value("client_secret"_ls)); - result.country = - fromJson<QString>(jo.value("country"_ls)); - result.phoneNumber = - fromJson<QString>(jo.value("phone_number"_ls)); - result.sendAttempt = - fromJson<int>(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson<QString>(jo.value("next_link"_ls)); - - return result; -} - diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h index 77bea2bc..d2ea463f 100644 --- a/lib/identity/definitions/request_msisdn_validation.h +++ b/lib/identity/definitions/request_msisdn_validation.h @@ -6,42 +6,55 @@ #include "converters.h" -#include "converters.h" - -namespace QMatrixClient -{ - // Data structures - - struct RequestMsisdnValidation +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 + /// 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. + QString country; + + /// The phone number to validate. + QString phoneNumber; + + /// 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` + /// 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 + /// they desire a new SMS (e.g. a reminder) to be sent. + int sendAttempt; + + /// Optional. When the validation is completed, the identity server will + /// redirect the user to this URL. This option is ignored when submitting + /// 3PID validation information through a POST request. + QString nextLink; +}; + +template <> +struct JsonObjectConverter<RequestMsisdnValidation> { + static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod) { - /// 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 - /// must not be empty. - QString clientSecret; - /// The two-letter uppercase ISO country code that the 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 - /// number greater than the most recent one which it has seen, - /// 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 - /// they desire a new SMS (e.g. a reminder) to be sent. - int sendAttempt; - /// Optional. When the validation is completed, the identity - /// server will redirect the user to this URL. - QString nextLink; - }; - - QJsonObject toJson(const RequestMsisdnValidation& pod); - - template <> struct FromJsonObject<RequestMsisdnValidation> + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("country"), pod.country); + addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); + addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); + addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); + } + static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod) { - RequestMsisdnValidation operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient + fromJson(jo.value("client_secret"_ls), pod.clientSecret); + fromJson(jo.value("country"_ls), pod.country); + fromJson(jo.value("phone_number"_ls), pod.phoneNumber); + fromJson(jo.value("send_attempt"_ls), pod.sendAttempt); + fromJson(jo.value("next_link"_ls), pod.nextLink); + } +}; + +} // namespace Quotient diff --git a/lib/identity/definitions/sid.cpp b/lib/identity/definitions/sid.cpp deleted file mode 100644 index 443dbedf..00000000 --- a/lib/identity/definitions/sid.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "sid.h" - -using namespace QMatrixClient; - -QJsonObject QMatrixClient::toJson(const Sid& pod) -{ - QJsonObject jo; - addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; -} - -Sid FromJsonObject<Sid>::operator()(const QJsonObject& jo) const -{ - Sid result; - result.sid = - fromJson<QString>(jo.value("sid"_ls)); - - return result; -} - diff --git a/lib/identity/definitions/sid.h b/lib/identity/definitions/sid.h deleted file mode 100644 index eae60c47..00000000 --- a/lib/identity/definitions/sid.h +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "converters.h" - - -namespace QMatrixClient -{ - // Data structures - - struct Sid - { - /// The session ID. Session IDs are opaque strings generated by the identity - /// server. They must consist entirely of the characters - /// ``[0-9a-zA-Z.=_-]``. Their length must not exceed 255 characters and they - /// must not be empty. - QString sid; - }; - - QJsonObject toJson(const Sid& pod); - - template <> struct FromJsonObject<Sid> - { - Sid operator()(const QJsonObject& jo) const; - }; - -} // namespace QMatrixClient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index b21173ae..da645a2d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -1,134 +1,217 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "basejob.h" #include "connectiondata.h" -#include "util.h" +#include <QtCore/QRegularExpression> +#include <QtCore/QTimer> +#include <QtCore/QMetaEnum> +#include <QtCore/QPointer> #include <QtNetwork/QNetworkAccessManager> -#include <QtNetwork/QNetworkRequest> #include <QtNetwork/QNetworkReply> -#include <QtCore/QTimer> -#include <QtCore/QRegularExpression> -#include <QtCore/QJsonObject> +#include <QtNetwork/QNetworkRequest> -#include <array> +using namespace Quotient; +using std::chrono::seconds, std::chrono::milliseconds; +using namespace std::chrono_literals; + +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 ? IncorrectRequest : NotFound; + switch (httpCode) { + case 401: + return Unauthorised; + // clang-format off + case 403: case 407: // clang-format on + return ContentAccessError; + case 404: + 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 IncorrectRequest; + case 429: + return TooManyRequests; + case 501: + case 510: + return RequestNotImplemented; + case 511: + return NetworkAuthRequired; + default: + return NetworkError; + } +} -using namespace QMatrixClient; +QDebug BaseJob::Status::dumpToLog(QDebug dbg) const +{ + QDebugStateSaver _s(dbg); + dbg.noquote().nospace(); + if (auto* const k = QMetaEnum::fromType<StatusCode>().valueToKey(code)) { + const QByteArray b = k; + dbg << b.mid(b.lastIndexOf(':')); + } else + dbg << code; + return dbg << ": " << message; +} + +class BaseJob::Private { +public: + struct JobTimeoutConfig { + seconds jobTimeout; + seconds nextRetryInterval; + }; + + // Using an idiom from clang-tidy: + // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html + Private(HttpVerb v, QByteArray endpoint, const QUrlQuery& q, + RequestData&& data, bool nt) + : verb(v) + , apiEndpoint(std::move(endpoint)) + , requestQuery(q) + , requestData(std::move(data)) + , needsToken(nt) + { + timer.setSingleShot(true); + retryTimer.setSingleShot(true); + } -struct NetworkReplyDeleter : public QScopedPointerDeleteLater -{ - static inline void cleanup(QNetworkReply* reply) + ~Private() + { + if (reply) { + if (reply->isRunning()) { + reply->abort(); + } + delete reply; + } + } + + void sendRequest(); + /*! \brief Parse the response byte array into JSON + * + * This calls QJsonDocument::fromJson() on rawResponse, converts + * the QJsonParseError result to BaseJob::Status and stores the resulting + * JSON in jsonResponse. + */ + Status parseJson(); + + ConnectionData* connection = nullptr; + + // Contents for the network request + HttpVerb verb; + QByteArray apiEndpoint; + QHash<QByteArray, QByteArray> requestHeaders; + QUrlQuery requestQuery; + RequestData requestData; + bool needsToken; + + bool inBackground = false; + + // There's no use of QMimeType here because we don't want to match + // content types against the known MIME type hierarchy; and at the same + // type QMimeType is of little help with MIME type globs (`text/*` etc.) + QByteArrayList expectedContentTypes { "application/json" }; + + QByteArrayList expectedKeys; + + // When the QNetworkAccessManager is destroyed it destroys all pending replies. + // Using QPointer allows us to know when that happend. + QPointer<QNetworkReply> reply; + + Status status = Unprepared; + QByteArray rawResponse; + /// Contains a null document in case of non-JSON body (for a successful + /// or unsuccessful response); a document with QJsonObject or QJsonArray + /// in case of a successful response with JSON payload, as per the API + /// definition (including an empty JSON object - QJsonObject{}); + /// and QJsonObject in case of an API error. + QJsonDocument jsonResponse; + QUrl errorUrl; //< May contain a URL to help with some errors + + LoggingCategory logCat = JOBS; + + QTimer timer; + QTimer retryTimer; + + static constexpr auto errorStrategy = std::to_array<const JobTimeoutConfig>( + { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } }); + int maxRetries = int(errorStrategy.size()); + int retriesTaken = 0; + + [[nodiscard]] const JobTimeoutConfig& getCurrentTimeoutConfig() const { - if (reply && reply->isRunning()) - reply->abort(); - QScopedPointerDeleteLater::cleanup(reply); + return errorStrategy[std::min(size_t(retriesTaken), + errorStrategy.size() - 1)]; } -}; -class BaseJob::Private -{ - 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) - : verb(v), apiEndpoint(std::move(endpoint)), requestQuery(q) - , requestData(std::move(data)), needsToken(nt) - { } - - void sendRequest(bool inBackground); - const JobTimeoutConfig& getCurrentTimeoutConfig() const; - - const ConnectionData* connection = nullptr; - - // Contents for the network request - HttpVerb verb; - QString apiEndpoint; - QHash<QByteArray, QByteArray> requestHeaders; - QUrlQuery requestQuery; - Data requestData; - bool needsToken; - - // There's no use of QMimeType here because we don't want to match - // content types against the known MIME type hierarchy; and at the same - // type QMimeType is of little help with MIME type globs (`text/*` etc.) - QByteArrayList expectedContentTypes; - - QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply; - Status status = Pending; - QByteArray rawResponse; - QUrl errorUrl; //< May contain a URL to help with some errors - - QTimer timer; - QTimer retryTimer; - - QVector<JobTimeoutConfig> errorStrategy = - { { 90, 5 }, { 90, 10 }, { 120, 30 } }; - int maxRetries = errorStrategy.size(); - int retriesTaken = 0; - - LoggingCategory logCat = JOBS; + [[nodiscard]] QString dumpRequest() const + { + static const std::array verbs { "GET"_ls, "PUT"_ls, "POST"_ls, + "DELETE"_ls }; + const auto verbWord = verbs.at(size_t(verb)); + return verbWord % ' ' + % (reply ? reply->url().toString(QUrl::RemoveQuery) + : makeRequestUrl(connection->baseUrl(), apiEndpoint) + .toString()); + } }; -BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken) - : BaseJob(verb, name, endpoint, Query { }, Data { }, 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)) +inline bool isHex(QChar c) { - setObjectName(name); - setExpectedContentTypes({ "application/json" }); - d->timer.setSingleShot(true); - connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout); + return c.isDigit() || (c >= u'A' && c <= u'F') || (c >= u'a' && c <= u'f'); } -BaseJob::~BaseJob() +QByteArray BaseJob::encodeIfParam(const QString& paramPart) { - stop(); - qCDebug(d->logCat) << this << "destroyed"; + 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); } -QUrl BaseJob::requestUrl() const -{ - return d->reply ? d->reply->request().url() : QUrl(); -} +BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, + bool needsToken) + : BaseJob(verb, name, std::move(endpoint), QUrlQuery {}, RequestData {}, + needsToken) +{} -bool BaseJob::isBackground() const +BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, + const QUrlQuery& query, RequestData&& data, bool needsToken) + : d(makeImpl<Private>(verb, std::move(endpoint), query, std::move(data), + needsToken)) { - return d->reply && d->reply->request().attribute( - QNetworkRequest::BackgroundRequestAttribute).toBool(); + setObjectName(name); + connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); + connect(&d->retryTimer, &QTimer::timeout, this, [this] { + qCDebug(d->logCat) << "Retrying" << this; + d->connection->submit(this); + }); } -const QString& BaseJob::apiEndpoint() const +BaseJob::~BaseJob() { - return d->apiEndpoint; + stop(); + d->retryTimer.stop(); // See #398 + qCDebug(d->logCat) << this << "destroyed"; } -void BaseJob::setApiEndpoint(const QString& apiEndpoint) -{ - d->apiEndpoint = apiEndpoint; -} +QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); } -const BaseJob::headers_t&BaseJob::requestHeaders() const +bool BaseJob::isBackground() const { return d->inBackground; } + +const BaseJob::headers_t& BaseJob::requestHeaders() const { return d->requestHeaders; } @@ -144,22 +227,16 @@ 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) +void BaseJob::setRequestData(RequestData&& data) { std::swap(d->requestData, data); } @@ -179,159 +256,190 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) d->expectedContentTypes = contentTypes; } -QUrl BaseJob::makeRequestUrl(QUrl baseUrl, - const QString& path, const QUrlQuery& query) +QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } + +void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; } + +void BaseJob::setExpectedKeys(const QByteArrayList& keys) { - auto pathBase = baseUrl.path(); - if (!pathBase.endsWith('/') && !path.startsWith('/')) - pathBase.push_back('/'); + d->expectedKeys = keys; +} + +const QNetworkReply* BaseJob::reply() const { return d->reply.data(); } - baseUrl.setPath( pathBase + path ); +QNetworkReply* BaseJob::reply() { return d->reply.data(); } + +QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QByteArray& encodedPath, + const QUrlQuery& query) +{ + // 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; } -void BaseJob::Private::sendRequest(bool inBackground) +void BaseJob::Private::sendRequest() { - QNetworkRequest req - { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) }; + QNetworkRequest req { makeRequestUrl(connection->baseUrl(), apiEndpoint, + requestQuery) }; if (!requestHeaders.contains("Content-Type")) req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - req.setRawHeader("Authorization", - QByteArray("Bearer ") + connection->accessToken()); + if (needsToken) + req.setRawHeader("Authorization", + QByteArray("Bearer ") + connection->accessToken()); req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); req.setMaximumRedirectsAllowed(10); -#endif req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); -#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) - // some sources claim that there are issues with QT 5.8 - req.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); -#endif + // Qt doesn't combine HTTP2 with SSL quite right, occasionally crashing at + // what seems like an attempt to write to a closed channel. If/when that + // changes, false should be turned to true below. + req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false); + Q_ASSERT(req.url().isValid()); for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) req.setRawHeader(it.key(), it.value()); - switch( verb ) - { - case HttpVerb::Get: - reply.reset( connection->nam()->get(req) ); - break; - case HttpVerb::Post: - reply.reset( connection->nam()->post(req, requestData.source()) ); - break; - case HttpVerb::Put: - reply.reset( connection->nam()->put(req, requestData.source()) ); - break; - case HttpVerb::Delete: - reply.reset( connection->nam()->deleteResource(req) ); - break; + + switch (verb) { + case HttpVerb::Get: + reply = connection->nam()->get(req); + break; + case HttpVerb::Post: + reply = connection->nam()->post(req, requestData.source()); + break; + case HttpVerb::Put: + reply = connection->nam()->put(req, requestData.source()); + break; + case HttpVerb::Delete: + reply = connection->nam()->sendCustomRequest(req, "DELETE", requestData.source()); + break; } } -void BaseJob::beforeStart(const ConnectionData*) -{ } +void BaseJob::doPrepare() { } -void BaseJob::afterStart(const ConnectionData*, QNetworkReply*) -{ } +void BaseJob::onSentRequest(QNetworkReply*) { } -void BaseJob::beforeAbandon(QNetworkReply*) -{ } +void BaseJob::beforeAbandon() { } -void BaseJob::start(const ConnectionData* connData, bool inBackground) +void BaseJob::initiate(ConnectionData* connData, bool inBackground) { - d->connection = connData; - d->retryTimer.setSingleShot(true); - connect (&d->retryTimer, &QTimer::timeout, - this, [this,inBackground] { sendRequest(inBackground); }); + if (Q_LIKELY(connData && connData->baseUrl().isValid())) { + d->inBackground = inBackground; + d->connection = connData; + doPrepare(); - beforeStart(connData); - if (status().good()) - sendRequest(inBackground); - if (status().good()) - afterStart(connData, d->reply.data()); - if (!status().good()) - QTimer::singleShot(0, this, &BaseJob::finishJob); + if (d->needsToken && d->connection->accessToken().isEmpty()) + setStatus(Unauthorised); + else if ((d->verb == HttpVerb::Post || d->verb == HttpVerb::Put) + && d->requestData.source() + && !d->requestData.source()->isReadable()) { + setStatus(FileError, "Request data not ready"); + } + Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this + if (Q_LIKELY(status().code == Unprepared)) { + d->connection->submit(this); + return; + } + qCWarning(d->logCat).noquote() + << "Request failed preparation and won't be sent:" + << d->dumpRequest(); + } else { + qCCritical(d->logCat) + << "Developers, ensure the Connection is valid before using it"; + Q_ASSERT(false); + setStatus(IncorrectRequest, tr("Invalid server connection")); + } + // The status is no good, finalise + QTimer::singleShot(0, this, &BaseJob::finishJob); } -void BaseJob::sendRequest(bool inBackground) +void BaseJob::sendRequest() { - emit aboutToStart(); - d->retryTimer.stop(); // In case we were counting down at the moment - qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint; - if (!d->requestQuery.isEmpty()) - qCDebug(d->logCat) << " query:" << d->requestQuery.toString(); - d->sendRequest(inBackground); - connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply ); - if (d->reply->isRunning()) - { - connect( d->reply.data(), &QNetworkReply::metaDataChanged, - this, &BaseJob::checkReply); - connect( d->reply.data(), &QNetworkReply::uploadProgress, - this, &BaseJob::uploadProgress); - connect( d->reply.data(), &QNetworkReply::downloadProgress, - this, &BaseJob::downloadProgress); - d->timer.start(getCurrentTimeout()); - qCDebug(d->logCat) << this << "request has been sent"; - emit started(); + if (status().code == Abandoned) { + qCDebug(d->logCat) << "Won't proceed with the abandoned request:" + << d->dumpRequest(); + return; } - else - qCWarning(d->logCat) << this << "request could not start"; + Q_ASSERT(d->connection && status().code == Pending); + qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest(); + d->needsToken |= d->connection->needsToken(objectName()); + emit aboutToSendRequest(); + d->sendRequest(); + Q_ASSERT(d->reply); + connect(reply(), &QNetworkReply::finished, this, [this] { + gotReply(); + finishJob(); + }); + if (d->reply->isRunning()) { + connect(reply(), &QNetworkReply::metaDataChanged, this, + [this] { checkReply(reply()); }); + connect(reply(), &QNetworkReply::uploadProgress, this, + &BaseJob::uploadProgress); + connect(reply(), &QNetworkReply::downloadProgress, this, + &BaseJob::downloadProgress); + d->timer.start(getCurrentTimeout()); + qCInfo(d->logCat).noquote() << "Sent" << d->dumpRequest(); + onSentRequest(reply()); + emit sentRequest(); + } else + qCCritical(d->logCat).noquote() + << "Request could not start:" << d->dumpRequest(); } -void BaseJob::checkReply() +BaseJob::Status BaseJob::Private::parseJson() { - setStatus(doCheckReply(d->reply.data())); + QJsonParseError error { 0, QJsonParseError::MissingObject }; + jsonResponse = QJsonDocument::fromJson(rawResponse, &error); + return { error.error == QJsonParseError::NoError ? NoError + : IncorrectResponse, + error.errorString() }; } void BaseJob::gotReply() { - checkReply(); - if (status().good()) - setStatus(parseReply(d->reply.data())); - else { - // FIXME: Factor out to smth like BaseJob::handleError() - d->rawResponse = d->reply->readAll(); - const auto jsonBody = - d->reply->rawHeader("Content-Type") == "application/json"; - qCDebug(d->logCat).noquote() - << "Error body (truncated if long):" << d->rawResponse.left(500); - if (jsonBody) - { - auto json = QJsonDocument::fromJson(d->rawResponse).object(); - const auto errCode = json.value("errcode"_ls).toString(); - if (error() == TooManyRequestsError || - errCode == "M_LIMIT_EXCEEDED") - { - QString msg = tr("Too many requests"); - auto retryInterval = json.value("retry_after_ms"_ls).toInt(-1); - if (retryInterval != -1) - msg += tr(", next retry advised after %1 ms") - .arg(retryInterval); - else // We still have to figure some reasonable interval - retryInterval = getNextRetryInterval(); - - setStatus(TooManyRequestsError, msg); - - // Shortcut to retry instead of executing finishJob() - stop(); - qCWarning(d->logCat) - << this << "will retry in" << retryInterval << "ms"; - d->retryTimer.start(retryInterval); - emit retryScheduled(d->retriesTaken, retryInterval); - return; - } - if (errCode == "M_CONSENT_NOT_GIVEN") - { - d->status.code = UserConsentRequiredError; - d->errorUrl = json.value("consent_uri"_ls).toString(); - } - else if (!json.isEmpty()) // Not localisable on the client side - setStatus(IncorrectRequestError, - json.value("error"_ls).toString()); + // 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(); + 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()) + statusSoFar = { IncorrectResponse, + tr("Required JSON keys missing: ") + + missingKeys.join() }; } + setStatus(statusSoFar); + if (!status().good()) // Bad JSON in a "good" reply: bail out + return; + } + // 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 (statusSoFar.good()) { + setStatus(prepareResult()); + return; } - finishJob(); + 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) @@ -342,138 +450,183 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) // ignore possible appendixes of the content type const auto ctype = type.split(';').front(); - for (const auto& pattern: patterns) - { + for (const auto& pattern: patterns) { if (pattern.startsWith('*') || ctype == pattern) // Fast lane return true; auto patternParts = pattern.split('/'); Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__, - "BaseJob: Expected content type should have up to two" - " /-separated parts; violating pattern: " + pattern); + "BaseJob: Expected content type should have up to two" + " /-separated parts; violating pattern: " + + pattern); - if (ctype.split('/').front() == patternParts.front() && - patternParts.back() == "*") + if (ctype.split('/').front() == patternParts.front() + && patternParts.back() == "*") return true; // Exact match already went on fast lane } return false; } -BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const +BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const { - // QNetworkReply error codes seem to be flawed when it comes to HTTP; - // see, e.g., https://github.com/QMatrixClient/libqmatrixclient/issues/200 - // so check genuine HTTP codes. The below processing is based on - // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + // QNetworkReply error codes are insufficient for our purposes (e.g. they + // don't allow to discern HTTP code 429) so check the original code instead const auto httpCodeHeader = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - if (!httpCodeHeader.isValid()) - { - qCWarning(d->logCat) << this << "didn't get valid HTTP headers"; + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + if (!httpCodeHeader.isValid()) { + qCWarning(d->logCat).noquote() + << "No valid HTTP headers from" << d->dumpRequest(); return { NetworkError, reply->errorString() }; } - const QString replyState = reply->isRunning() ? - QStringLiteral("(tentative)") : QStringLiteral("(final)"); - const auto urlString = '|' + d->reply->url().toDisplayString(); const auto httpCode = httpCodeHeader.toInt(); - const auto reason = - reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); if (httpCode / 100 == 2) // 2xx { - qCDebug(d->logCat).noquote().nospace() << this << urlString; - qCDebug(d->logCat).noquote() << " " << httpCode << reason << replyState; + if (reply->isFinished()) + qCInfo(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest(); if (!checkContentType(reply->rawHeader("Content-Type"), - d->expectedContentTypes)) + d->expectedContentTypes)) return { UnexpectedResponseTypeWarning, "Unexpected content type of the response" }; return NoError; } + if (reply->isFinished()) + qCWarning(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest(); - qCWarning(d->logCat).noquote().nospace() << this << urlString; - qCWarning(d->logCat).noquote() << " " << httpCode << reason << replyState; - return { [httpCode]() -> StatusCode { - if (httpCode / 10 == 41) - return httpCode == 410 ? IncorrectRequestError : NotFoundError; - switch (httpCode) - { - case 401: case 403: case 407: - return ContentAccessError; - case 404: - return NotFoundError; - case 400: case 405: case 406: case 426: case 428: - case 505: - case 494: // Unofficial nginx "Request header too large" - case 497: // Unofficial nginx "HTTP request sent to HTTPS port" - return IncorrectRequestError; - case 429: - return TooManyRequestsError; - case 501: case 510: - return RequestNotImplementedError; - case 511: - return NetworkAuthRequiredError; - default: - return NetworkError; - } - }(), reply->errorString() }; + auto message = reply->errorString(); + if (message.isEmpty()) + message = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute) + .toString(); + + return Status::fromHttpCode(httpCode, message); } -BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) +BaseJob::Status BaseJob::prepareResult() { return Success; } + +BaseJob::Status BaseJob::prepareError(Status currentStatus) { - d->rawResponse = reply->readAll(); - QJsonParseError error; - const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); - if( error.error == QJsonParseError::NoError ) - return parseJson(json); - else - return { IncorrectResponseError, error.errorString() }; + // 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) + if (!d->rawResponse.isEmpty() + && reply()->rawHeader("Content-Type") == "application/json") + d->parseJson(); + + // 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 retaining the current status) + const auto& errorJson = jsonData(); + const auto errCode = errorJson.value("errcode"_ls).toString(); + 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) + msg += tr(", next retry advised after %1 ms").arg(retryAfterMs); + else // We still have to figure some reasonable interval + retryAfterMs = getNextRetryMs(); + + d->connection->limitRate(milliseconds(retryAfterMs)); + + return { TooManyRequests, msg }; + } + + if (errCode == "M_CONSENT_NOT_GIVEN") { + d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString()); + return { UserConsentRequired }; + } + if (errCode == "M_UNSUPPORTED_ROOM_VERSION" + || errCode == "M_INCOMPATIBLE_ROOM_VERSION") + return { UnsupportedRoomVersion, + errorJson.contains("room_version"_ls) + ? tr("Requested room version: %1") + .arg(errorJson.value("room_version"_ls).toString()) + : errorJson.value("error"_ls).toString() }; + if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") + return { CannotLeaveRoom, + tr("It's not allowed to leave a server notices room") }; + if (errCode == "M_USER_DEACTIVATED") + return { UserDeactivated }; + + // Not localisable on the client side + if (errorJson.contains("error"_ls)) // Keep the code, update the message + return { currentStatus.code, errorJson.value("error"_ls).toString() }; + + return currentStatus; // The error payload is not recognised } -BaseJob::Status BaseJob::parseJson(const QJsonDocument&) +QJsonValue BaseJob::takeValueFromJson(const QString& key) { - return Success; + if (!d->jsonResponse.isObject()) + return QJsonValue::Undefined; + auto o = d->jsonResponse.object(); + auto v = o.take(key); + d->jsonResponse.setObject(o); + return v; } void BaseJob::stop() { + // This method is (also) used to semi-finalise the job before retrying; so + // stop the timeout timer but keep the retry timer running. d->timer.stop(); - if (d->reply) - { + if (d->reply) { d->reply->disconnect(this); // Ignore whatever comes from the reply - if (d->reply->isRunning()) - { - qCWarning(d->logCat) << this << "stopped without ready network reply"; - d->reply->abort(); + if (d->reply->isRunning()) { + qCWarning(d->logCat) + << this << "stopped without ready network reply"; + d->reply->abort(); // Keep the reply object in case clients need it } - } - else + } else qCWarning(d->logCat) << this << "stopped with empty network reply"; } void BaseJob::finishJob() { stop(); - if ((error() == NetworkError || error() == TimeoutError) - && d->retriesTaken < d->maxRetries) - { - // TODO: The whole retrying thing should be put to ConnectionManager - // otherwise independently retrying jobs make a bit of notification - // storm towards the UI. - const auto retryInterval = - error() == TimeoutError ? 0 : getNextRetryInterval(); - ++d->retriesTaken; - qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken - << " in " << retryInterval/1000 << " s"; - d->retryTimer.start(retryInterval); - emit retryScheduled(d->retriesTaken, retryInterval); + switch(error()) { + case TooManyRequests: + emit rateLimited(); + d->connection->submit(this); return; + case Unauthorised: + if (!d->needsToken && !d->connection->accessToken().isEmpty()) { + // Rerun with access token (extension of the spec while + // https://github.com/matrix-org/matrix-doc/issues/701 is pending) + d->connection->setNeedsToken(objectName()); + qCWarning(d->logCat) << this << "re-running with authentication"; + emit retryScheduled(d->retriesTaken, 0); + d->connection->submit(this); + return; + } + break; + case NetworkError: + case IncorrectResponse: + case Timeout: + if (d->retriesTaken < d->maxRetries) { + // TODO: The whole retrying thing should be put to + // Connection(Manager) otherwise independently retrying jobs make a + // bit of notification storm towards the UI. + const seconds retryIn = error() == Timeout ? 0s + : getNextRetryInterval(); + ++d->retriesTaken; + qCWarning(d->logCat).nospace() + << this << ": retry #" << d->retriesTaken << " in " + << retryIn.count() << " s"; + setStatus(Pending, "Pending retry"); + d->retryTimer.start(retryIn); + emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count()); + return; + } + [[fallthrough]]; + default:; } - // Notify those interested in any completion of the job (including killing) + Q_ASSERT(status().code != Pending); + + // Notify those interested in any completion of the job including abandon() emit finished(this); - emit result(this); + emit result(this); // abandon() doesn't emit this if (error()) emit failure(this); else @@ -482,107 +635,147 @@ void BaseJob::finishJob() deleteLater(); } -const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const +seconds BaseJob::getCurrentTimeout() const { - return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)]; + return d->getCurrentTimeoutConfig().jobTimeout; } -BaseJob::duration_t BaseJob::getCurrentTimeout() const +BaseJob::duration_ms_t BaseJob::getCurrentTimeoutMs() const { - return d->getCurrentTimeoutConfig().jobTimeout * 1000; + return milliseconds(getCurrentTimeout()).count(); } -BaseJob::duration_t BaseJob::getNextRetryInterval() const +seconds BaseJob::getNextRetryInterval() const { - return d->getCurrentTimeoutConfig().nextRetryInterval * 1000; + return d->getCurrentTimeoutConfig().nextRetryInterval; } -BaseJob::duration_t BaseJob::millisToRetry() const +BaseJob::duration_ms_t BaseJob::getNextRetryMs() const { - return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0; + return milliseconds(getNextRetryInterval()).count(); } -int BaseJob::maxRetries() const +milliseconds BaseJob::timeToRetry() const { - return d->maxRetries; + return d->retryTimer.isActive() ? d->retryTimer.remainingTimeAsDuration() + : 0s; } -void BaseJob::setMaxRetries(int newMaxRetries) +BaseJob::duration_ms_t BaseJob::millisToRetry() const { - d->maxRetries = newMaxRetries; + return timeToRetry().count(); } -BaseJob::Status BaseJob::status() const +int BaseJob::maxRetries() const { return d->maxRetries; } + +void BaseJob::setMaxRetries(int newMaxRetries) { - return d->status; + d->maxRetries = newMaxRetries; } +BaseJob::Status BaseJob::status() const { return d->status; } + QByteArray BaseJob::rawData(int bytesAtMost) const { - return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ? - d->rawResponse.left(bytesAtMost) + "...(truncated)" : d->rawResponse; + return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost + ? d->rawResponse.left(bytesAtMost) + : d->rawResponse; } -QString BaseJob::statusCaption() const +const QByteArray& BaseJob::rawData() const { return d->rawResponse; } + +QString BaseJob::rawDataSample(int bytesAtMost) const { - switch (d->status.code) - { - case Success: - return tr("Success"); - case Pending: - return tr("Request still pending response"); - case UnexpectedResponseTypeWarning: - return tr("Warning: Unexpected response type"); - case Abandoned: - return tr("Request was abandoned"); - case NetworkError: - return tr("Network problems"); - case JsonParseError: - return tr("Response could not be parsed"); - case TimeoutError: - return tr("Request timed out"); - case ContentAccessError: - return tr("Access error"); - case NotFoundError: - return tr("Not found"); - case IncorrectRequestError: - return tr("Invalid request"); - case IncorrectResponseError: - return tr("Response could not be parsed"); - case TooManyRequestsError: - return tr("Too many requests"); - case RequestNotImplementedError: - return tr("Function not implemented by the server"); - case NetworkAuthRequiredError: - return tr("Network authentication required"); - case UserConsentRequiredError: - return tr("User consent required"); - default: - return tr("Request failed"); - } + auto data = rawData(bytesAtMost); + Q_ASSERT(data.size() <= d->rawResponse.size()); + return data.size() == d->rawResponse.size() + ? data + : data + tr("...(truncated, %Ln bytes in total)", + "Comes after trimmed raw network response", + d->rawResponse.size()); } -int BaseJob::error() const +QJsonObject BaseJob::jsonData() const { - return d->status.code; + return d->jsonResponse.object(); } -QString BaseJob::errorString() const +QJsonArray BaseJob::jsonItems() const { - return d->status.message; + return d->jsonResponse.array(); } -QUrl BaseJob::errorUrl() const +QString BaseJob::statusCaption() const { - return d->errorUrl; + switch (d->status.code) { + case Success: + return tr("Success"); + case Pending: + return tr("Request still pending response"); + case UnexpectedResponseTypeWarning: + return tr("Warning: Unexpected response type"); + case Abandoned: + return tr("Request was abandoned"); + case NetworkError: + return tr("Network problems"); + case Timeout: + return tr("Request timed out"); + case Unauthorised: + return tr("Unauthorised request"); + case ContentAccessError: + return tr("Access error"); + case NotFound: + return tr("Not found"); + case IncorrectRequest: + return tr("Invalid request"); + case IncorrectResponse: + return tr("Response could not be parsed"); + case TooManyRequests: + return tr("Too many requests"); + case RequestNotImplemented: + return tr("Function not implemented by the server"); + case NetworkAuthRequired: + return tr("Network authentication required"); + case UserConsentRequired: + return tr("User consent required"); + case UnsupportedRoomVersion: + return tr("The server does not support the needed room version"); + default: + return tr("Request failed"); + } } +int BaseJob::error() const { + return d->status.code; } + +QString BaseJob::errorString() const { + return d->status.message; } + +QUrl BaseJob::errorUrl() const { + return d->errorUrl; } + void BaseJob::setStatus(Status s) { + // The crash that led to this code has been reported in + // https://github.com/quotient-im/Quaternion/issues/566 - basically, + // when cleaning up children of a deleted Connection, there's a chance + // of pending jobs being abandoned, calling setStatus(Abandoned). + // There's nothing wrong with this; however, the safety check for + // cleartext access tokens below uses d->connection - which is a dangling + // pointer. + // To alleviate that, a stricter condition is applied, that for Abandoned + // and to-be-Abandoned jobs the status message will be disregarded entirely. + // We could rectify the situation by making d->connection a QPointer<> + // (and deriving ConnectionData from QObject, respectively) but it's + // a too edge case for the hassle. if (d->status == s) return; - if (!d->connection->accessToken().isEmpty()) + if (d->status.code == Abandoned || s.code == Abandoned) + s.message.clear(); + + if (!s.message.isEmpty() && d->connection + && !d->connection->accessToken().isEmpty()) s.message.replace(d->connection->accessToken(), "(REDACTED)"); if (!s.good()) qCWarning(d->logCat) << this << "status" << s; @@ -597,9 +790,10 @@ void BaseJob::setStatus(int code, QString message) void BaseJob::abandon() { - beforeAbandon(d->reply.data()); + beforeAbandon(); + d->timer.stop(); + d->retryTimer.stop(); // In case abandon() was called between retries setStatus(Abandoned); - this->disconnect(); if (d->reply) d->reply->disconnect(this); emit finished(this); @@ -609,11 +803,8 @@ void BaseJob::abandon() void BaseJob::timeout() { - setStatus( TimeoutError, "The job has timed out" ); + setStatus(Timeout, "The job has timed out"); finishJob(); } -void BaseJob::setLoggingCategory(LoggingCategory lcf) -{ - d->logCat = lcf; -} +void BaseJob::setLoggingCategory(LoggingCategory lcf) { d->logCat = lcf; } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 4ef25ab8..555c602b 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -1,339 +1,477 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "../logging.h" #include "requestdata.h" +#include "logging.h" +#include "converters.h" // Common for csapi/ headers even though not used here +#include "quotient_common.h" // For DECL_DEPRECATED_ENUMERATOR #include <QtCore/QObject> -#include <QtCore/QUrlQuery> -#include <QtCore/QJsonDocument> +#include <QtCore/QStringBuilder> class QNetworkReply; class QSslError; -namespace QMatrixClient -{ - class ConnectionData; +namespace Quotient { +class ConnectionData; + +enum class HttpVerb { Get, Put, Post, Delete }; - enum class HttpVerb { Get, Put, Post, Delete }; +class QUOTIENT_API BaseJob : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) + Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) + Q_PROPERTY(int statusCode READ error NOTIFY statusChanged) - struct JobTimeoutConfig + static QByteArray encodeIfParam(const QString& paramPart); + template <int N> + static auto encodeIfParam(const char (&constPart)[N]) { - int jobTimeout; - int nextRetryInterval; + 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() + * from Connection (if things are fine) it go to Pending status. After + * that, the next transition comes after the reply arrives and its contents + * are analysed. At any point in time the job can be abandon()ed, causing + * it to switch to status Abandoned for a brief period before deletion. + */ + enum StatusCode { + Success = 0, + NoError = Success, + Pending = 1, + WarningLevel = 20, //< Warnings have codes starting from this + UnexpectedResponseType = 21, + UnexpectedResponseTypeWarning = UnexpectedResponseType, + Unprepared = 25, //< Initial job state is incomplete, hence warning level + Abandoned = 50, //< A tiny period between abandoning and object deletion + ErrorLevel = 100, //< Errors have codes starting from this + NetworkError = 101, + WITH_DEPRECATED_ERROR_VERSION(Timeout), + Unauthorised, + ContentAccessError, + WITH_DEPRECATED_ERROR_VERSION(NotFound), + WITH_DEPRECATED_ERROR_VERSION(IncorrectRequest), + WITH_DEPRECATED_ERROR_VERSION(IncorrectResponse), + WITH_DEPRECATED_ERROR_VERSION(TooManyRequests), + RateLimited = TooManyRequests, + WITH_DEPRECATED_ERROR_VERSION(RequestNotImplemented), + WITH_DEPRECATED_ERROR_VERSION(UnsupportedRoomVersion), + WITH_DEPRECATED_ERROR_VERSION(NetworkAuthRequired), + WITH_DEPRECATED_ERROR_VERSION(UserConsentRequired), + CannotLeaveRoom, + UserDeactivated, + FileError, + UserDefinedError = 256 }; + Q_ENUM(StatusCode) + +#undef WITH_DEPRECATED_ERROR_VERSION - class BaseJob: public QObject + template <typename... StrTs> + static QByteArray makePath(StrTs&&... parts) { - Q_OBJECT - Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) - Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) - public: - /* Just in case, the values are compatible with KJob - * (which BaseJob used to inherit from). */ - enum StatusCode { NoError = 0 // To be compatible with Qt conventions - , Success = 0 - , Pending = 1 - , WarningLevel = 20 - , UnexpectedResponseTypeWarning = 21 - , Abandoned = 50 //< A very brief period between abandoning and object deletion - , ErrorLevel = 100 //< Errors have codes starting from this - , NetworkError = 100 - , JsonParseError // TODO: Merge into IncorrectResponseError - , TimeoutError - , ContentAccessError - , NotFoundError - , IncorrectRequestError - , IncorrectResponseError - , TooManyRequestsError - , RequestNotImplementedError - , NetworkAuthRequiredError - , UserConsentRequiredError - , UserDefinedError = 200 - }; - - /** - * 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); - } - }; - - using Data = RequestData; - - /** - * This structure stores the status of a server call job. The status consists - * of a code, that is described (but not delimited) by the respective enum, - * and a freeform message. - * - * To extend the list of error codes, define an (anonymous) enum - * along the lines of StatusCode, with additional values - * starting at UserDefinedError - */ - class Status - { - public: - Status(StatusCode c) : code(c) { } - Status(int c, QString m) : code(c), message(std::move(m)) { } - - bool good() const { return code < ErrorLevel; } - friend QDebug operator<<(QDebug dbg, const Status& s) - { - QDebugStateSaver _s(dbg); - return dbg.noquote().nospace() - << s.code << ": " << s.message; - } - - bool operator==(const Status& other) const - { - return code == other.code && message == other.message; - } - bool operator!=(const Status& other) const - { - return !operator==(other); - } - - int code; - QString message; - }; - - using duration_t = int; // milliseconds - - public: - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - 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; - - /** Current status of the job */ - Status status() const; - /** Short human-friendly message on the job status */ - QString statusCaption() const; - /** Raw response body as received from the server */ - QByteArray rawData(int bytesAtMost = -1) const; - - /** Error (more generally, status) code - * Equivalent to status().code - * \sa status - */ - int error() const; - /** Error-specific message, as returned by the server */ - virtual QString errorString() const; - /** A URL to help/clarify the error, if provided by the server */ - QUrl errorUrl() const; - - int maxRetries() const; - void setMaxRetries(int newMaxRetries); - - Q_INVOKABLE duration_t getCurrentTimeout() const; - Q_INVOKABLE duration_t getNextRetryInterval() const; - Q_INVOKABLE duration_t millisToRetry() const; - - friend QDebug operator<<(QDebug dbg, const BaseJob* j) - { - return dbg << j->objectName(); - } - - public slots: - void start(const ConnectionData* connData, - bool inBackground = false); - - /** - * Abandons the result of this job, arrived or unarrived. - * - * This aborts waiting for a reply from the server (if there was - * any pending) and deletes the job object. No result signals - * (result, success, failure) are emitted. - */ - void abandon(); - - signals: - /** The job is about to send a network request */ - void aboutToStart(); - - /** The job has sent a network request */ - void started(); - - /** The job has changed its status */ - void statusChanged(Status newStatus); - - /** - * The previous network request has failed; the next attempt will - * be done in the specified time - * @param nextAttempt the 1-based number of attempt (will always be more than 1) - * @param inMilliseconds the interval after which the next attempt will be taken - */ - void retryScheduled(int nextAttempt, int inMilliseconds); - - /** - * Emitted when the job is finished, in any case. It is used to notify - * observers that the job is terminated and that progress can be hidden. - * - * This should not be emitted directly by subclasses; - * use finishJob() instead. - * - * In general, to be notified of a job's completion, client code - * should connect to result(), success(), or failure() - * rather than finished(). However if you store a list of jobs - * and need to track their lifecycle, then you should connect to this - * instead of result(), to avoid dangling pointers in your list. - * - * @param job the job that emitted this signal - * - * @see result, success, failure - */ - void finished(BaseJob* job); - - /** - * Emitted when the job is finished (except when abandoned). - * - * Use error() to know if the job was finished with error. - * - * @param job the job that emitted this signal - * - * @see success, failure - */ - void result(BaseJob* job); - - /** - * Emitted together with result() in case there's no error. - * - * @see result, failure - */ - void success(BaseJob*); - - /** - * Emitted together with result() if there's an error. - * Similar to result(), this won't be emitted in case of abandon(). - * - * @see result, success - */ - void failure(BaseJob*); - - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); - void uploadProgress(qint64 bytesSent, qint64 bytesTotal); - - protected: - using headers_t = QHash<QByteArray, QByteArray>; - - const QString& apiEndpoint() const; - 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; - void setRequestQuery(const QUrlQuery& query); - const Data& requestData() const; - void setRequestData(Data&& data); - const QByteArrayList& expectedContentTypes() const; - void addExpectedContentType(const QByteArray& contentType); - void setExpectedContentTypes(const QByteArrayList& contentTypes); - - /** Construct a URL out of baseUrl, path and query - * The function automatically adds '/' between baseUrl's path and - * \p path if necessary. The query component of \p baseUrl - * is ignored. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& path, - const QUrlQuery& query = {}); - - virtual void beforeStart(const ConnectionData* connData); - virtual void afterStart(const ConnectionData* connData, - QNetworkReply* reply); - virtual void beforeAbandon(QNetworkReply*); - - /** - * Used by gotReply() to check the received reply for general - * issues such as network errors or access denial. - * Returning anything except NoError/Success prevents - * further parseReply()/parseJson() invocation. - * - * @param reply the reply received from the server - * @return the result of checking the reply - * - * @see gotReply - */ - virtual Status doCheckReply(QNetworkReply* reply) const; - - /** - * Processes the reply. By default, parses the reply into - * a QJsonDocument and calls parseJson() if it's a valid JSON. - * - * @param reply raw contents of a HTTP reply from the server (without headers) - * - * @see gotReply, parseJson - */ - virtual Status parseReply(QNetworkReply* reply); - - /** - * Processes the JSON document received from the Matrix server. - * By default returns succesful status without analysing the JSON. - * - * @param json valid JSON document received from the server - * - * @see parseReply - */ - virtual Status parseJson(const QJsonDocument&); - - void setStatus(Status s); - void setStatus(int code, QString message); - - // Q_DECLARE_LOGGING_CATEGORY return different function types - // in different versions - using LoggingCategory = decltype(JOBS)*; - void setLoggingCategory(LoggingCategory lcf); - - // Job objects should only be deleted via QObject::deleteLater - ~BaseJob() override; - - protected slots: - void timeout(); - - private slots: - void sendRequest(bool inBackground); - void checkReply(); - void gotReply(); - - private: - void stop(); - void finishJob(); - - class Private; - QScopedPointer<Private> d; + 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 + * consists of a code, that is described (but not delimited) by the + * respective enum, and a freeform message. + * + * To extend the list of error codes, define an (anonymous) enum + * along the lines of StatusCode, with additional values + * starting at UserDefinedError + */ + struct Status { + Status(StatusCode c) : code(c) {} + Status(int c, QString m) : code(c), message(std::move(m)) {} + + static StatusCode fromHttpCode(int httpCode); + static Status fromHttpCode(int httpCode, QString msg) + { + return { fromHttpCode(httpCode), std::move(msg) }; + } + + bool good() const { return code < ErrorLevel; } + QDebug dumpToLog(QDebug dbg) const; + friend QDebug operator<<(const QDebug& dbg, const Status& s) + { + return s.dumpToLog(dbg); + } + + bool operator==(const Status& other) const + { + return code == other.code && message == other.message; + } + bool operator!=(const Status& other) const + { + return !operator==(other); + } + bool operator==(int otherCode) const + { + return code == otherCode; + } + bool operator!=(int otherCode) const + { + return !operator==(otherCode); + } + + int code; + QString message; }; - inline bool isJobRunning(BaseJob* job) +public: + 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); + + QUrl requestUrl() const; + bool isBackground() const; + + /** Current status of the job */ + Status status() const; + + /** Short human-friendly message on the job status */ + QString statusCaption() const; + + /*! Get first bytes of the raw response body as received from the server + * + * \param bytesAtMost the number of leftmost bytes to return + * + * \sa rawDataSample + */ + QByteArray rawData(int bytesAtMost) const; + + /*! Access the whole response body as received from the server */ + const QByteArray& rawData() const; + + /** Get UI-friendly sample of raw data + * + * This is almost the same as rawData but appends the "truncated" + * suffix if not all data fit in bytesAtMost. This call is + * recommended to present a sample of raw data as "details" next to + * error messages. Note that the default \p bytesAtMost value is + * also tailored to UI cases. + * + * \sa rawData + */ + QString rawDataSample(int bytesAtMost = 65535) const; + + /** Get the response body as a JSON object + * + * If the job's returned content type is not `application/json` + * or if the top-level JSON entity is not an object, an empty object + * is returned. + */ + QJsonObject jsonData() const; + + /** Get the response body as a JSON array + * + * If the job's returned content type is not `application/json` + * or if the top-level JSON entity is not an array, an empty array + * is returned. + */ + QJsonArray jsonItems() const; + + /** Load the property from the JSON response assuming a given C++ type + * + * 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> + T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const + { + const auto& jv = jsonData().value(keyName); + return jv.isUndefined() ? std::forward<T>(defaultValue) + : fromJson<T>(jv); + } + + /** Load the property from the JSON response and delete it from JSON + * + * 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> + T takeFromJson(const QString& key, T&& defaultValue = {}) + { + if (const auto& jv = takeValueFromJson(key); !jv.isUndefined()) + return fromJson<T>(jv); + + return std::forward<T>(defaultValue); + } + + /** Error (more generally, status) code + * Equivalent to status().code + * \sa status + */ + int error() const; + + /** Error-specific message, as returned by the server */ + virtual QString errorString() const; + + /** A URL to help/clarify the error, if provided by the server */ + QUrl errorUrl() const; + + int maxRetries() const; + void setMaxRetries(int newMaxRetries); + + using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t + + std::chrono::seconds getCurrentTimeout() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t getCurrentTimeoutMs() const; + std::chrono::seconds getNextRetryInterval() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t getNextRetryMs() const; + std::chrono::milliseconds timeToRetry() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t millisToRetry() const; + + friend QDebug operator<<(QDebug dbg, const BaseJob* j) { - return job && job->error() == BaseJob::Pending; + return dbg << j->objectName(); } -} // namespace QMatrixClient + +public Q_SLOTS: + void initiate(Quotient::ConnectionData* connData, bool inBackground); + + /** + * Abandons the result of this job, arrived or unarrived. + * + * This aborts waiting for a reply from the server (if there was + * any pending) and deletes the job object. No result signals + * (result, success, failure) are emitted. + */ + void abandon(); + +Q_SIGNALS: + /** The job is about to send a network request */ + void aboutToSendRequest(); + + /** The job has sent a network request */ + void sentRequest(); + + /** The job has changed its status */ + void statusChanged(Quotient::BaseJob::Status newStatus); + + /** + * The previous network request has failed; the next attempt will + * be done in the specified time + * @param nextAttempt the 1-based number of attempt (will always be more + * than 1) + * @param inMilliseconds the interval after which the next attempt will be + * taken + */ + void retryScheduled(int nextAttempt, + Quotient::BaseJob::duration_ms_t inMilliseconds); + + /** + * The previous network request has been rate-limited; the next attempt + * will be queued and run sometime later. Since other jobs may already + * wait in the queue, it's not possible to predict the wait time. + */ + void rateLimited(); + + /** + * Emitted when the job is finished, in any case. It is used to notify + * observers that the job is terminated and that progress can be hidden. + * + * This should not be emitted directly by subclasses; + * use finishJob() instead. + * + * In general, to be notified of a job's completion, client code + * should connect to result(), success(), or failure() + * rather than finished(). However if you need to track the job's + * lifecycle you should connect to this instead of result(); + * in particular, only this signal will be emitted on abandoning. + * + * @param job the job that emitted this signal + * + * @see result, success, failure + */ + void finished(Quotient::BaseJob* job); + + /** + * Emitted when the job is finished (except when abandoned). + * + * Use error() to know if the job was finished with error. + * + * @param job the job that emitted this signal + * + * @see success, failure + */ + void result(Quotient::BaseJob* job); + + /** + * Emitted together with result() in case there's no error. + * + * @see result, failure + */ + void success(Quotient::BaseJob*); + + /** + * Emitted together with result() if there's an error. + * Similar to result(), this won't be emitted in case of abandon(). + * + * @see result, success + */ + void failure(Quotient::BaseJob*); + + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + +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); + QUrlQuery query() const; + void setRequestQuery(const QUrlQuery& query); + const RequestData& requestData() const; + void setRequestData(RequestData&& data); + const QByteArrayList& expectedContentTypes() const; + void addExpectedContentType(const QByteArray& contentType); + void setExpectedContentTypes(const QByteArrayList& contentTypes); + QByteArrayList expectedKeys() const; + void addExpectedKey(const QByteArray &key); + void setExpectedKeys(const QByteArrayList &keys); + + const QNetworkReply* reply() const; + QNetworkReply* reply(); + + /** Construct a URL out of baseUrl, path and query + * + * 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 QByteArray &encodedPath, + const QUrlQuery& query = {}); + + /*! Prepares the job for execution + * + * This method is called no more than once per job lifecycle, + * when it's first scheduled for execution; in particular, it is not called + * on retries. + */ + virtual void doPrepare(); + + /*! Postprocessing after the network request has been sent + * + * This method is called every time the job receives a running + * QNetworkReply object from NetworkAccessManager - basically, after + * successfully sending a network request (including retries). + */ + virtual void onSentRequest(QNetworkReply*); + virtual void beforeAbandon(); + + /*! \brief An extension point for additional reply processing. + * + * The base implementation does nothing and returns Success. + * + * \sa gotReply + */ + virtual Status prepareResult(); + + /*! \brief Process details of the error + * + * The function processes the reply in case when status from checkReply() + * 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 and only then process custom errors, + * with JSON or non-JSON payload. + * + * \return updated (if necessary) job status + */ + virtual Status prepareError(Status currentStatus); + + /*! \brief Get direct access to the JSON response object in the job + * + * This allows to implement deserialisation with "move" semantics for parts + * of the response. Assuming that the response body is a valid JSON object, + * the function calls QJsonObject::take(key) on it and returns the result. + * + * \return QJsonValue::Null, if the response content type is not + * advertised as `application/json`; + * QJsonValue::Undefined, if the response is a JSON object but + * doesn't have \p key; + * the value for \p key otherwise. + * + * \sa takeFromJson + */ + QJsonValue takeValueFromJson(const QString& key); + + void setStatus(Status s); + void setStatus(int code, QString message); + + // Q_DECLARE_LOGGING_CATEGORY return different function types + // in different versions + using LoggingCategory = decltype(JOBS)*; + void setLoggingCategory(LoggingCategory lcf); + + // Job objects should only be deleted via QObject::deleteLater + ~BaseJob() override; + +protected Q_SLOTS: + void timeout(); + + /*! \brief Check the pending or received reply for upfront issues + * + * This is invoked when headers are first received and also once + * the complete reply is obtained; the base implementation checks the HTTP + * headers to detect general issues such as network errors or access denial + * and it's strongly recommended to call it from overrides, + * as early as possible. + * This slot is const and cannot read the response body. If you need to read + * the body on the fly, override onSentRequest() and connect in it + * to reply->readyRead(); and if you only need to validate the body after + * it fully arrived, use prepareResult() for that). Returning anything + * except NoError/Success switches further processing from prepareResult() + * to prepareError(). + * + * @return the result of checking the reply + * + * @see gotReply + */ + virtual Status checkReply(const QNetworkReply *reply) const; + +private Q_SLOTS: + void sendRequest(); + void gotReply(); + + friend class ConnectionData; // to provide access to sendRequest() + +private: + void stop(); + void finishJob(); + + class Private; + ImplPtr<Private> d; +}; + +inline bool QUOTIENT_API isJobPending(BaseJob* job) +{ + return job && job->error() == BaseJob::Pending; +} +} // namespace Quotient diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 2bf9dd8f..759d52c9 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -1,56 +1,80 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "downloadfilejob.h" -#include <QtNetwork/QNetworkReply> #include <QtCore/QFile> #include <QtCore/QTemporaryFile> +#include <QtNetwork/QNetworkReply> -using namespace QMatrixClient; +#ifdef Quotient_E2EE_ENABLED +# include "events/filesourceinfo.h" -class DownloadFileJob::Private -{ - public: - Private() : tempFile(new QTemporaryFile()) { } +# include <QtCore/QCryptographicHash> +#endif - explicit Private(const QString& localFilename) - : targetFile(new QFile(localFilename)) - , tempFile(new QFile(targetFile->fileName() + ".qmcdownload")) - { } +using namespace Quotient; +class DownloadFileJob::Private { +public: + Private() : tempFile(new QTemporaryFile()) {} - QScopedPointer<QFile> targetFile; - QScopedPointer<QFile> tempFile; + explicit Private(const QString& localFilename) + : targetFile(new QFile(localFilename)) + , tempFile(new QFile(targetFile->fileName() + ".qtntdownload")) + {} + + QScopedPointer<QFile> targetFile; + QScopedPointer<QFile> tempFile; + +#ifdef Quotient_E2EE_ENABLED + Omittable<EncryptedFileMetadata> encryptedFileMetadata; +#endif }; QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) { - return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1)); + return makeRequestUrl(std::move(baseUrl), mxcUri.authority(), + mxcUri.path().mid(1)); } DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename) : GetContentJob(serverName, mediaId) - , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) + , d(localFilename.isEmpty() ? makeImpl<Private>() + : makeImpl<Private>(localFilename)) { - setObjectName("DownloadFileJob"); + setObjectName(QStringLiteral("DownloadFileJob")); } +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob::DownloadFileJob(const QString& serverName, + const QString& mediaId, + const EncryptedFileMetadata& file, + const QString& localFilename) + : GetContentJob(serverName, mediaId) + , d(localFilename.isEmpty() ? makeImpl<Private>() + : makeImpl<Private>(localFilename)) +{ + setObjectName(QStringLiteral("DownloadFileJob")); + d->encryptedFileMetadata = file; +} +#endif QString DownloadFileJob::targetFileName() const { return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); } -void DownloadFileJob::beforeStart(const ConnectionData*) +void DownloadFileJob::doPrepare() { - if (d->targetFile && !d->targetFile->isReadable() && - !d->targetFile->open(QIODevice::WriteOnly)) - { - qCWarning(JOBS) << "Couldn't open the file" - << d->targetFile->fileName() << "for writing"; + if (d->targetFile && !d->targetFile->isReadable() + && !d->targetFile->open(QIODevice::WriteOnly)) { + qCWarning(JOBS) << "Couldn't open the file" << d->targetFile->fileName() + << "for writing"; setStatus(FileError, "Could not open the target file for writing"); return; } - if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) - { + if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) { qCWarning(JOBS) << "Couldn't open the temporary file" << d->tempFile->fileName() << "for writing"; setStatus(FileError, "Could not open the temporary download file"); @@ -59,18 +83,16 @@ void DownloadFileJob::beforeStart(const ConnectionData*) qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName(); } -void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) +void DownloadFileJob::onSentRequest(QNetworkReply* reply) { - connect(reply, &QNetworkReply::metaDataChanged, this, [this,reply] { + connect(reply, &QNetworkReply::metaDataChanged, this, [this, reply] { if (!status().good()) return; auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader); - if (sizeHeader.isValid()) - { - auto targetSize = sizeHeader.value<qint64>(); + if (sizeHeader.isValid()) { + auto targetSize = sizeHeader.toLongLong(); if (targetSize != -1) - if (!d->tempFile->resize(targetSize)) - { + if (!d->tempFile->resize(targetSize)) { qCWarning(JOBS) << "Failed to allocate" << targetSize << "bytes for" << d->tempFile->fileName(); setStatus(FileError, @@ -78,45 +100,79 @@ void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) } } }); - connect(reply, &QIODevice::readyRead, this, [this,reply] { + connect(reply, &QIODevice::readyRead, this, [this, reply] { if (!status().good()) return; auto bytes = reply->read(reply->bytesAvailable()); if (!bytes.isEmpty()) d->tempFile->write(bytes); else - qCWarning(JOBS) - << "Unexpected empty chunk when downloading from" - << reply->url() << "to" << d->tempFile->fileName(); + qCWarning(JOBS) << "Unexpected empty chunk when downloading from" + << reply->url() << "to" << d->tempFile->fileName(); }); } -void DownloadFileJob::beforeAbandon(QNetworkReply*) +void DownloadFileJob::beforeAbandon() { if (d->targetFile) d->targetFile->remove(); d->tempFile->remove(); } -BaseJob::Status DownloadFileJob::parseReply(QNetworkReply*) +void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata, + QFile& targetFile) { - if (d->targetFile) - { - d->targetFile->close(); - if (!d->targetFile->remove()) - { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; - return { FileError, "Couldn't finalise the download" }; + sourceFile.seek(0); + const auto encrypted = sourceFile.readAll(); // TODO: stream decryption + const auto decrypted = decryptFile(encrypted, metadata); + targetFile.write(decrypted); +} + +BaseJob::Status DownloadFileJob::prepareResult() +{ + if (d->targetFile) { +#ifdef Quotient_E2EE_ENABLED + if (d->encryptedFileMetadata.has_value()) { + decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile); + d->tempFile->remove(); + } else { +#endif + d->targetFile->close(); + if (!d->targetFile->remove()) { + qWarning(JOBS) << "Failed to remove the target file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!d->tempFile->rename(d->targetFile->fileName())) { + qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + << "to" << d->targetFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } +#ifdef Quotient_E2EE_ENABLED } - if (!d->tempFile->rename(d->targetFile->fileName())) - { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() - << "to" << d->targetFile->fileName(); - return { FileError, "Couldn't finalise the download" }; +#endif + } else { +#ifdef Quotient_E2EE_ENABLED + if (d->encryptedFileMetadata.has_value()) { + QTemporaryFile tempTempFile; // Assuming it to be next to tempFile + decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile); + d->tempFile->close(); + if (!d->tempFile->remove()) { + qWarning(JOBS) + << "Failed to remove the decrypted file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!tempTempFile.rename(d->tempFile->fileName())) { + qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() + << "to" << d->tempFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } + } else { +#endif + d->tempFile->close(); +#ifdef Quotient_E2EE_ENABLED } +#endif } - else - d->tempFile->close(); - qCDebug(JOBS) << "Saved a file as" << targetFileName(); + qDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index ce47ab1c..cbbfd244 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -1,30 +1,33 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "csapi/content-repo.h" -namespace QMatrixClient -{ - class DownloadFileJob : public GetContentJob - { - public: - enum { FileError = BaseJob::UserDefinedError + 1 }; +#include "events/filesourceinfo.h" - using GetContentJob::makeRequestUrl; - static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); +namespace Quotient { +class QUOTIENT_API DownloadFileJob : public GetContentJob { +public: + using GetContentJob::makeRequestUrl; + static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); - DownloadFileJob(const QString& serverName, const QString& mediaId, - const QString& localFilename = {}); + DownloadFileJob(const QString& serverName, const QString& mediaId, + const QString& localFilename = {}); - QString targetFileName() const; +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {}); +#endif + QString targetFileName() const; - private: - class Private; - QScopedPointer<Private> d; +private: + class Private; + ImplPtr<Private> d; - void beforeStart(const ConnectionData*) override; - void afterStart(const ConnectionData*, - QNetworkReply* reply) override; - void beforeAbandon(QNetworkReply*) override; - Status parseReply(QNetworkReply*) override; - }; -} + void doPrepare() override; + void onSentRequest(QNetworkReply* reply) override; + void beforeAbandon() override; + Status prepareResult() override; +}; +} // namespace Quotient diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index aeb49839..6fe8ef26 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -1,63 +1,46 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "mediathumbnailjob.h" -using namespace QMatrixClient; +using namespace Quotient; -QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, - const QUrl& mxcUri, QSize requestedSize) +QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, + QSize requestedSize) { - return makeRequestUrl(std::move(baseUrl), - mxcUri.authority(), mxcUri.path().mid(1), - requestedSize.width(), requestedSize.height()); + return makeRequestUrl(std::move(baseUrl), mxcUri.authority(), + mxcUri.path().mid(1), requestedSize.width(), + requestedSize.height()); } MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, const QString& mediaId, QSize requestedSize) - : GetContentThumbnailJob(serverName, mediaId, - requestedSize.width(), requestedSize.height()) -{ } + : 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 '/' + : MediaThumbnailJob(mxcUri.authority(), + mxcUri.path().mid(1), // sans leading '/' requestedSize) -{ } - -QImage MediaThumbnailJob::thumbnail() const { - return _thumbnail; + setLoggingCategory(THUMBNAILJOB); } +QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; } + QImage MediaThumbnailJob::scaledThumbnail(QSize toSize) const { - return _thumbnail.scaled(toSize, - Qt::KeepAspectRatio, Qt::SmoothTransformation); + return _thumbnail.scaled(toSize, Qt::KeepAspectRatio, + Qt::SmoothTransformation); } -BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) +BaseJob::Status MediaThumbnailJob::prepareResult() { - auto result = GetContentThumbnailJob::parseReply(reply); - if (!result.good()) - return result; - - if( _thumbnail.loadFromData(data()->readAll()) ) + if (_thumbnail.loadFromData(data()->readAll())) return Success; - return { IncorrectResponseError, "Could not read image data" }; + return { IncorrectResponse, QStringLiteral("Could not read image data") }; } diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index 7963796e..c9f6da35 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -22,26 +7,24 @@ #include <QtGui/QPixmap> -namespace QMatrixClient -{ - class MediaThumbnailJob: public GetContentThumbnailJob - { - public: - using GetContentThumbnailJob::makeRequestUrl; - static QUrl makeRequestUrl(QUrl baseUrl, - const QUrl& mxcUri, QSize requestedSize); - - MediaThumbnailJob(const QString& serverName, const QString& mediaId, - QSize requestedSize); - MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize); - - QImage thumbnail() const; - QImage scaledThumbnail(QSize toSize) const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - QImage _thumbnail; - }; -} // namespace QMatrixClient +namespace Quotient { +class QUOTIENT_API MediaThumbnailJob : public GetContentThumbnailJob { +public: + using GetContentThumbnailJob::makeRequestUrl; + static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, + QSize requestedSize); + + MediaThumbnailJob(const QString& serverName, const QString& mediaId, + QSize requestedSize); + MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize); + + QImage thumbnail() const; + QImage scaledThumbnail(QSize toSize) const; + +protected: + Status prepareResult() override; + +private: + QImage _thumbnail; +}; +} // namespace Quotient diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h deleted file mode 100644 index 63a8e1d0..00000000 --- a/lib/jobs/postreadmarkersjob.h +++ /dev/null @@ -1,39 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "basejob.h" - -#include <QtCore/QJsonObject> - -using namespace QMatrixClient; - -class PostReadMarkersJob : public BaseJob -{ - public: - explicit PostReadMarkersJob(const QString& roomId, - const QString& readUpToEventId) - : BaseJob(HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers") - .arg(roomId)) - { - setRequestData(QJsonObject {{ - QStringLiteral("m.fully_read"), readUpToEventId }}); - } -}; diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 5cb62221..ab249f6d 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -1,19 +1,22 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "requestdata.h" +#include <QtCore/QIODevice> +#include <QtCore/QBuffer> #include <QtCore/QByteArray> -#include <QtCore/QJsonObject> #include <QtCore/QJsonArray> #include <QtCore/QJsonDocument> -#include <QtCore/QBuffer> +#include <QtCore/QJsonObject> -using namespace QMatrixClient; +using namespace Quotient; auto fromData(const QByteArray& data) { - auto source = std::make_unique<QBuffer>(); - source->open(QIODevice::WriteOnly); - source->write(data); - source->close(); + auto source = makeImpl<QBuffer, QIODevice>(); + source->setData(data); + source->open(QIODevice::ReadOnly); return source; } @@ -23,16 +26,12 @@ inline auto fromJson(const JsonDataT& jdata) return fromData(QJsonDocument(jdata).toJson(QJsonDocument::Compact)); } -RequestData::RequestData(const QByteArray& a) - : _source(fromData(a)) -{ } +RequestData::RequestData(const QByteArray& a) : _source(fromData(a)) {} -RequestData::RequestData(const QJsonObject& jo) - : _source(fromJson(jo)) -{ } +RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} -RequestData::RequestData(const QJsonArray& ja) - : _source(fromJson(ja)) -{ } +RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} -RequestData::~RequestData() = default; +RequestData::RequestData(QIODevice* source) + : _source(acquireImpl(source)) +{} diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index db011b61..accc8f71 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -1,61 +1,35 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include <memory> +#include "util.h" -class QByteArray; class QJsonObject; class QJsonArray; class QJsonDocument; class QIODevice; -namespace QMatrixClient -{ - /** - * A simple wrapper that represents the request body. - * Provides a unified interface to dump an unstructured byte stream - * as well as JSON (and possibly other structures in the future) to - * a QByteArray consumed by QNetworkAccessManager request methods. - */ - class RequestData - { - public: - RequestData() = default; - RequestData(const QByteArray& a); - RequestData(const QJsonObject& jo); - RequestData(const QJsonArray& ja); - RequestData(QIODevice* source) - : _source(std::unique_ptr<QIODevice>(source)) - { } - RequestData(const RequestData&) = delete; - RequestData& operator=(const RequestData&) = delete; - RequestData(RequestData&&) = default; - RequestData& operator=(RequestData&&) = default; - ~RequestData(); +namespace Quotient { +/** + * A simple wrapper that represents the request body. + * Provides a unified interface to dump an unstructured byte stream + * as well as JSON (and possibly other structures in the future) to + * a QByteArray consumed by QNetworkAccessManager request methods. + */ +class QUOTIENT_API RequestData { +public: + // NOLINTBEGIN(google-explicit-constructor): that check should learn about + // explicit(false) + QUO_IMPLICIT RequestData(const QByteArray& a = {}); + QUO_IMPLICIT RequestData(const QJsonObject& jo); + QUO_IMPLICIT RequestData(const QJsonArray& ja); + QUO_IMPLICIT RequestData(QIODevice* source); + // NOLINTEND(google-explicit-constructor) - QIODevice* source() const - { - return _source.get(); - } + QIODevice* source() const { return _source.get(); } - private: - std::unique_ptr<QIODevice> _source; - }; -} // namespace QMatrixClient +private: + ImplPtr<QIODevice> _source; +}; +} // namespace Quotient diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 6baf388e..f5c632bf 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -1,156 +1,48 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "syncjob.h" -#include "events/eventloader.h" - -#include <QtCore/QElapsedTimer> - -using namespace QMatrixClient; +using namespace Quotient; 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; - if( !filter.isEmpty() ) + if (!filter.isEmpty()) query.addQueryItem(QStringLiteral("filter"), filter); - if( !presence.isEmpty() ) + if (!presence.isEmpty()) query.addQueryItem(QStringLiteral("set_presence"), presence); - if( timeout >= 0 ) + if (timeout >= 0) query.addQueryItem(QStringLiteral("timeout"), QString::number(timeout)); - if( !since.isEmpty() ) + if (!since.isEmpty()) query.addQueryItem(QStringLiteral("since"), since); setRequestQuery(query); setMaxRetries(std::numeric_limits<int>::max()); } -QString SyncData::nextBatch() const -{ - return nextBatch_; -} - -SyncDataList&& SyncData::takeRoomData() -{ - return std::move(roomData); -} - -Events&& SyncData::takePresenceData() -{ - return std::move(presenceData); -} - -Events&& SyncData::takeAccountData() -{ - return std::move(accountData); -} - -Events&&SyncData::takeToDeviceEvents() -{ - return std::move(toDeviceEvents); -} - -template <typename EventsArrayT, typename StrT> -inline EventsArrayT load(const QJsonObject& batches, StrT keyName) -{ - return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); -} - -BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) -{ - return d.parseJson(data); -} - -BaseJob::Status SyncData::parseJson(const QJsonDocument &data) -{ - QElapsedTimer et; et.start(); - - auto json = data.object(); - nextBatch_ = json.value("next_batch"_ls).toString(); - presenceData = load<Events>(json, "presence"_ls); - accountData = load<Events>(json, "account_data"_ls); - toDeviceEvents = load<Events>(json, "to_device"_ls); - - auto rooms = json.value("rooms"_ls).toObject(); - JoinStates::Int ii = 1; // ii is used to make a JoinState value - auto totalRooms = 0; - auto totalEvents = 0; - for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) - { - const auto rs = rooms.value(JoinStateStrings[i]).toObject(); - // We have a Qt container on the right and an STL one on the left - roomData.reserve(static_cast<size_t>(rs.size())); - for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) - { - roomData.emplace_back(roomIt.key(), JoinState(ii), - roomIt.value().toObject()); - const auto& r = roomData.back(); - totalEvents += r.state.size() + r.ephemeral.size() + - r.accountData.size() + r.timeline.size(); - } - totalRooms += rs.size(); - } - if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << totalRooms << "room(s)," - << totalEvents << "event(s) in" << et; - return BaseJob::Success; -} - -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-qmatrixclient.unread_count"); - -SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, - const QJsonObject& room_) - : roomId(roomId_) - , joinState(joinState_) - , state(load<StateEvents>(room_, - joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) -{ - switch (joinState) { - case JoinState::Join: - ephemeral = load<Events>(room_, "ephemeral"_ls); - FALLTHROUGH; - case JoinState::Leave: - { - accountData = load<Events>(room_, "account_data"_ls); - timeline = load<RoomEvents>(room_, "timeline"_ls); - const auto timelineJson = room_.value("timeline"_ls).toObject(); - timelineLimited = timelineJson.value("limited"_ls).toBool(); - timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); - - break; - } - default: /* nothing on top of state */; - } - - const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); - unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); - highlightCount = unreadJson.value("highlight_count"_ls).toInt(); - notificationCount = unreadJson.value("notification_count"_ls).toInt(); - if (highlightCount > 0 || notificationCount > 0) - qCDebug(SYNCJOB) << "Room" << roomId_ - << "has highlights:" << highlightCount - << "and notifications:" << notificationCount; +SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout, + const QString& presence) + : SyncJob(since, + QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact), + timeout, presence) +{} + +BaseJob::Status SyncJob::prepareResult() +{ + d.parseJson(jsonData()); + if (Q_LIKELY(d.unresolvedRooms().isEmpty())) + return Success; + + Q_ASSERT(d.unresolvedRooms().isEmpty()); + qCCritical(MAIN).noquote() << "Rooms missing after processing sync " + "response, possibly a bug in SyncData: " + << d.unresolvedRooms().join(','); + return IncorrectResponse; } diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 6b9bedfa..b7bfbbb3 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -1,88 +1,26 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "../csapi/definitions/sync_filter.h" +#include "../syncdata.h" #include "basejob.h" -#include "joinstate.h" -#include "events/stateevent.h" -#include "util.h" +namespace Quotient { +class SyncJob : public BaseJob { +public: + explicit SyncJob(const QString& since = {}, const QString& filter = {}, + int timeout = -1, const QString& presence = {}); + explicit SyncJob(const QString& since, const Filter& filter, + int timeout = -1, const QString& presence = {}); -namespace QMatrixClient -{ - class SyncRoomData - { - public: - QString roomId; - JoinState joinState; - StateEvents state; - RoomEvents timeline; - Events ephemeral; - Events accountData; + SyncData takeData() { return std::move(d); } - bool timelineLimited; - QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; +protected: + Status prepareResult() override; - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); - SyncRoomData(SyncRoomData&&) = default; - SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; - }; - // QVector cannot work with non-copiable objects, std::vector can. - using SyncDataList = std::vector<SyncRoomData>; - - class SyncData - { - public: - BaseJob::Status parseJson(const QJsonDocument &data); - Events&& takePresenceData(); - Events&& takeAccountData(); - Events&& takeToDeviceEvents(); - SyncDataList&& takeRoomData(); - QString nextBatch() const; - - private: - QString nextBatch_; - Events presenceData; - Events accountData; - Events toDeviceEvents; - SyncDataList roomData; - }; - - class SyncJob: public BaseJob - { - public: - explicit SyncJob(const QString& since = {}, - const QString& filter = {}, - int timeout = -1, const QString& presence = {}); - - SyncData &&takeData() { return std::move(d); } - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - SyncData d; - }; -} // namespace QMatrixClient +private: + SyncData d; +}; +} // namespace Quotient diff --git a/lib/joinstate.h b/lib/joinstate.h deleted file mode 100644 index c172f576..00000000 --- a/lib/joinstate.h +++ /dev/null @@ -1,48 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include <QtCore/QFlags> - -#include <array> - -namespace QMatrixClient -{ - enum class JoinState - { - Join = 0x1, - Invite = 0x2, - Leave = 0x4, - }; - - Q_DECLARE_FLAGS(JoinStates, JoinState) - - // We cannot use REGISTER_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 >>= 1) ++index; - return JoinStateStrings[index]; - } -} // namespace QMatrixClient -Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::JoinStates) diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp new file mode 100644 index 00000000..4c61964c --- /dev/null +++ b/lib/keyverificationsession.cpp @@ -0,0 +1,501 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "keyverificationsession.h" + +#include "connection.h" +#include "database.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutils.h" +#include "olm/sas.h" + +#include "events/event.h" + +#include <QtCore/QCryptographicHash> +#include <QtCore/QTimer> +#include <QtCore/QUuid> + +#include <chrono> + +using namespace Quotient; +using namespace std::chrono; + +const QStringList supportedMethods = { SasV1Method }; + +QStringList commonSupportedMethods(const QStringList& remoteMethods) +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (supportedMethods.contains(method)) { + result += method; + } + } + return result; +} + +KeyVerificationSession::KeyVerificationSession( + QString remoteUserId, const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted) + : QObject(connection) + , m_remoteUserId(std::move(remoteUserId)) + , m_remoteDeviceId(event.fromDevice()) + , m_transactionId(event.transactionId()) + , m_connection(connection) + , m_encrypted(encrypted) + , m_remoteSupportedMethods(event.methods()) +{ + const auto& currentTime = QDateTime::currentDateTime(); + const auto timeoutTime = + std::min(event.timestamp().addSecs(600), currentTime.addSecs(120)); + const milliseconds timeout{ currentTime.msecsTo(timeoutTime) }; + if (timeout > 5s) + init(timeout); + // Otherwise don't even bother starting up +} + +KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId, + Connection* connection) + : QObject(connection) + , m_remoteUserId(std::move(userId)) + , m_remoteDeviceId(std::move(deviceId)) + , m_transactionId(QUuid::createUuid().toString()) + , m_connection(connection) + , m_encrypted(false) +{ + init(600s); + QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); +} + +void KeyVerificationSession::init(milliseconds timeout) +{ + QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); + + m_sas = olm_sas(new std::byte[olm_sas_size()]); + const auto randomLength = olm_create_sas_random_length(m_sas); + olm_create_sas(m_sas, RandomBuffer(randomLength), randomLength); +} + +KeyVerificationSession::~KeyVerificationSession() +{ + olm_clear_sas(m_sas); + delete[] reinterpret_cast<std::byte*>(m_sas); +} + +void KeyVerificationSession::handleEvent(const KeyVerificationEvent& baseEvent) +{ + if (!switchOnType( + baseEvent, + [this](const KeyVerificationCancelEvent& event) { + setError(stringToError(event.code())); + setState(CANCELED); + return true; + }, + [this](const KeyVerificationStartEvent& event) { + if (state() != WAITINGFORREADY && state() != READY) + return false; + handleStart(event); + return true; + }, + [this](const KeyVerificationReadyEvent& event) { + if (state() == WAITINGFORREADY) + handleReady(event); + // ACCEPTED is also fine here because it's possible to receive + // ready and start in the same sync, in which case start might + // be handled before ready. + return state() == WAITINGFORREADY || state() == ACCEPTED; + }, + [this](const KeyVerificationAcceptEvent& event) { + if (state() != WAITINGFORACCEPT) + return false; + m_commitment = event.commitment(); + sendKey(); + setState(WAITINGFORKEY); + return true; + }, + [this](const KeyVerificationKeyEvent& event) { + if (state() != ACCEPTED && state() != WAITINGFORKEY) + return false; + handleKey(event); + return true; + }, + [this](const KeyVerificationMacEvent& event) { + if (state() != WAITINGFORMAC && state() != WAITINGFORVERIFICATION) + return false; + handleMac(event); + return true; + }, + [this](const KeyVerificationDoneEvent&) { return state() == DONE; })) + cancelVerification(UNEXPECTED_MESSAGE); +} + +struct EmojiStoreEntry : EmojiEntry { + QHash<QString, QString> translatedDescriptions; + + explicit EmojiStoreEntry(const QJsonObject& json) + : EmojiEntry{ fromJson<QString>(json["emoji"]), + fromJson<QString>(json["description"]) } + , translatedDescriptions{ fromJson<QHash<QString, QString>>( + json["translated_descriptions"]) } + {} +}; + +using EmojiStore = QVector<EmojiStoreEntry>; + +EmojiStore loadEmojiStore() +{ + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + return fromJson<EmojiStore>( + QJsonDocument::fromJson(dataFile.readAll()).array()); +} + +EmojiEntry emojiForCode(int code, const QString& language) +{ + static const EmojiStore emojiStore = loadEmojiStore(); + const auto& entry = emojiStore[code]; + if (!language.isEmpty()) + if (const auto translatedDescription = + emojiStore[code].translatedDescriptions.value(language); + !translatedDescription.isNull()) + return { entry.emoji, translatedDescription }; + + return SLICE(entry, EmojiEntry); +} + +void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) +{ + auto eventKey = event.key().toLatin1(); + olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size()); + + if (startSentByUs) { + const auto paddedCommitment = + QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), + QCryptographicHash::Sha256) + .toBase64(); + const QLatin1String unpaddedCommitment(paddedCommitment.constData(), + paddedCommitment.indexOf('=')); + if (unpaddedCommitment != m_commitment) { + qCWarning(E2EE) << "Commitment mismatch; aborting verification"; + cancelVerification(MISMATCHED_COMMITMENT); + return; + } + } else { + sendKey(); + } + + std::string key(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, key.data(), key.size()); + + std::array<std::byte, 6> output{}; + const auto infoTemplate = + startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls + : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + const auto info = infoTemplate + .arg(m_connection->userId(), m_connection->deviceId(), + key.data(), m_remoteUserId, m_remoteDeviceId, + event.key(), m_transactionId) + .toLatin1(); + olm_sas_generate_bytes(m_sas, info.data(), info.size(), output.data(), + output.size()); + + static constexpr auto x3f = std::byte{ 0x3f }; + const std::array<std::byte, 7> code{ + output[0] >> 2, + (output[0] << 4 & x3f) | output[1] >> 4, + (output[1] << 2 & x3f) | output[2] >> 6, + output[2] & x3f, + output[3] >> 2, + (output[3] << 4 & x3f) | output[4] >> 4, + (output[4] << 2 & x3f) | output[5] >> 6 + }; + + const auto uiLanguages = QLocale().uiLanguages(); + const auto preferredLanguage = uiLanguages.isEmpty() + ? QString() + : uiLanguages.front().section('-', 0, 0); + for (const auto& c : code) + m_sasEmojis += emojiForCode(std::to_integer<int>(c), preferredLanguage); + + emit sasEmojisChanged(); + emit keyReceived(); + setState(WAITINGFORVERIFICATION); +} + +QString KeyVerificationSession::calculateMac(const QString& input, + bool verifying, + const QString& keyId) +{ + QByteArray inputBytes = input.toLatin1(); + QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); + const auto macInfo = + (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls + : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls) + .arg(m_connection->userId(), m_connection->deviceId(), + m_remoteUserId, m_remoteDeviceId, m_transactionId, keyId) + .toLatin1(); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), + macInfo.data(), macInfo.size(), outputBytes.data(), + outputBytes.size()); + return QString::fromLatin1(outputBytes.data(), outputBytes.indexOf('=')); +} + +void KeyVerificationSession::sendMac() +{ + QString edKeyId = "ed25519:" % m_connection->deviceId(); + + auto keys = calculateMac(edKeyId, false); + + QJsonObject mac; + auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; + mac[edKeyId] = calculateMac(key, false, edKeyId); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationMacEvent(m_transactionId, keys, + mac), + m_encrypted); + setState (macReceived ? DONE : WAITINGFORMAC); + m_verified = true; + if (!m_pendingEdKeyId.isEmpty()) { + trustKeys(); + } +} + +void KeyVerificationSession::sendDone() +{ + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationDoneEvent(m_transactionId), + m_encrypted); +} + +void KeyVerificationSession::sendKey() +{ + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationKeyEvent(m_transactionId, + keyBytes), + m_encrypted); +} + + +void KeyVerificationSession::cancelVerification(Error error) +{ + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationCancelEvent(m_transactionId, + errorToString(error)), + m_encrypted); + setState(CANCELED); + setError(error); + emit finished(); + deleteLater(); +} + +void KeyVerificationSession::sendReady() +{ + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(), + methods), + m_encrypted); + setState(READY); + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::sendStartSas() +{ + startSentByUs = true; + KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId()); + m_startEvent = + QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, event, + m_encrypted); + setState(WAITINGFORACCEPT); +} + +void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) +{ + setState(READY); + m_remoteSupportedMethods = event.methods(); + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) + cancelVerification(UNKNOWN_METHOD); + else if (methods.size() == 1) + sendStartSas(); // -> WAITINGFORACCEPT +} + +void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) +{ + if (startSentByUs) { + if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) { + return; + } else { + startSentByUs = false; + } + } + QByteArray publicKey(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, publicKey.data(), publicKey.size()); + const auto canonicalEvent = QString(QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact)); + auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationAcceptEvent(m_transactionId, + commitment), + m_encrypted); + setState(ACCEPTED); +} + +void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) +{ + QStringList keys = event.mac().keys(); + keys.sort(); + const auto& key = keys.join(","); + const QString edKeyId = "ed25519:"_ls % m_remoteDeviceId; + + if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId), true, edKeyId) != event.mac()[edKeyId]) { + cancelVerification(KEY_MISMATCH); + return; + } + + if (calculateMac(key, true) != event.keys()) { + cancelVerification(KEY_MISMATCH); + return; + } + + m_pendingEdKeyId = edKeyId; + + if (m_verified) { + trustKeys(); + } +} + +void KeyVerificationSession::trustKeys() +{ + m_connection->database()->setSessionVerified(m_pendingEdKeyId); + emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); + macReceived = true; + + if (state() == WAITINGFORMAC) { + setState(DONE); + sendDone(); + emit finished(); + deleteLater(); + } +} + +QVector<EmojiEntry> KeyVerificationSession::sasEmojis() const +{ + return m_sasEmojis; +} + +void KeyVerificationSession::sendRequest() +{ + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(), + supportedMethods, + QDateTime::currentDateTime()), + m_encrypted); + setState(WAITINGFORREADY); +} + +KeyVerificationSession::State KeyVerificationSession::state() const +{ + return m_state; +} + +void KeyVerificationSession::setState(KeyVerificationSession::State state) +{ + m_state = state; + emit stateChanged(); +} + +KeyVerificationSession::Error KeyVerificationSession::error() const +{ + return m_error; +} + +void KeyVerificationSession::setError(Error error) +{ + m_error = error; + emit errorChanged(); +} + +QString KeyVerificationSession::errorToString(Error error) +{ + switch(error) { + case NONE: + return "none"_ls; + case TIMEOUT: + return "m.timeout"_ls; + case USER: + return "m.user"_ls; + case UNEXPECTED_MESSAGE: + return "m.unexpected_message"_ls; + case UNKNOWN_TRANSACTION: + return "m.unknown_transaction"_ls; + case UNKNOWN_METHOD: + return "m.unknown_method"_ls; + case KEY_MISMATCH: + return "m.key_mismatch"_ls; + case USER_MISMATCH: + return "m.user_mismatch"_ls; + case INVALID_MESSAGE: + return "m.invalid_message"_ls; + case SESSION_ACCEPTED: + return "m.accepted"_ls; + case MISMATCHED_COMMITMENT: + return "m.mismatched_commitment"_ls; + case MISMATCHED_SAS: + return "m.mismatched_sas"_ls; + default: + return "m.user"_ls; + } +} + +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) +{ + if (error == "m.timeout"_ls) { + return REMOTE_TIMEOUT; + } else if (error == "m.user"_ls) { + return REMOTE_USER; + } else if (error == "m.unexpected_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_transaction"_ls) { + return REMOTE_UNKNOWN_TRANSACTION; + } else if (error == "m.unknown_method"_ls) { + return REMOTE_UNKNOWN_METHOD; + } else if (error == "m.key_mismatch"_ls) { + return REMOTE_KEY_MISMATCH; + } else if (error == "m.user_mismatch"_ls) { + return REMOTE_USER_MISMATCH; + } else if (error == "m.invalid_message"_ls) { + return REMOTE_INVALID_MESSAGE; + } else if (error == "m.accepted"_ls) { + return REMOTE_SESSION_ACCEPTED; + } else if (error == "m.mismatched_commitment"_ls) { + return REMOTE_MISMATCHED_COMMITMENT; + } else if (error == "m.mismatched_sas"_ls) { + return REMOTE_MISMATCHED_SAS; + } + return NONE; +} + +QString KeyVerificationSession::remoteDeviceId() const +{ + return m_remoteDeviceId; +} diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h new file mode 100644 index 00000000..32a91cfc --- /dev/null +++ b/lib/keyverificationsession.h @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/keyverificationevent.h" + +#include <QtCore/QObject> + +struct OlmSAS; + +namespace Quotient { +class Connection; + +struct QUOTIENT_API EmojiEntry { + QString emoji; + QString description; + + Q_GADGET + Q_PROPERTY(QString emoji MEMBER emoji CONSTANT) + Q_PROPERTY(QString description MEMBER description CONSTANT) + +public: + bool operator==(const EmojiEntry& rhs) const = default; +}; + +/** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. + Start a new session using Connection::startKeyVerificationSession. + The object is delete after finished is emitted. +*/ +class QUOTIENT_API KeyVerificationSession : public QObject +{ + Q_OBJECT + +public: + enum State { + INCOMING, ///< There is a request for verification incoming + //! We sent a request for verification and are waiting for ready + WAITINGFORREADY, + //! Either party sent a ready as a response to a request; the user + //! selects a method + READY, + WAITINGFORACCEPT, ///< We sent a start and are waiting for an accept + ACCEPTED, ///< The other party sent an accept and is waiting for a key + WAITINGFORKEY, ///< We're waiting for a key + //! We're waiting for the *user* to verify the emojis + WAITINGFORVERIFICATION, + WAITINGFORMAC, ///< We're waiting for the mac + CANCELED, ///< The session has been canceled + DONE, ///< The verification is done + }; + Q_ENUM(State) + + enum Error { + NONE, + TIMEOUT, + REMOTE_TIMEOUT, + USER, + REMOTE_USER, + UNEXPECTED_MESSAGE, + REMOTE_UNEXPECTED_MESSAGE, + UNKNOWN_TRANSACTION, + REMOTE_UNKNOWN_TRANSACTION, + UNKNOWN_METHOD, + REMOTE_UNKNOWN_METHOD, + KEY_MISMATCH, + REMOTE_KEY_MISMATCH, + USER_MISMATCH, + REMOTE_USER_MISMATCH, + INVALID_MESSAGE, + REMOTE_INVALID_MESSAGE, + SESSION_ACCEPTED, + REMOTE_SESSION_ACCEPTED, + MISMATCHED_COMMITMENT, + REMOTE_MISMATCHED_COMMITMENT, + MISMATCHED_SAS, + REMOTE_MISMATCHED_SAS, + }; + Q_ENUM(Error) + + Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) + Q_PROPERTY(QVector<EmojiEntry> sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(Error error READ error NOTIFY errorChanged) + + KeyVerificationSession(QString remoteUserId, + const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted); + KeyVerificationSession(QString userId, QString deviceId, + Connection* connection); + ~KeyVerificationSession() override; + Q_DISABLE_COPY_MOVE(KeyVerificationSession) + + void handleEvent(const KeyVerificationEvent& baseEvent); + + QVector<EmojiEntry> sasEmojis() const; + State state() const; + + Error error() const; + + QString remoteDeviceId() const; + +public Q_SLOTS: + void sendRequest(); + void sendReady(); + void sendMac(); + void sendStartSas(); + void sendKey(); + void sendDone(); + void cancelVerification(Error error); + +Q_SIGNALS: + void keyReceived(); + void sasEmojisChanged(); + void stateChanged(); + void errorChanged(); + void finished(); + +private: + const QString m_remoteUserId; + const QString m_remoteDeviceId; + const QString m_transactionId; + Connection* m_connection; + OlmSAS* m_sas = nullptr; + QVector<EmojiEntry> m_sasEmojis; + bool startSentByUs = false; + State m_state = INCOMING; + Error m_error = NONE; + QString m_startEvent; + QString m_commitment; + bool macReceived = false; + bool m_encrypted; + QStringList m_remoteSupportedMethods; + bool m_verified = false; + QString m_pendingEdKeyId{}; + + void handleReady(const KeyVerificationReadyEvent& event); + void handleStart(const KeyVerificationStartEvent& event); + void handleKey(const KeyVerificationKeyEvent& event); + void handleMac(const KeyVerificationMacEvent& event); + void init(std::chrono::milliseconds timeout); + void setState(State state); + void setError(Error error); + static QString errorToString(Error error); + static Error stringToError(const QString& error); + void trustKeys(); + + QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); + QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); +}; + +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::EmojiEntry) diff --git a/lib/logging.cpp b/lib/logging.cpp index 7476781f..460caced 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -1,33 +1,22 @@ -/****************************************************************************** - * Copyright (C) 2017 Elvis Angelaccio <elvid.angelaccio@kde.org> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Elvis Angelaccio <elvid.angelaccio@kde.org> +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "logging.h" -#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) #define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg) -#else -#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id)) -#endif // Use LOGGING_CATEGORY instead of Q_LOGGING_CATEGORY in the rest of the code -LOGGING_CATEGORY(MAIN, "libqmatrixclient.main") -LOGGING_CATEGORY(PROFILER, "libqmatrixclient.profiler") -LOGGING_CATEGORY(EVENTS, "libqmatrixclient.events") -LOGGING_CATEGORY(EPHEMERAL, "libqmatrixclient.events.ephemeral") -LOGGING_CATEGORY(JOBS, "libqmatrixclient.jobs") -LOGGING_CATEGORY(SYNCJOB, "libqmatrixclient.jobs.sync") +LOGGING_CATEGORY(MAIN, "quotient.main") +LOGGING_CATEGORY(EVENTS, "quotient.events") +LOGGING_CATEGORY(STATE, "quotient.events.state") +LOGGING_CATEGORY(MEMBERS, "quotient.events.members") +LOGGING_CATEGORY(MESSAGES, "quotient.events.messages") +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") +LOGGING_CATEGORY(DATABASE, "quotient.database") diff --git a/lib/logging.h b/lib/logging.h index a3a65887..1fafa04b 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -1,20 +1,6 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Elvis Angelaccio <elvid.angelaccio@kde.org> +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -22,68 +8,74 @@ #include <QtCore/QLoggingCategory> Q_DECLARE_LOGGING_CATEGORY(MAIN) -Q_DECLARE_LOGGING_CATEGORY(PROFILER) +Q_DECLARE_LOGGING_CATEGORY(STATE) +Q_DECLARE_LOGGING_CATEGORY(MEMBERS) +Q_DECLARE_LOGGING_CATEGORY(MESSAGES) Q_DECLARE_LOGGING_CATEGORY(EVENTS) 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) +Q_DECLARE_LOGGING_CATEGORY(DATABASE) -namespace QMatrixClient -{ - // QDebug manipulators +namespace Quotient { +// QDebug manipulators - using QDebugManip = QDebug (*)(QDebug); +using QDebugManip = QDebug (*)(QDebug); - /** - * @brief QDebug manipulator to setup the stream for JSON output - * - * Originally made to encapsulate the change in QDebug behavior in Qt 5.4 - * and the respective addition of QDebug::noquote(). - * Together with the operator<<() helper, the proposed usage is - * (similar to std:: I/O manipulators): - * - * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.) - */ - 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 QDebug manipulator to setup the stream for JSON output + * + * Originally made to encapsulate the change in QDebug behavior in Qt 5.4 + * and the respective addition of QDebug::noquote(). + * Together with the operator<<() helper, the proposed usage is + * (similar to std:: I/O manipulators): + * + * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.) + */ +inline QDebug formatJson(QDebug debug_object) +{ + return debug_object.noquote(); +} - /** - * @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) - { - return qdm(debug_object); - } +//! Suppress full qualification of enums/QFlags when logging +inline QDebug terse(QDebug dbg) +{ + return dbg.verbosity(QDebug::MinimumVerbosity); +} - inline qint64 profilerMinNsecs() - { - return +inline qint64 profilerMinNsecs() +{ + return #ifdef PROFILER_LOG_USECS - PROFILER_LOG_USECS + PROFILER_LOG_USECS #else - 200 + 200 #endif * 1000; - } +} +} // namespace 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); // NOLINT(performance-unnecessary-value-param) } -inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et) +inline QDebug operator<<(QDebug debug_object, QElapsedTimer et) { - auto val = et.nsecsElapsed() / 1000; - if (val < 1000) - debug_object << val << "µs"; - else - debug_object << val / 1000 << "ms"; + // NOLINTNEXTLINE(bugprone-integer-division) + debug_object << static_cast<double>(et.nsecsElapsed() / 1000) / 1000 + << "ms"; // Show in ms with 3 decimal digits precision return debug_object; } diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp new file mode 100644 index 00000000..ce833b98 --- /dev/null +++ b/lib/mxcreply.cpp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "mxcreply.h" + +#include <QtCore/QBuffer> +#include "accountregistry.h" +#include "room.h" + +#ifdef Quotient_E2EE_ENABLED +#include "events/filesourceinfo.h" +#endif + +using namespace Quotient; + +class MxcReply::Private +{ +public: + explicit Private(QNetworkReply* r = nullptr) + : m_reply(r) + {} + QNetworkReply* m_reply; + Omittable<EncryptedFileMetadata> m_encryptedFile; + QIODevice* m_device = nullptr; +}; + +MxcReply::MxcReply(QNetworkReply* reply) + : d(makeImpl<Private>(reply)) +{ + d->m_device = d->m_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(makeImpl<Private>(reply)) +{ + reply->setParent(this); + connect(d->m_reply, &QNetworkReply::finished, this, [this]() { + setError(d->m_reply->error(), d->m_reply->errorString()); + +#ifdef Quotient_E2EE_ENABLED + if(!d->m_encryptedFile.has_value()) { + d->m_device = d->m_reply; + } else { + auto buffer = new QBuffer(this); + buffer->setData( + decryptFile(d->m_reply->readAll(), *d->m_encryptedFile)); + buffer->open(ReadOnly); + d->m_device = buffer; + } +#else + d->m_device = d->m_reply; +#endif + setOpenMode(ReadOnly); + emit finished(); + }); + +#ifdef Quotient_E2EE_ENABLED + auto eventIt = room->findInTimeline(eventId); + if(eventIt != room->historyEdge()) { + if (auto event = eventIt->viewAs<RoomMessageEvent>()) { + if (auto* efm = std::get_if<EncryptedFileMetadata>( + &event->content()->fileInfo()->source)) + d->m_encryptedFile = *efm; + } + } +#endif +} + +MxcReply::MxcReply() + : d(ZeroImpl<Private>()) +{ + 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 errorOccurred(QNetworkReply::ProtocolInvalidOperationError); + emit finished(); + }, Qt::QueuedConnection); +} + +qint64 MxcReply::readData(char *data, qint64 maxSize) +{ + return d->m_device->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..f6c4a34d --- /dev/null +++ b/lib/mxcreply.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "util.h" + +#include <QtNetwork/QNetworkReply> + +namespace Quotient { +class Room; + +class QUOTIENT_API MxcReply : public QNetworkReply +{ + Q_OBJECT +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; + ImplPtr<Private> d; +}; +} diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 89967a8a..44a306d1 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -1,42 +1,62 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "networkaccessmanager.h" -#include <QtNetwork/QNetworkReply> +#include "connection.h" +#include "room.h" +#include "accountregistry.h" +#include "mxcreply.h" + #include <QtCore/QCoreApplication> +#include <QtCore/QThread> +#include <QtCore/QSettings> +#include <QtNetwork/QNetworkReply> -using namespace QMatrixClient; +using namespace Quotient; -class NetworkAccessManager::Private -{ - public: - QList<QSslError> ignoredSslErrors; +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) : d(std::make_unique<Private>()) -{ } +NetworkAccessManager::NetworkAccessManager(QObject* parent) + : QNetworkAccessManager(parent), d(makeImpl<Private>(this)) +{} QList<QSslError> NetworkAccessManager::ignoredSslErrors() const { return d->ignoredSslErrors; } +void NetworkAccessManager::ignoreSslErrors(bool ignore) const +{ + if (ignore) { + connect(this, &QNetworkAccessManager::sslErrors, this, + [](QNetworkReply* reply) { reply->ignoreSslErrors(); }); + } else { + disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr); + } +} + void NetworkAccessManager::addIgnoredSslError(const QSslError& error) { d->ignoredSslErrors << error; @@ -47,29 +67,63 @@ void NetworkAccessManager::clearIgnoredSslErrors() d->ignoredSslErrors.clear(); } -static NetworkAccessManager* createNam() -{ - auto nam = new NetworkAccessManager(QCoreApplication::instance()); - // See #109. Once Qt bearer management gets better, this workaround - // should become unnecessary. - nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged, - [nam] { nam->setNetworkAccessible(QNetworkAccessManager::Accessible); }); - return nam; -} - NetworkAccessManager* NetworkAccessManager::instance() { - static auto* nam = createNam(); + thread_local auto* nam = [] { + auto* namInit = new NetworkAccessManager(); + connect(QThread::currentThread(), &QThread::finished, namInit, + &QObject::deleteLater); + return namInit; + }(); return nam; } -NetworkAccessManager::~NetworkAccessManager() = default; - -QNetworkReply* NetworkAccessManager::createRequest(Operation op, - const QNetworkRequest& request, QIODevice* outgoingData) +QNetworkReply* NetworkAccessManager::createRequest( + Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { - auto reply = - QNetworkAccessManager::createRequest(op, request, 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 = Accounts.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 ae847582..01b0599d 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -1,49 +1,35 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "util.h" + #include <QtNetwork/QNetworkAccessManager> -#include <memory> - -namespace QMatrixClient -{ - class NetworkAccessManager : public QNetworkAccessManager - { - Q_OBJECT - public: - NetworkAccessManager(QObject* parent = nullptr); - ~NetworkAccessManager() override; - - QList<QSslError> ignoredSslErrors() const; - void addIgnoredSslError(const QSslError& error); - void clearIgnoredSslErrors(); - - /** Get a pointer to the singleton */ - static NetworkAccessManager* instance(); - - private: - QNetworkReply * createRequest(Operation op, - const QNetworkRequest &request, - QIODevice *outgoingData = Q_NULLPTR) override; - - class Private; - std::unique_ptr<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { + +class QUOTIENT_API NetworkAccessManager : public QNetworkAccessManager { + Q_OBJECT +public: + NetworkAccessManager(QObject* parent = nullptr); + + QList<QSslError> ignoredSslErrors() const; + void addIgnoredSslError(const QSslError& error); + void clearIgnoredSslErrors(); + void ignoreSslErrors(bool ignore = true) const; + + /** 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; + + class Private; + ImplPtr<Private> d; +}; +} // namespace Quotient diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index 48bd09f3..06b1fdf9 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -1,24 +1,9 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "networksettings.h" -using namespace QMatrixClient; +using namespace Quotient; void NetworkSettings::setupApplicationProxy() const { @@ -26,6 +11,9 @@ void NetworkSettings::setupApplicationProxy() const { proxyType(), proxyHostName(), proxyPort() }); } -QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName) -QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) +QUO_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, + "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) +QUO_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", + {}, setProxyHostName) +QUO_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, + setProxyPort) diff --git a/lib/networksettings.h b/lib/networksettings.h index 83613060..44247e59 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -24,21 +9,20 @@ Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) -namespace QMatrixClient { - class NetworkSettings: public SettingsGroup - { - Q_OBJECT - QMC_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) - QMC_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) - QMC_DECLARE_SETTING(quint16, proxyPort, setProxyPort) - Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) - public: - template <typename... ArgTs> - explicit NetworkSettings(ArgTs... qsettingsArgs) - : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...) - { } - ~NetworkSettings() override = default; +namespace Quotient { +class QUOTIENT_API NetworkSettings : public SettingsGroup { + Q_OBJECT + QUO_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) + QUO_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) + QUO_DECLARE_SETTING(quint16, proxyPort, setProxyPort) + Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) +public: + template <typename... ArgTs> + explicit NetworkSettings(ArgTs... qsettingsArgs) + : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...) + {} + ~NetworkSettings() override = default; - Q_INVOKABLE void setupApplicationProxy() const; - }; -} + Q_INVOKABLE void setupApplicationProxy() const; +}; +} // namespace Quotient diff --git a/lib/omittable.h b/lib/omittable.h new file mode 100644 index 00000000..0718aaff --- /dev/null +++ b/lib/omittable.h @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <optional> +#include <functional> + +namespace Quotient { + +template <typename T> +class Omittable; + +constexpr auto none = std::nullopt; + +//! \brief Lift an operation into dereferenceable types (Omittables or pointers) +//! +//! This is a more generic version of Omittable::then() that extends to +//! an arbitrary number of arguments of any type that is dereferenceable (unary +//! operator*() can be applied to it) and (explicitly or implicitly) convertible +//! to bool. This allows to streamline checking for nullptr/none before applying +//! the operation on the underlying types. \p fn is only invoked if all \p args +//! are "truthy" (i.e. <tt>(... && bool(args)) == true</tt>). +//! \param fn A callable that should accept the types stored inside +//! Omittables/pointers passed in \p args +//! \return Always an Omittable: if \p fn returns another type, lift() wraps +//! it in an Omittable; if \p fn returns an Omittable, that return value +//! (or none) is returned as is. +template <typename FnT, typename... ArgTs> +inline auto lift(FnT&& fn, ArgTs&&... args) +{ + if constexpr (std::is_void_v<decltype(std::invoke(std::forward<FnT>(fn), + *args...))>) { + if ((... && bool(args))) + std::invoke(std::forward<FnT>(fn), *args...); + } else + return (... && bool(args)) + ? Omittable(std::invoke(std::forward<FnT>(fn), *args...)) + : none; +} + +/** `std::optional` with tweaks + * + * The tweaks are: + * - streamlined assignment (operator=)/emplace()ment of values that can be + * used to implicitly construct the underlying type, including + * direct-list-initialisation, e.g.: + * \code + * struct S { int a; char b; } + * Omittable<S> o; + * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } + * \endcode + * - entirely deleted value(). The technical reason is that Xcode 10 doesn't + * have it; but besides that, value_or() or (after explicit checking) + * `operator*()`/`operator->()` are better alternatives within Quotient + * that doesn't practice throwing exceptions (as doesn't most of Qt). + * - ensure() to provide a safer lvalue accessor instead of operator* or + * operator->. Allows chained initialisation of nested Omittables: + * \code + * struct Inner { int member = 10; Omittable<int> innermost; }; + * struct Outer { int anotherMember = 10; Omittable<Inner> inner; }; + * Omittable<Outer> o; // = { 10, std::nullopt }; + * o.ensure().inner.ensure().innermost.emplace(42); + * \endcode + * - merge() - a soft version of operator= that only overwrites its first + * operand with the second one if the second one is not empty. + * - then() and then_or() to streamline read-only interrogation in a "monadic" + * interface. + */ +template <typename T> +class Omittable : public std::optional<T> { +public: + using base_type = std::optional<T>; + using value_type = std::decay_t<T>; + + using std::optional<T>::optional; + + // Overload emplace() and operator=() to allow passing braced-init-lists + // (the standard emplace() does direct-initialisation but + // not direct-list-initialisation). + using base_type::operator=; + Omittable& operator=(const value_type& v) + { + base_type::operator=(v); + return *this; + } + Omittable& operator=(value_type&& v) + { + base_type::operator=(std::move(v)); + return *this; + } + + using base_type::emplace; + T& emplace(const T& val) { return base_type::emplace(val); } + T& emplace(T&& val) { return base_type::emplace(std::move(val)); } + + // Use value_or() or check (with operator! or has_value) before accessing + // with operator-> or operator* + // The technical reason is that Xcode 10 has incomplete std::optional + // that has no value(); but using value() may also mean that you rely + // on the optional throwing an exception (which is not an assumed practice + // throughout Quotient) or that you spend unnecessary CPU cycles on + // an extraneous has_value() check. + auto& value() = delete; + const auto& value() const = delete; + + template <typename U> + value_type& ensure(U&& defaultValue = value_type {}) + { + return this->has_value() ? this->operator*() + : this->emplace(std::forward<U>(defaultValue)); + } + value_type& ensure(const value_type& defaultValue) + { + return ensure<>(defaultValue); + } + value_type& ensure(value_type&& defaultValue) + { + return ensure<>(std::move(defaultValue)); + } + + //! Merge the value from another Omittable + //! \return true if \p other is not omitted and the value of + //! the current Omittable was different (or omitted), + //! in other words, if the current Omittable has changed; + //! false otherwise + template <typename T1> + auto merge(const std::optional<T1>& other) + -> std::enable_if_t<std::is_convertible_v<T1, T>, bool> + { + if (!other || (this->has_value() && **this == *other)) + return false; + this->emplace(*other); + return true; + } + + // The below is inspired by the proposed std::optional monadic operations + // (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0798r6.html). + + //! \brief Lift a callable into the Omittable + //! + //! 'Lifting', as used in functional programming, means here invoking + //! a callable (e.g., a function) on the contents of the Omittable if it has + //! any and wrapping the returned value (that may be of a different type T2) + //! into a new Omittable\<T2>. If the current Omittable is empty, + //! the invocation is skipped altogether and Omittable\<T2>{none} is + //! returned instead. + //! \note if \p fn already returns an Omittable (i.e., it is a 'functor', + //! in functional programming terms), then() will not wrap another + //! Omittable around but will just return what \p fn returns. The + //! same doesn't hold for the parameter: if \p fn accepts an Omittable + //! you have to wrap it in another Omittable before calling then(). + //! \return `none` if the current Omittable has `none`; + //! otherwise, the Omittable returned from a call to \p fn + //! \tparam FnT a callable with \p T (or <tt>const T&</tt>) + //! returning Omittable<T2>, T2 is any supported type + //! \sa then_or, transform + template <typename FnT> + auto then(FnT&& fn) const + { + return lift(std::forward<FnT>(fn), *this); + } + + //! \brief Lift a callable into the rvalue Omittable + //! + //! This is an rvalue overload for then(). + template <typename FnT> + auto then(FnT&& fn) + { + return lift(std::forward<FnT>(fn), *this); + } + + //! \brief Lift a callable into the const lvalue Omittable, with a fallback + //! + //! This effectively does the same what then() does, except that it returns + //! a value of type returned by the callable, or the provided fallback value + //! if the current Omittable is empty. This is a typesafe version to apply + //! an operation on an Omittable without having to deal with another + //! Omittable afterwards. + template <typename FnT, typename FallbackT> + auto then_or(FnT&& fn, FallbackT&& fallback) const + { + return then(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + + //! \brief Lift a callable into the rvalue Omittable, with a fallback + //! + //! This is an overload for functions that accept rvalue + template <typename FnT, typename FallbackT> + auto then_or(FnT&& fn, FallbackT&& fallback) + { + return then(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } +}; + +template <typename T> +Omittable(T&&) -> Omittable<T>; + +//! \brief Merge the value from an optional +//! This is an adaptation of Omittable::merge() to the case when the value +//! on the left hand side is not an Omittable. +//! \return true if \p rhs is not omitted and the \p lhs value was different, +//! in other words, if \p lhs has changed; +//! false otherwise +template <typename T1, typename T2> +inline auto merge(T1& lhs, const std::optional<T2>& rhs) + -> std::enable_if_t<std::is_assignable_v<T1&, const T2&>, bool> +{ + if (!rhs || lhs == *rhs) + return false; + lhs = *rhs; + return true; +} + +} // namespace Quotient diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h new file mode 100644 index 00000000..ef7f6f80 --- /dev/null +++ b/lib/qt_connection_util.h @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "function_traits.h" + +#include <QtCore/QPointer> + +namespace Quotient { +namespace _impl { + enum ConnectionType { SingleShot, Until }; + + template <ConnectionType CType> + inline auto connect(auto* sender, auto signal, auto* context, auto slotLike, + Qt::ConnectionType connType) + { + auto pConn = std::make_unique<QMetaObject::Connection>(); + auto& c = *pConn; // Save the reference before pConn is moved from + c = QObject::connect( + sender, signal, context, + [slotLike, pConn = std::move(pConn)](const auto&... args) + // The requires-expression below is necessary to prevent Qt + // from eagerly trying to fill the lambda with more arguments + // than slotLike() (i.e., the original slot) can handle + requires requires { slotLike(args...); } { + static_assert(CType == Until || CType == SingleShot, + "Unsupported disconnection type"); + if constexpr (CType == SingleShot) { + // Disconnect early to avoid re-triggers during slotLike() + QObject::disconnect(*pConn); + // Qt kindly keeps slot objects until they do their job, + // even if they disconnect themselves in the process (see + // how doActivate() in qobject.cpp handles c->slotObj). + slotLike(args...); + } else if constexpr (CType == Until) { + if (slotLike(args...)) + QObject::disconnect(*pConn); + } + }, + connType); + return c; + } + + template <typename SlotT, typename ReceiverT> + concept PmfSlot = + (fn_arg_count_v<SlotT> > 0 + && std::is_base_of_v<std::decay_t<fn_arg_t<SlotT, 0>>, ReceiverT>); +} // namespace _impl + +//! \brief Create a connection that self-disconnects when its slot returns true +//! +//! A slot accepted by connectUntil() is different from classic Qt slots +//! in that its return value must be bool, not void. Because of that different +//! signature connectUntil() doesn't accept member functions in the way +//! QObject::connect or Quotient::connectSingleShot do; you should pass a lambda +//! or a pre-bound member function to it. +//! \return whether the connection should be dropped; false means that the +//! connection remains; upon returning true, the slot is disconnected +//! from the signal. +inline auto connectUntil(auto* sender, auto signal, auto* context, + auto smartSlot, + Qt::ConnectionType connType = Qt::AutoConnection) +{ + return _impl::connect<_impl::Until>(sender, signal, context, smartSlot, + connType); +} + +//! Create a connection that self-disconnects after triggering on the signal +template <typename ContextT, typename SlotT> +inline auto connectSingleShot(auto* sender, auto signal, ContextT* context, + SlotT slot, + Qt::ConnectionType connType = Qt::AutoConnection) +{ +#if QT_VERSION_MAJOR >= 6 + return QObject::connect(sender, signal, context, slot, + Qt::ConnectionType(connType + | Qt::SingleShotConnection)); +#else + // In case of classic Qt pointer-to-member-function slots the receiver + // object has to be pre-bound to the slot to make it self-contained + if constexpr (_impl::PmfSlot<SlotT, ContextT>) { + auto&& boundSlot = +# if __cpp_lib_bind_front // Needs Apple Clang 13 (other platforms are fine) + std::bind_front(slot, context); +# else + [context, slot](const auto&... args) + requires requires { (context->*slot)(args...); } + { + (context->*slot)(args...); + }; +# endif + return _impl::connect<_impl::SingleShot>( + sender, signal, context, + std::forward<decltype(boundSlot)>(boundSlot), connType); + } else { + return _impl::connect<_impl::SingleShot>(sender, signal, context, slot, + connType); + } +#endif +} + +/*! \brief A guard pointer that disconnects an interested object upon destruction + * + * It's almost QPointer<> except that you have to initialise it with one + * more additional parameter - a pointer to a QObject that will be + * disconnected from signals of the underlying pointer upon the guard's + * destruction. Note that destructing the guide doesn't destruct either QObject. + */ +template <typename T> +class ConnectionsGuard : public QPointer<T> { +public: + ConnectionsGuard(T* publisher, QObject* subscriber) + : QPointer<T>(publisher), subscriber(subscriber) + {} + ~ConnectionsGuard() + { + if (*this) + (*this)->disconnect(subscriber); + } + ConnectionsGuard(ConnectionsGuard&&) = default; + ConnectionsGuard& operator=(ConnectionsGuard&&) = default; + Q_DISABLE_COPY(ConnectionsGuard) + using QPointer<T>::operator=; + +private: + QObject* subscriber; +}; +} // namespace Quotient diff --git a/lib/quotient_common.h b/lib/quotient_common.h new file mode 100644 index 00000000..7fec9274 --- /dev/null +++ b/lib/quotient_common.h @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "quotient_export.h" + +#include <qobjectdefs.h> + +#include <array> + + +//! \brief Quotient replacement for the Q_FLAG/Q_DECLARE_FLAGS combination +//! +//! Although the comment in QTBUG-82295 says that Q_FLAG[_NS] "should" be +//! applied to the enum type only, Qt then doesn't allow to wrap the +//! corresponding flag type (defined with Q_DECLARE_FLAGS) into a QVariant. +//! This macro defines Q_FLAG and on top of that adds Q_ENUM_IMPL which is +//! a part of Q_ENUM() macro that enables the metatype data but goes under +//! the moc radar to avoid double registration of the same data in the map +//! defined in moc_*.cpp. +//! +//! Simply put, instead of using Q_FLAG/Q_DECLARE_FLAGS combo (and struggling +//! to figure out what you should pass to Q_FLAG if you want to make it +//! wrappable in a QVariant) use the macro below, and things will just work. +//! +//! \sa https://bugreports.qt.io/browse/QTBUG-82295 +#define QUO_DECLARE_FLAGS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_IMPL(Enum) \ + Q_FLAG(Flags) + +//! \brief Quotient replacement for the Q_FLAG_NS/Q_DECLARE_FLAGS combination +//! +//! This is the equivalent of QUO_DECLARE_FLAGS for enums declared at the +//! namespace level (be sure to provide Q_NAMESPACE _in the same file_ +//! as the enum definition and this macro). +//! \sa QUO_DECLARE_FLAGS +#define QUO_DECLARE_FLAGS_NS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_NS_IMPL(Enum) \ + Q_FLAG_NS(Flags) + +namespace Quotient { +Q_NAMESPACE_EXPORT(QUOTIENT_API) + +// 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 : uint16_t { + // 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 std::array MembershipStrings { + // The order MUST be the same as the order in the Membership 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) + +[[maybe_unused]] constexpr std::array JoinStateStrings { + 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 : int8_t { + StillResolving = -1, + UriResolved = 0, + CouldNotResolve, + IncorrectAction, + InvalidUri, + NoAccount +}; +Q_ENUM_NS(UriResolveResult) + +enum class RoomType : uint8_t { + Space = 0, + Undefined = 0xFF, +}; +Q_ENUM_NS(RoomType) + +[[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" }; + +enum class EncryptionType : uint8_t { + MegolmV1AesSha2 = 0, + Undefined = 0xFF, +}; +Q_ENUM_NS(EncryptionType) + +} // namespace Quotient +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) diff --git a/lib/quotient_export.h b/lib/quotient_export.h new file mode 100644 index 00000000..56767443 --- /dev/null +++ b/lib/quotient_export.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QtCore/qglobal.h> + +#ifdef QUOTIENT_STATIC +# define QUOTIENT_API +# define QUOTIENT_HIDDEN +#else +# ifndef QUOTIENT_API +# ifdef BUILDING_SHARED_QUOTIENT + /* We are building this library */ +# define QUOTIENT_API Q_DECL_EXPORT +# else + /* We are using this library */ +# define QUOTIENT_API Q_DECL_IMPORT +# endif +# endif + +# ifndef QUOTIENT_HIDDEN +# define QUOTIENT_HIDDEN Q_DECL_HIDDEN +# endif +#endif diff --git a/lib/room.cpp b/lib/room.cpp index ea771f17..0cf818ce 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1,64 +1,77 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> +// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> +// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "room.h" -#include "csapi/kicking.h" -#include "csapi/inviting.h" +#include "avatar.h" +#include "connection.h" +#include "converters.h" +#include "syncdata.h" +#include "user.h" +#include "eventstats.h" +#include "roomstateview.h" +#include "qt_connection_util.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" +#include "csapi/inviting.h" +#include "csapi/kicking.h" #include "csapi/leaving.h" +#include "csapi/read_markers.h" #include "csapi/receipts.h" #include "csapi/redaction.h" -#include "csapi/account-data.h" -#include "csapi/message_pagination.h" -#include "csapi/room_state.h" #include "csapi/room_send.h" +#include "csapi/room_state.h" +#include "csapi/room_upgrades.h" +#include "csapi/rooms.h" #include "csapi/tags.h" -#include "events/simplestateevents.h" + +#include "events/callevents.h" +#include "events/encryptionevent.h" +#include "events/reactionevent.h" +#include "events/receiptevent.h" +#include "events/redactionevent.h" #include "events/roomavatarevent.h" +#include "events/roomcanonicalaliasevent.h" +#include "events/roomcreateevent.h" #include "events/roommemberevent.h" +#include "events/roompowerlevelsevent.h" +#include "events/roomtombstoneevent.h" +#include "events/simplestateevents.h" #include "events/typingevent.h" -#include "events/receiptevent.h" -#include "events/callinviteevent.h" -#include "events/callcandidatesevent.h" -#include "events/callanswerevent.h" -#include "events/callhangupevent.h" -#include "events/redactionevent.h" -#include "jobs/mediathumbnailjob.h" #include "jobs/downloadfilejob.h" -#include "jobs/postreadmarkersjob.h" -#include "avatar.h" -#include "connection.h" -#include "user.h" -#include "converters.h" +#include "jobs/mediathumbnailjob.h" +#include <QtCore/QDir> #include <QtCore/QHash> -#include <QtCore/QStringBuilder> // for efficient string concats (operator%) -#include <QtCore/QElapsedTimer> #include <QtCore/QPointer> -#include <QtCore/QDir> +#include <QtCore/QRegularExpression> +#include <QtCore/QStringBuilder> // for efficient string concats (operator%) #include <QtCore/QTemporaryFile> #include <array> -#include <functional> #include <cmath> +#include <functional> + +#ifdef Quotient_E2EE_ENABLED +#include "e2ee/e2ee.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolminboundsession.h" +#include "e2ee/qolmutility.h" +#include "database.h" +#endif // Quotient_E2EE_ENABLED -using namespace QMatrixClient; + +using namespace Quotient; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) @@ -67,194 +80,396 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -// A workaround for MSVC 2015 that fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" -#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) -# define WORKAROUND_EXTENDED_INITIALIZER_LIST -#endif - -class Room::Private -{ - public: - /** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */ - using members_map_t = QMultiHash<QString, User*>; - - Private(Connection* c, QString id_, JoinState initialJoinState) - : q(nullptr), connection(c), id(move(id_)) - , joinState(initialJoinState) - { } - - Room* q; - - // This updates the room displayname field (which is the way a room - // should be shown in the room list) It should be called whenever the - // list of members or the room name (m.room.name) or canonical alias change. - void updateDisplayname(); - - Connection* connection; - Timeline timeline; - PendingEvents unsyncedEvents; - QHash<QString, TimelineItem::index_t> eventsIndex; - QString id; - QStringList aliases; - QString canonicalAlias; - QString name; - QString displayname; - QString topic; - QString encryptionAlgorithm; - Avatar avatar; - JoinState joinState; - int highlightCount = 0; - int notificationCount = 0; - members_map_t membersMap; - QList<User*> usersTyping; - QMultiHash<QString, User*> eventIdReadUsers; - QList<User*> membersLeft; - int unreadMessages = 0; - bool displayed = false; - QString firstDisplayedEventId; - QString lastDisplayedEventId; - QHash<const User*, QString> lastReadEventIds; - QString serverReadMarker; - TagsMap tags; - std::unordered_map<QString, EventPtr> accountData; - QString prevBatch; - QPointer<GetRoomEventsJob> eventsHistoryJob; - - struct FileTransferPrivateInfo +class Room::Private { +public: + /// Map of user names to users + /** User names potentially duplicate, hence QMultiHash. */ + using members_map_t = QMultiHash<QString, User*>; + + Private(Connection* c, QString id_, JoinState initialJoinState) + : q(nullptr), connection(c), id(move(id_)), joinState(initialJoinState) + {} + + Room* q; + + Connection* connection; + QString id; + JoinState joinState; + RoomSummary summary = { none, 0, none }; + /// The state of the room at timeline position before-0 + /// \sa timelineBase + 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 syncEdge() + /// \sa syncEdge + RoomStateView currentState; + /// Servers with aliases for this room except the one of the local user + /// \sa Room::remoteAliases + QSet<QString> aliasServers; + + Timeline timeline; + PendingEvents unsyncedEvents; + QHash<QString, TimelineItem::index_t> eventsIndex; + // A map from evtId to a map of relation type to a vector of event + // pointers. Not using QMultiHash, because we want to quickly return + // a number of relations for a given event without enumerating them. + QHash<std::pair<QString, QString>, RelatedEvents> relations; + QString displayname; + Avatar avatar; + 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; + QHash<QString, QSet<QString>> eventIdReadUsers; + QList<User*> usersInvited; + QList<User*> membersLeft; + bool displayed = false; + QString firstDisplayedEventId; + QString lastDisplayedEventId; + QHash<QString, ReadReceipt> lastReadReceipts; + QString fullyReadUntilEventId; + TagsMap tags; + UnorderedMap<QString, EventPtr> accountData; + QString prevBatch; + QPointer<GetRoomEventsJob> eventsHistoryJob; + QPointer<GetMembersByRoomJob> allMembersJob; + // Map from megolm sessionId to set of eventIds + UnorderedMap<QString, QSet<QString>> undecryptedEvents; + + struct FileTransferPrivateInfo { + FileTransferPrivateInfo() = default; + FileTransferPrivateInfo(BaseJob* j, const QString& fileName, + bool isUploading = false) + : status(FileTransferInfo::Started) + , job(j) + , localFileInfo(fileName) + , isUpload(isUploading) + {} + + FileTransferInfo::Status status = FileTransferInfo::None; + QPointer<BaseJob> job = nullptr; + QFileInfo localFileInfo {}; + bool isUpload = false; + qint64 progress = 0; + qint64 total = -1; + + void update(qint64 p, qint64 t) { -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST - FileTransferPrivateInfo() = default; - FileTransferPrivateInfo(BaseJob* j, QString fileName) - : job(j), localFileInfo(fileName) - { } -#endif - QPointer<BaseJob> job = nullptr; - QFileInfo localFileInfo { }; - FileTransferInfo::Status status = FileTransferInfo::Started; - qint64 progress = 0; - qint64 total = -1; - - void update(qint64 p, qint64 t) - { - if (t == 0) - { - t = -1; - if (p == 0) - p = -1; - } - if (p != -1) - qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t - << "=" << llround(double(p) / t * 100) << "%"; - progress = p; total = t; + if (t == 0) { + t = -1; + if (p == 0) + p = -1; } - }; - void failedTransfer(const QString& tid, const QString& errorMessage = {}) - { - qCWarning(MAIN) << "File transfer failed for id" << tid; - if (!errorMessage.isEmpty()) - qCWarning(MAIN) << "Message:" << errorMessage; - fileTransfers[tid].status = FileTransferInfo::Failed; - emit q->fileTransferFailed(tid, errorMessage); + if (p != -1) + qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t + << "=" << llround(double(p) / t * 100) << "%"; + progress = p; + total = t; } - /// A map from event/txn ids to information about the long operation; - /// used for both download and upload operations - QHash<QString, FileTransferPrivateInfo> fileTransfers; + }; + void failedTransfer(const QString& tid, const QString& errorMessage = {}) + { + qCWarning(MAIN) << "File transfer failed for id" << tid; + if (!errorMessage.isEmpty()) + qCWarning(MAIN) << "Message:" << errorMessage; + fileTransfers[tid].status = FileTransferInfo::Failed; + emit q->fileTransferFailed(tid, errorMessage); + } + /// A map from event/txn ids to information about the long operation; + /// used for both download and upload operations + QHash<QString, FileTransferPrivateInfo> fileTransfers; - const RoomMessageEvent* getEventWithFile(const QString& eventId) const; - QString fileNameToDownload(const RoomMessageEvent* event) const; + const RoomMessageEvent* getEventWithFile(const QString& eventId) const; + QString fileNameToDownload(const RoomMessageEvent* event) const; - //void inviteUser(User* u); // We might get it at some point in time. - void insertMemberIntoMap(User* u); - void renameMember(User* u, QString oldName); - void removeMemberFromMap(const QString& username, User* u); + Changes setSummary(RoomSummary&& newSummary); - void getPreviousContent(int limit = 10); + // void inviteUser(User* u); // We might get it at some point in time. + void insertMemberIntoMap(User* u); + void removeMemberFromMap(User* u); - bool isEventNotable(const TimelineItem& ti) const - { - return !ti->isRedacted() && - ti->senderId() != connection->userId() && - is<RoomMessageEvent>(*ti); + // This updates the room displayname field (which is the way a room + // should be shown in the room list); called whenever the list of + // members, the room name (m.room.name) or canonical alias change. + void updateDisplayname(); + // This is used by updateDisplayname() but only calculates the new name + // without any updates. + QString calculateDisplayname() const; + + /// A point in the timeline corresponding to baseState + rev_iter_t timelineBase() const { return q->findInTimeline(-1); } + rev_iter_t historyEdge() const { return timeline.crend(); } + Timeline::const_iterator syncEdge() const { return timeline.cend(); } + + void getPreviousContent(int limit = 10, const QString &filter = {}); + + const StateEvent* getCurrentState(const StateEventKey& evtKey) const + { + const auto* evt = currentState.value(evtKey, nullptr); + if (!evt) { + if (stubbedState.find(evtKey) == stubbedState.end()) { + // In the absence of a real event, make a stub as-if an event + // with empty content has been received. Event classes should be + // prepared for empty/invalid/malicious content anyway. + stubbedState.emplace( + evtKey, loadEvent<StateEvent>(evtKey.first, 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); } + Q_ASSERT(evt->matrixType() == evtKey.first + && evt->stateKey() == evtKey.second); + return evt; + } - void addNewMessageEvents(RoomEvents&& events); - void addHistoricalMessageEvents(RoomEvents&& events); - - /** Move events into the timeline - * - * Insert events into the timeline, either new or historical. - * Pointers in the original container become empty, the ownership - * is passed to the timeline container. - * @param events - the range of events to be inserted - * @param placement - position and direction of insertion: Older for - * historical messages, Newer for new ones - */ - Timeline::difference_type moveEventsToTimeline(RoomEventsRange events, - EventsPlacement placement); - - /** - * Remove events from the passed container that are already in the timeline - */ - void dropDuplicateEvents(RoomEvents& events) const; - - void setLastReadEvent(User* u, QString eventId); - void updateUnreadCount(rev_iter_t from, rev_iter_t to); - void promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); - - void markMessagesAsRead(rev_iter_t upToMarker); - - QString sendEvent(RoomEventPtr&& event); - - template <typename EventT, typename... ArgTs> - QString sendEvent(ArgTs&&... eventArgs) - { - return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...)); + template <typename EventArrayT> + Changes updateStateFrom(EventArrayT&& events) + { + Changes changes {}; + if (!events.empty()) { + QElapsedTimer et; + et.start(); + for (auto&& eptr : events) { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + 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) + << "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. + * Pointers in the original container become empty, the ownership + * is passed to the timeline container. + * @param events - the range of events to be inserted + * @param placement - position and direction of insertion: Older for + * historical messages, Newer for new ones + */ + Timeline::size_type moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement); + + /** + * Remove events from the passed container that are already in the timeline + */ + void dropDuplicateEvents(RoomEvents& events) const; + void decryptIncomingEvents(RoomEvents& events); + + //! \brief update last receipt record for a given user + //! + //! \return previous event id of the receipt if the new receipt changed + //! it, or `none` if no change took place + Omittable<QString> setLastReadReceipt(const QString& userId, rev_iter_t newMarker, + ReadReceipt newReceipt = {}); + Changes setLocalLastReadReceipt(const 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(); + + QString sendEvent(RoomEventPtr&& event); + + template <typename EventT, typename... ArgTs> + QString sendEvent(ArgTs&&... eventArgs) + { + return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...)); + } - QString doSendEvent(const RoomEvent* pEvent); - PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); - void onEventSendingFailure(const RoomEvent* pEvent, - const QString& txnId, BaseJob* call = nullptr); + QString doPostFile(RoomEventPtr &&msgEvent, const QUrl &localUrl); - template <typename EvT> - auto requestSetState(const QString& stateKey, const EvT& event) - { - // TODO: Queue up state events sending (see #133). - return connection->callApi<SetRoomStateWithKeyJob>( - id, EvT::matrixTypeId(), stateKey, event.contentJson()); - } + RoomEvent* addAsPending(RoomEventPtr&& event); - template <typename EvT> - auto requestSetState(const EvT& event) - { - return connection->callApi<SetRoomStateJob>( - id, EvT::matrixTypeId(), event.contentJson()); + QString doSendEvent(const RoomEvent* pEvent); + void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); + + SetRoomStateWithKeyJob* requestSetState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) + { + // if (event.roomId().isEmpty()) + // event.setRoomId(id); + // if (event.senderId().isEmpty()) + // event.setSender(connection->userId()); + // TODO: Queue up state events sending (see #133). + // TODO: Maybe addAsPending() as well, despite having no txnId + return connection->callApi<SetRoomStateWithKeyJob>(id, evtType, stateKey, + contentJson); + } + + /*! Apply redaction to the timeline + * + * Tries to find an event in the timeline and redact it; deletes the + * redaction event whether the redacted event was found or not. + * \return true if the event has been found and redacted; false otherwise + */ + bool processRedaction(const RedactionEvent& redaction); + + /*! Apply a new revision of the event to the timeline + * + * Tries to find an event in the timeline and replace it with the new + * content passed in \p newMessage. + * \return true if the event has been found and replaced; false otherwise + */ + bool processReplacement(const RoomMessageEvent& newEvent); + + void setTags(TagsMap&& newTags); + + QJsonObject toJson() const; + + bool isLocalUser(const User* u) const { return u == q->localUser(); } + +#ifdef Quotient_E2EE_ENABLED + UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions; + int currentMegolmSessionMessageCount = 0; + //TODO save this to database + unsigned long long currentMegolmSessionCreationTimestamp = 0; + QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr; + + bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, + const QString& senderId, + const QString& olmSessionId) + { + if (groupSessions.contains(sessionId)) { + qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists"; + return false; } - /** - * @brief Apply redaction to the timeline - * - * Tries to find an event in the timeline and redact it; deletes the - * redaction event whether the redacted event was found or not. - */ - bool processRedaction(const RedactionEvent& redaction); + auto expectedMegolmSession = QOlmInboundGroupSession::create(sessionKey); + Q_ASSERT(expectedMegolmSession.has_value()); + auto&& megolmSession = *expectedMegolmSession; + if (megolmSession->sessionId() != sessionId) { + qCWarning(E2EE) << "Session ID mismatch in m.room_key event"; + return false; + } + megolmSession->setSenderId(senderId); + megolmSession->setOlmSessionId(olmSessionId); + qCWarning(E2EE) << "Adding inbound session"; + connection->saveMegolmSession(q, *megolmSession); + groupSessions[sessionId] = std::move(megolmSession); + return true; + } - void setTags(TagsMap newTags); + QString groupSessionDecryptMessage(QByteArray cipher, + const QString& sessionId, + const QString& eventId, + QDateTime timestamp, + const QString& senderId) + { + auto groupSessionIt = groupSessions.find(sessionId); + if (groupSessionIt == groupSessions.end()) { + // qCWarning(E2EE) << "Unable to decrypt event" << eventId + // << "The sender's device has not sent us the keys for " + // "this message"; + return {}; + } + auto& senderSession = groupSessionIt->second; + if (senderSession->senderId() != senderId) { + qCWarning(E2EE) << "Sender from event does not match sender from session"; + return {}; + } + auto decryptResult = senderSession->decrypt(cipher); + if(!decryptResult) { + qCWarning(E2EE) << "Unable to decrypt event" << eventId + << "with matching megolm session:" << decryptResult.error(); + return {}; + } + const auto& [content, index] = *decryptResult; + const auto& [recordEventId, ts] = + q->connection()->database()->groupSessionIndexRecord( + q->id(), senderSession->sessionId(), index); + if (recordEventId.isEmpty()) { + q->connection()->database()->addGroupSessionIndexRecord( + q->id(), senderSession->sessionId(), index, eventId, + timestamp.toMSecsSinceEpoch()); + } else { + if ((eventId != recordEventId) + || (ts != timestamp.toMSecsSinceEpoch())) { + qCWarning(E2EE) << "Detected a replay attack on event" << eventId; + return {}; + } + } + return content; + } - QJsonObject toJson() const; + bool shouldRotateMegolmSession() const + { + const auto* encryptionConfig = currentState.get<EncryptionEvent>(); + if (!encryptionConfig || !encryptionConfig->useEncryption()) + return false; - private: - QString calculateDisplayname() const; - QString roomNameFromMemberNames(const QList<User*>& userlist) const; + const auto rotationInterval = encryptionConfig->rotationPeriodMs(); + const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs(); + return currentOutboundMegolmSession->messageCount() + >= rotationMessageCount + || currentOutboundMegolmSession->creationTime().addMSecs( + rotationInterval) + < QDateTime::currentDateTime(); + } - bool isLocalUser(const User* u) const - { - return u == q->localUser(); + bool hasValidMegolmSession() const + { + if (!q->usesEncryption()) { + return false; } + return currentOutboundMegolmSession != nullptr; + } + + void createMegolmSession() { + qCDebug(E2EE) << "Creating new outbound megolm session for room " + << q->objectName(); + currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); + + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), + currentOutboundMegolmSession->sessionKey(), + q->localUser()->id(), "SELF"_ls); + } + + QMultiHash<QString, QString> getDevicesWithoutKey() const + { + QMultiHash<QString, QString> devices; + for (const auto& user : q->users()) + for (const auto& deviceId : connection->devicesForUser(user->id())) + devices.insert(user->id(), deviceId); + + return connection->database()->devicesWithoutKey( + id, devices, currentOutboundMegolmSession->sessionId()); + } +#endif // Quotient_E2EE_ENABLED + +private: + using users_shortlist_t = std::array<User*, 3>; + template <typename ContT> + users_shortlist_t buildShortlist(const ContT& users) const; + users_shortlist_t buildShortlist(const QStringList& userIds) const; }; +decltype(Room::Private::baseState) Room::Private::stubbedState {}; + Room::Room(Connection* connection, QString id, JoinState initialJoinState) : QObject(connection), d(new Private(connection, id, initialJoinState)) { @@ -262,83 +477,175 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; - connect(this, &Room::userAdded, this, &Room::memberListChanged); - connect(this, &Room::userRemoved, this, &Room::memberListChanged); - connect(this, &Room::memberRenamed, this, &Room::memberListChanged); - qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; + d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name +#ifdef Quotient_E2EE_ENABLED + connectSingleShot(this, &Room::encryption, this, [this, connection](){ + connection->encryptionUpdate(this); + }); + connect(this, &Room::userAdded, this, [this, connection](){ + if(usesEncryption()) { + connection->encryptionUpdate(this); + } + }); + d->groupSessions = connection->loadRoomMegolmSessions(this); + d->currentOutboundMegolmSession = + connection->loadCurrentOutboundMegolmSession(this->id()); + if (d->shouldRotateMegolmSession()) { + d->currentOutboundMegolmSession = nullptr; + } + connect(this, &Room::userRemoved, this, [this](){ + if (!usesEncryption()) { + return; + } + if (d->hasValidMegolmSession()) { + d->createMegolmSession(); + } + qCDebug(E2EE) << "Invalidating current megolm session because user left"; + + }); + + connect(this, &Room::beforeDestruction, this, [=](){ + connection->database()->clearRoomData(id); + }); +#endif + qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } -Room::~Room() +Room::~Room() { delete d; } + +const QString& Room::id() const { return d->id; } + +QString Room::version() const { - delete d; + const auto v = currentState().query(&RoomCreateEvent::version); + return v && !v->isEmpty() ? *v : QStringLiteral("1"); } -const QString& Room::id() const +bool Room::isUnstable() const { - return d->id; + return !connection()->loadingCapabilities() + && !connection()->stableRoomVersions().contains(version()); } -const Room::Timeline& Room::messageEvents() const +QString Room::predecessorId() const { - return d->timeline; + if (const auto* evt = currentState().get<RoomCreateEvent>()) + return evt->predecessor().roomId; + + return {}; } +Room* Room::predecessor(JoinStates statesFilter) const +{ + if (const auto& predId = predecessorId(); !predId.isEmpty()) + if (auto* r = connection()->room(predId, statesFilter); + r && r->successorId() == id()) + return r; + + return nullptr; +} + +QString Room::successorId() const +{ + return currentState().queryOr(&RoomTombstoneEvent::successorRoomId, + QString()); +} + +Room* Room::successor(JoinStates statesFilter) const +{ + if (const auto& succId = successorId(); !succId.isEmpty()) + if (auto* r = connection()->room(succId, statesFilter); + r && r->predecessorId() == id()) + return r; + + return nullptr; +} + +const Room::Timeline& Room::messageEvents() const { return d->timeline; } + const Room::PendingEvents& Room::pendingEvents() const { return d->unsyncedEvents; } +bool Room::allHistoryLoaded() const +{ + return !d->timeline.empty() && is<RoomCreateEvent>(*d->timeline.front()); +} + QString Room::name() const { - return d->name; + return currentState().content<RoomNameEvent>().value; } QStringList Room::aliases() const { - return d->aliases; + if (const auto* evt = currentState().get<RoomCanonicalAliasEvent>()) { + auto result = evt->altAliases(); + if (!evt->alias().isEmpty()) + result << evt->alias(); + return result; + } + return {}; } -QString Room::canonicalAlias() const +QStringList Room::altAliases() const { - return d->canonicalAlias; + return currentState().content<RoomCanonicalAliasEvent>().altAliases; } -QString Room::displayName() const +QString Room::canonicalAlias() const { - return d->displayname; + return currentState().queryOr(&RoomCanonicalAliasEvent::alias, QString()); } -QString Room::topic() const -{ - return d->topic; +QString Room::displayName() const { return d->displayname; } + +QStringList Room::pinnedEventIds() const { + return currentState().queryOr(&RoomPinnedEvent::pinnedEvents, QStringList()); } -QString Room::avatarMediaId() const +QVector<const Quotient::RoomEvent*> Quotient::Room::pinnedEvents() const { - return d->avatar.mediaId(); + QVector<const RoomEvent*> pinnedEvents; + for (const auto& evtId : pinnedEventIds()) + if (const auto& it = findInTimeline(evtId); it != historyEdge()) + pinnedEvents.append(it->event()); + + return pinnedEvents; } -QUrl Room::avatarUrl() const +QString Room::displayNameForHtml() const { - return d->avatar.url(); + return displayName().toHtmlEscaped(); } -QImage Room::avatar(int dimension) +void Room::refreshDisplayName() { d->updateDisplayname(); } + +QString Room::topic() const { - return avatar(dimension, dimension); + return currentState().queryOr(&RoomTopicEvent::topic, QString()); } +QString Room::avatarMediaId() const { return d->avatar.mediaId(); } + +QUrl Room::avatarUrl() const { return d->avatar.url(); } + +const Avatar& Room::avatarObject() const { return d->avatar; } + +QImage Room::avatar(int dimension) { return avatar(dimension, dimension); } + 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) + 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 {}; } @@ -350,176 +657,356 @@ User* Room::user(const QString& userId) const JoinState Room::memberJoinState(User* user) const { - return - d->membersMap.contains(user->name(this), user) ? JoinState::Join : - JoinState::Leave; + return d->membersMap.contains(user->name(this), user) ? JoinState::Join + : JoinState::Leave; } -JoinState Room::joinState() const +Membership Room::memberState(const QString& userId) const { - return d->joinState; + return currentState().queryOr(userId, &RoomMemberEvent::membership, + Membership::Leave); } +bool Room::isMember(const QString& userId) const +{ + return memberState(userId) == Membership::Join; +} + +JoinState Room::joinState() const { return d->joinState; } + void Room::setJoinState(JoinState state) { JoinState oldState = d->joinState; - if( state == oldState ) + if (state == oldState) return; d->joinState = state; - qCDebug(MAIN) << "Room" << id() << "changed state: " - << int(oldState) << "->" << int(state); + qCDebug(STATE) << "Room" << id() << "changed state: " << terse << oldState + << "->" << state; emit joinStateChanged(oldState, state); } -void Room::Private::setLastReadEvent(User* u, QString eventId) -{ - auto& storedId = lastReadEventIds[u]; - if (storedId == eventId) - return; - 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<PostReadMarkersJob>(id, storedId); - emit q->readMarkerMoved(eventId, storedId); +Omittable<QString> Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, + ReadReceipt newReceipt) +{ + 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); + qDebug(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. + // This logic tackles, in particular, the case when the new 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; in that case, + // the previous marker is kept because read receipts are not supposed + // to move backwards. If neither new nor old event is found, the new receipt + // is blindly stored, in a hope it's also "newer" in the timeline. + // NB: with reverse iterators, timeline history edge >= sync edge + if (prevEventId == newReceipt.eventId + || newMarker > q->findInTimeline(prevEventId)) + return {}; -void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) -{ - Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); - Q_ASSERT(to >= from && to <= timeline.crend()); + // Finally make the change - // 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. - const auto readMarker = q->readMarker(); - if (readMarker >= from && readMarker < to) - { - promoteReadMarker(q->localUser(), readMarker, true); - return; + 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); - 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/QMatrixClient/libqmatrixclient/wiki/unread_count - if (unreadMessages < 0) - unreadMessages = 0; + auto dbg = qDebug(EPHEMERAL); // NB: qCDebug can't be used like that + dbg << "The new read receipt for" << userId << "is now at"; + if (newMarker == historyEdge()) + dbg << storedReceipt.eventId; + else + dbg << *newMarker; + } - unreadMessages += newUnreadMessages; - qCDebug(MAIN) << "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); + // NB: This method, unlike setLocalLastReadReceipt, doesn't emit + // lastReadEventChanged() to avoid numerous emissions when many read + // receipts arrive. It can be called thousands of times during an initial + // sync, e.g. + // TODO: remove in 0.8 + if (const auto member = q->user(userId); !isLocalUser(member)) + QT_IGNORE_DEPRECATIONS(emit q->readMarkerForUserMoved( + member, prevEventId, storedReceipt.eventId);) + return prevEventId; +} + +Room::Changes Room::Private::setLocalLastReadReceipt(const rev_iter_t& newMarker, + ReadReceipt newReceipt, + bool deferStatsUpdate) +{ + auto prevEventId = + setLastReadReceipt(connection->userId(), newMarker, move(newReceipt)); + if (!prevEventId) + return Change::None; + Changes changes = Change::Other; + if (!deferStatsUpdate) { + if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(*prevEventId), + newMarker)) { + qDebug(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 } + emit q->lastReadEventChanged({ connection->userId() }); + return changes; } -void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) +Room::Changes Room::Private::updateStats(const rev_iter_t& from, + const rev_iter_t& to) { - 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; - - Q_ASSERT(newMarker < timeline.crend()); + Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); + Q_ASSERT(to >= from && to <= 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(); }); + 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 + && setLocalLastReadReceipt(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(); + } - setLastReadEvent(u, (*(eagerMarker - 1))->id()); - if (isLocalUser(u)) - { - const auto oldUnreadCount = unreadMessages; - QElapsedTimer et; et.start(); - unreadMessages = count_if(eagerMarker, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1)); - if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Recounting unread messages took" << et; + 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; + } + } - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count - if (unreadMessages == 0) - unreadMessages = -1; + // 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); + + const auto newStats = EventStats::fromRange(q, from, to); + Q_ASSERT(!newStats.isEstimate); + if (newStats.empty()) + return changes; + + 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(MAIN) << "Room" << displayname - << "has no more unread messages"; - } else - qCDebug(MAIN) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); + 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::setFullyReadMarker(const QString& eventId) +{ + if (fullyReadUntilEventId == eventId) + return Change::None; + + 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 |= setLocalLastReadReceipt(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 + QT_IGNORE_DEPRECATIONS( + emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);) + return changes; } -void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +void Room::setReadReceipt(const QString& atEventId) { - const auto prevMarker = q->readMarker(); - promoteReadMarker(q->localUser(), upToMarker); - if (prevMarker != upToMarker) - qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); - - // 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>(id, "m.read", - (*upToMarker)->id()); - break; - } - } + if (const auto changes = + d->setLocalLastReadReceipt(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(QString uptoEventId) +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::hasUnreadMessages() const +bool Room::canSwitchVersions() const { - return unreadCount() >= 0; + if (!successorId().isEmpty()) + return false; // No one can upgrade a room that's already upgraded + + if (const auto* plEvt = currentState().get<RoomPowerLevelsEvent>()) { + const auto currentUserLevel = + plEvt->powerLevelForUser(localUser()->id()); + const auto tombstonePowerLevel = + plEvt->powerLevelForState("m.room.tombstone"_ls); + return currentUserLevel >= tombstonePowerLevel; + } + return true; +} + +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(); } -int Room::unreadCount() const +Notification Room::notificationFor(const TimelineItem &ti) const { - return d->unreadMessages; + return d->notifications.value(ti->id()); } -Room::rev_iter_t Room::timelineEdge() const +Notification Room::checkForNotifications(const TimelineItem &ti) { - return d->timeline.crend(); + return { Notification::None }; } +bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); } + +int countFromStats(const EventStats& s) +{ + return s.empty() ? -1 : int(s.notableCount); +} + +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 { return d->timeline.empty() ? 0 : d->timeline.front().index(); @@ -532,33 +1019,91 @@ TimelineItem::index_t Room::maxTimelineIndex() const bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const { - return !d->timeline.empty() && - timelineIndex >= minTimelineIndex() && - timelineIndex <= maxTimelineIndex(); + return !d->timeline.empty() && timelineIndex >= minTimelineIndex() + && timelineIndex <= maxTimelineIndex(); } Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const { - return timelineEdge() - - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); + return historyEdge() + - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); } Room::rev_iter_t Room::findInTimeline(const QString& evtId) const { - if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) - { + if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) { auto it = findInTimeline(d->eventsIndex.value(evtId)); - Q_ASSERT((*it)->id() == evtId); + Q_ASSERT(it != historyEdge() && (*it)->id() == evtId); return it; } - return timelineEdge(); + return historyEdge(); +} + +Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) +{ + return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), + [txnId](const auto& item) { + return item->transactionId() == txnId; + }); +} + +Room::PendingEvents::const_iterator +Room::findPendingEvent(const QString& txnId) const +{ + return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(), + [txnId](const auto& item) { + return item->transactionId() == txnId; + }); +} + +const Room::RelatedEvents Room::relatedEvents( + const QString& evtId, EventRelation::reltypeid_t relType) const +{ + return d->relations.value({ evtId, relType }); +} + +const Room::RelatedEvents Room::relatedEvents( + const RoomEvent& evt, EventRelation::reltypeid_t relType) const +{ + return relatedEvents(evt.id(), relType); +} + +const RoomCreateEvent* Room::creation() const +{ + return currentState().get<RoomCreateEvent>(); } -bool Room::displayed() const +const RoomTombstoneEvent *Room::tombstone() const { - return d->displayed; + return currentState().get<RoomTombstoneEvent>(); } +void Room::Private::getAllMembers() +{ + // If already loaded or already loading, there's nothing to do here. + if (q->joinedCount() <= membersMap.size() || isJobPending(allMembersJob)) + return; + + allMembersJob = connection->callApi<GetMembersByRoomJob>( + id, connection->nextBatchToken(), "join"); + auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; + 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 != syncEdge(); ++it) + if (is<RoomMemberEvent>(**it)) + roomChanges |= q->processStateEvent(**it); + postprocessChanges(roomChanges); + emit q->allMembersLoaded(); + }); +} + +bool Room::displayed() const { return d->displayed; } + void Room::setDisplayed(bool displayed) { if (d->displayed == displayed) @@ -566,17 +1111,11 @@ 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; -} +QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; } Room::rev_iter_t Room::firstDisplayedMarker() const { @@ -588,6 +1127,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(); } @@ -598,10 +1142,7 @@ void Room::setFirstDisplayedEvent(TimelineItem::index_t index) setFirstDisplayedEventId(findInTimeline(index)->event()->id()); } -QString Room::lastDisplayedEventId() const -{ - return d->lastDisplayedEventId; -} +QString Room::lastDisplayedEventId() const { return d->lastDisplayedEventId; } Room::rev_iter_t Room::lastDisplayedMarker() const { @@ -613,6 +1154,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(); } @@ -626,47 +1173,84 @@ 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 +Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); } + +QString Room::readMarkerEventId() const { return lastFullyReadEventId(); } + +ReadReceipt Room::lastReadReceipt(const QString& userId) const { - return readMarker(localUser()); + 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) { - return d->eventIdReadUsers.values(eventId); +Room::rev_iter_t Room::localReadReceiptMarker() const +{ + return findInTimeline(lastLocalReadReceipt().eventId); } -int Room::notificationCount() const +QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } + +Room::rev_iter_t Room::fullyReadMarker() const { - return d->notificationCount; + return findInTimeline(d->fullyReadUntilEventId); } -void Room::resetNotificationCount() +QSet<QString> Room::userIdsAtEvent(const QString& eventId) { - if( d->notificationCount == 0 ) - return; - d->notificationCount = 0; - emit notificationCountChanged(this); + 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; } -int Room::highlightCount() const +qsizetype Room::notificationCount() const { - return d->highlightCount; + return d->unreadStats.notableCount; } +void Room::resetNotificationCount() +{ + if (d->unreadStats.notableCount == 0) + return; + d->unreadStats.notableCount = 0; + emit notificationCountChanged(); +} + +qsizetype Room::highlightCount() const { return d->serverHighlightCount; } + void Room::resetHighlightCount() { - if( d->highlightCount == 0 ) + if (d->serverHighlightCount == 0) return; - d->highlightCount = 0; - emit highlightCountChanged(this); + d->serverHighlightCount = 0; + emit highlightCountChanged(); +} + +void Room::switchVersion(QString newVersion) +{ + if (!successorId().isEmpty()) { + Q_ASSERT(!successorId().isEmpty()); + emit upgradeFailed(tr("The room is already upgraded")); + } + if (auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion)) + connect(job, &BaseJob::failure, this, + [this, job] { emit upgradeFailed(job->errorString()); }); + else + emit upgradeFailed(tr("Couldn't initiate upgrade")); } bool Room::hasAccountData(const QString& type) const @@ -681,30 +1265,21 @@ const EventPtr& Room::accountData(const QString& type) const return it != d->accountData.end() ? it->second : NoEventPtr; } -QStringList Room::tagNames() const -{ - return d->tags.keys(); -} +QStringList Room::tagNames() const { return d->tags.keys(); } -TagsMap Room::tags() const -{ - return d->tags; -} +TagsMap Room::tags() const { return d->tags; } -TagRecord Room::tag(const QString& name) const -{ - return d->tags.value(name); -} +TagRecord Room::tag(const QString& name) const { return d->tags.value(name); } std::pair<bool, QString> validatedTag(QString name) { - if (name.contains('.')) + if (name.isEmpty() || name.indexOf('.', 1) != -1) return { false, name }; - qWarning(MAIN) << "The tag" << name - << "doesn't follow the CS API conventions"; + qCWarning(MAIN) << "The tag" << name + << "doesn't follow the CS API conventions"; name.prepend("u."); - qWarning(MAIN) << "Using " << name << "instead"; + qCWarning(MAIN) << "Using " << name << "instead"; return { true, name }; } @@ -712,8 +1287,8 @@ std::pair<bool, QString> validatedTag(QString name) void Room::addTag(const QString& name, const TagRecord& record) { const auto& checkRes = validatedTag(name); - if (d->tags.contains(name) || - (checkRes.first && d->tags.contains(checkRes.second))) + if (d->tags.contains(name) + || (checkRes.first && d->tags.contains(checkRes.second))) return; emit tagsAboutToChange(); @@ -725,13 +1300,12 @@ void Room::addTag(const QString& name, const TagRecord& record) void Room::addTag(const QString& name, float order) { - addTag(name, TagRecord{order}); + addTag(name, TagRecord { order }); } void Room::removeTag(const QString& name) { - if (d->tags.contains(name)) - { + if (d->tags.contains(name)) { emit tagsAboutToChange(); d->tags.remove(name); emit tagsChanged(); @@ -739,133 +1313,149 @@ void Room::removeTag(const QString& name) } else if (!name.startsWith("u.")) removeTag("u." + name); else - qWarning(MAIN) << "Tag" << name << "on room" << objectName() + qCWarning(MAIN) << "Tag" << name << "on room" << objectName() << "not found, nothing to remove"; } -void Room::setTags(TagsMap newTags) +void Room::setTags(TagsMap newTags, ActionScope applyOn) { + bool propagate = applyOn != ActionScope::ThisRoomOnly; + auto joinStates = + applyOn == ActionScope::WithinSameState ? joinState() : + applyOn == ActionScope::OmitLeftState ? JoinState::Join|JoinState::Invite : + JoinState::Join|JoinState::Invite|JoinState::Leave; + if (propagate) { + for (auto* r = this; (r = r->predecessor(joinStates));) + r->setTags(newTags, ActionScope::ThisRoomOnly); + } + d->setTags(move(newTags)); connection()->callApi<SetAccountDataPerRoomJob>( - localUser()->id(), id(), TagEvent::matrixTypeId(), - TagEvent(d->tags).contentJson()); + localUser()->id(), id(), TagEvent::TypeId, + Quotient::toJson(TagEvent::content_type { d->tags })); + + if (propagate) { + for (auto* r = this; (r = r->successor(joinStates));) + r->setTags(d->tags, ActionScope::ThisRoomOnly); + } } -void Room::Private::setTags(TagsMap newTags) +void Room::Private::setTags(TagsMap&& newTags) { emit q->tagsAboutToChange(); const auto keys = newTags.keys(); - for (const auto& k: keys) - { - const auto& checkRes = validatedTag(k); - if (checkRes.first) - { - if (newTags.contains(checkRes.second)) + for (const auto& k : keys) + if (const auto& [adjusted, adjustedTag] = validatedTag(k); adjusted) { + if (newTags.contains(adjustedTag)) newTags.remove(k); else - newTags.insert(checkRes.second, newTags.take(k)); + newTags.insert(adjustedTag, newTags.take(k)); } - } + tags = move(newTags); - qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with" - << q->tagNames().join(", "); + qCDebug(STATE) << "Room" << q->objectName() << "is tagged with" + << q->tagNames().join(QStringLiteral(", ")); emit q->tagsChanged(); } -bool Room::isFavourite() const +bool Room::isFavourite() const { return d->tags.contains(FavouriteTag); } + +bool Room::isLowPriority() const { return d->tags.contains(LowPriorityTag); } + +bool Room::isServerNoticeRoom() const { - return d->tags.contains(FavouriteTag); + return d->tags.contains(ServerNoticeTag); } -bool Room::isLowPriority() const +bool Room::isDirectChat() const { return connection()->isDirectChat(id()); } + +QList<User*> Room::directChatUsers() const { - return d->tags.contains(LowPriorityTag); + return connection()->directChatUsers(this); } -bool Room::isDirectChat() const +QUrl Room::makeMediaUrl(const QString& eventId, const QUrl& mxcUrl) const { - return connection()->isDirectChat(id()); + 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; } -QList<User*> Room::directChatUsers() const +QString safeFileName(QString rawName) { - return connection()->directChatUsers(this); + return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_"); } const RoomMessageEvent* Room::Private::getEventWithFile(const QString& eventId) const { auto evtIt = q->findInTimeline(eventId); - if (evtIt != timeline.rend() && is<RoomMessageEvent>(**evtIt)) - { + if (evtIt != timeline.rend() && is<RoomMessageEvent>(**evtIt)) { auto* event = evtIt->viewAs<RoomMessageEvent>(); if (event->hasFileContent()) return event; } - qWarning() << "No files to download in event" << eventId; + qCWarning(MAIN) << "No files to download in event" << eventId; return nullptr; } QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const { - Q_ASSERT(event->hasFileContent()); + Q_ASSERT(event && event->hasFileContent()); const auto* fileInfo = event->content()->fileInfo(); QString fileName; if (!fileInfo->originalName.isEmpty()) - { - fileName = QFileInfo(fileInfo->originalName).fileName(); - } - else if (!event->plainBody().isEmpty()) - { - // Having no better options, assume that the body has - // the original file URL or at least the file name. - QUrl u { event->plainBody() }; - if (u.isValid()) - fileName = QFileInfo(u.path()).fileName(); + fileName = QFileInfo(safeFileName(fileInfo->originalName)).fileName(); + else if (QUrl u { event->plainBody() }; u.isValid()) { + qDebug(MAIN) << event->id() + << "has no file name supplied but the event body " + "looks like a URL - using the file name from it"; + fileName = u.fileName(); } - // Check the file name for sanity - if (fileName.isEmpty() || !QTemporaryFile(fileName).open()) - return "file." % fileInfo->mimeType.preferredSuffix(); - - if (QSysInfo::productType() == "windows") - { - const auto& suffixes = fileInfo->mimeType.suffixes(); - if (!suffixes.isEmpty() && - std::none_of(suffixes.begin(), suffixes.end(), - [&fileName] (const QString& s) { - return fileName.endsWith(s); })) + if (fileName.isEmpty()) + return safeFileName(fileInfo->mediaId()).replace('.', '-') % '.' + % fileInfo->mimeType.preferredSuffix(); + + if (QSysInfo::productType() == "windows") { + if (const auto& suffixes = fileInfo->mimeType.suffixes(); + !suffixes.isEmpty() + && std::none_of(suffixes.begin(), suffixes.end(), + [&fileName](const QString& s) { + return fileName.endsWith(s); + })) return fileName % '.' % fileInfo->mimeType.preferredSuffix(); } return fileName; } -QUrl Room::urlToThumbnail(const QString& eventId) +QUrl Room::urlToThumbnail(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) - if (event->hasThumbnail()) - { + 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); } - qDebug() << "Event" << eventId << "has no thumbnail"; + qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; } -QUrl Room::urlToDownload(const QString& eventId) +QUrl Room::urlToDownload(const QString& eventId) const { - if (auto* event = d->getEventWithFile(eventId)) - { + 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 {}; } -QString Room::fileNameToDownload(const QString& eventId) +QString Room::fileNameToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) return d->fileNameToDownload(event); @@ -874,8 +1464,8 @@ QString Room::fileNameToDownload(const QString& eventId) FileTransferInfo Room::fileTransferInfo(const QString& id) const { - auto infoIt = d->fileTransfers.find(id); - if (infoIt == d->fileTransfers.end()) + const auto infoIt = d->fileTransfers.constFind(id); + if (infoIt == d->fileTransfers.cend()) return {}; // FIXME: Add lib tests to make sure FileTransferInfo::status stays @@ -883,79 +1473,208 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const qint64 progress = infoIt->progress; qint64 total = infoIt->total; - if (total > INT_MAX) - { + if (total > INT_MAX) { // JavaScript doesn't deal with 64-bit integers; scale down if necessary progress = llround(double(progress) / total * INT_MAX); total = INT_MAX; } -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST - FileTransferInfo fti; - fti.status = infoIt->status; - fti.progress = int(progress); - fti.total = int(total); - fti.localDir = QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()); - fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); - return fti; -#else - return { infoIt->status, int(progress), int(total), - QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), - QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) - }; -#endif + return { infoIt->status, + infoIt->isUpload, + int(progress), + int(total), + QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), + QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) }; } -QString Room::prettyPrint(const QString& plainText) const +QUrl Room::fileSource(const QString& id) const { - return QMatrixClient::prettyPrint(plainText); + auto url = urlToDownload(id); + if (url.isValid()) + return url; + + // No urlToDownload means it's a pending or completed upload. + auto infoIt = d->fileTransfers.constFind(id); + if (infoIt != d->fileTransfers.cend()) + return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); + + qCWarning(MAIN) << "File source for identifier" << id << "not found"; + return {}; } -QList< User* > Room::usersTyping() const +QString Room::prettyPrint(const QString& plainText) const { - return d->usersTyping; + return Quotient::prettyPrint(plainText); } -QList< User* > Room::membersLeft() const +QList<User*> Room::usersTyping() const { return d->usersTyping; } + +QList<User*> Room::membersLeft() const { return d->membersLeft; } + +QList<User*> Room::users() const { return d->membersMap.values(); } + +QStringList Room::memberNames() const { - return d->membersLeft; + return safeMemberNames(); } -QList< User* > Room::users() const +QStringList Room::safeMemberNames() const { - return d->membersMap.values(); + QStringList res; + res.reserve(d->membersMap.size()); + for (const auto* u: std::as_const(d->membersMap)) + res.append(safeMemberName(u->id())); + + return res; } -QStringList Room::memberNames() const +QStringList Room::htmlSafeMemberNames() const { QStringList res; - for (auto u : qAsConst(d->membersMap)) - res.append( roomMembername(u) ); + res.reserve(d->membersMap.size()); + for (const auto* u: std::as_const(d->membersMap)) + res.append(htmlSafeMemberName(u->id())); return res; } -int Room::memberCount() const +int Room::timelineSize() const { return int(d->timeline.size()); } + +bool Room::usesEncryption() const { - return d->membersMap.size(); + return !currentState() + .queryOr(&EncryptionEvent::algorithm, QString()) + .isEmpty(); } -int Room::timelineSize() const +const StateEvent* Room::getCurrentState(const QString& evtType, + const QString& stateKey) const { - return int(d->timeline.size()); + return d->getCurrentState({ evtType, stateKey }); } -bool Room::usesEncryption() const +RoomStateView Room::currentState() const { - return !d->encryptionAlgorithm.isEmpty(); + return d->currentState; } -void Room::Private::insertMemberIntoMap(User *u) +RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { - const auto userName = u->name(q); - // If there is exactly one namesake of the added user, signal member renaming - // for that other one because the two should be disambiguated now. - auto namesakes = membersMap.values(userName); +#ifndef Quotient_E2EE_ENABLED + Q_UNUSED(encryptedEvent) + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return {}; +#else // Quotient_E2EE_ENABLED + if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) { + qWarning(E2EE) << "Algorithm of the encrypted event with id" + << encryptedEvent.id() << "is not decryptable by the current device"; + return {}; + } + QString decrypted = d->groupSessionDecryptMessage( + encryptedEvent.ciphertext(), encryptedEvent.sessionId(), + encryptedEvent.id(), encryptedEvent.originTimestamp(), + encryptedEvent.senderId()); + if (decrypted.isEmpty()) { + // qCWarning(E2EE) << "Encrypted message is empty"; + return {}; + } + auto decryptedEvent = encryptedEvent.createDecrypted(decrypted); + if (decryptedEvent->roomId() == id()) { + return decryptedEvent; + } + qCWarning(E2EE) << "Decrypted event" << encryptedEvent.id() << "not for this room; discarding."; + return {}; +#endif // Quotient_E2EE_ENABLED +} + +void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, + const QString& senderId, + const QString& olmSessionId) +{ +#ifndef Quotient_E2EE_ENABLED + Q_UNUSED(roomKeyEvent) + Q_UNUSED(senderId) + Q_UNUSED(olmSessionId) + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED + if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) { + qCWarning(E2EE) << "Ignoring unsupported algorithm" + << roomKeyEvent.algorithm() << "in m.room_key event"; + } + if (d->addInboundGroupSession(roomKeyEvent.sessionId(), + roomKeyEvent.sessionKey(), senderId, + olmSessionId)) { + qCWarning(E2EE) << "added new inboundGroupSession:" + << d->groupSessions.size(); + auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()]; + for (const auto& eventId : undecryptedEvents) { + const auto pIdx = d->eventsIndex.constFind(eventId); + if (pIdx == d->eventsIndex.cend()) + continue; + auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())]; + if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) { + if (auto decrypted = decryptMessage(*encryptedEvent)) { + // The reference will survive the pointer being moved + auto& decryptedEvent = *decrypted; + auto oldEvent = ti.replaceEvent(std::move(decrypted)); + decryptedEvent.setOriginalEvent(std::move(oldEvent)); + emit replacedEvent(ti.event(), decryptedEvent.originalEvent()); + d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; + } + } + } + } +#endif // Quotient_E2EE_ENABLED +} + +int Room::joinedCount() const +{ + return d->summary.joinedMemberCount.value_or(d->membersMap.size()); +} + +int Room::invitedCount() const +{ + // TODO: Store invited users in Room too + Q_ASSERT(d->summary.invitedMemberCount.has_value()); + return d->summary.invitedMemberCount.value_or(0); +} + +int Room::totalMemberCount() const { return joinedCount() + invitedCount(); } + +GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; } + +Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) +{ + if (!summary.merge(newSummary)) + return Change::None; + qCDebug(STATE).nospace().noquote() + << "Updated room summary for " << q->objectName() << ": " << summary; + return Change::Summary; +} + +void Room::Private::insertMemberIntoMap(User* u) +{ + const auto maybeUserName = + currentState.query(u->id(), &RoomMemberEvent::newDisplayName); + if (!maybeUserName) + qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id() + << "has no name (even empty)"; + const auto userName = maybeUserName.value_or(QString()); + const auto namesakes = membersMap.values(userName); + qCDebug(MEMBERS) << "insertMemberIntoMap(), user" << u->id() + << "with name" << userName << '-' + << namesakes.size() << "namesake(s) found"; + + // Callers should make sure they are not adding an existing user once more + Q_ASSERT(!namesakes.contains(u)); + if (namesakes.contains(u)) { // Release version whines but continues + qCCritical(MEMBERS) << "Trying to add a user" << u->id() << "to room" + << q->objectName() << "but that's already in it"; + return; + } + + // If there is exactly one namesake of the added user, signal member + // renaming for that other one because the two should be disambiguated now if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); @@ -964,278 +1683,470 @@ void Room::Private::insertMemberIntoMap(User *u) emit q->memberRenamed(namesakes.front()); } -void Room::Private::renameMember(User* u, QString oldName) +void Room::Private::removeMemberFromMap(User* u) { - if (u->name(q) == oldName) - { - qCWarning(MAIN) << "Room::Private::renameMember(): the user " - << u->fullName(q) - << "is already known in the room under a new name."; - } - else if (membersMap.contains(oldName, u)) - { - removeMemberFromMap(oldName, u); - insertMemberIntoMap(u); - } - emit q->memberRenamed(u); -} + const auto userName = currentState.queryOr(u->id(), + &RoomMemberEvent::newDisplayName, + QString()); -void Room::Private::removeMemberFromMap(const QString& username, User* u) -{ + qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName + << "for user" << u->id(); User* namesake = nullptr; - auto namesakes = membersMap.values(username); - if (namesakes.size() == 2) - { - namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); + auto namesakes = membersMap.values(userName); + // If there was one namesake besides the removed user, signal member + // renaming for it because it doesn't need to be disambiguated any more. + if (namesakes.size() == 2) { + namesake = + namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); - emit q->memberAboutToRename(namesake, username); + emit q->memberAboutToRename(namesake, userName); + } + if (membersMap.remove(userName, u) == 0) { + qCDebug(MEMBERS) << "No entries removed; checking the whole list"; + // Unless at the stage of initial filling, this no removed entries + // is suspicious; double-check that this user is not found in + // the whole map, and stop (for debug builds) or shout in the logs + // (for release builds) if there's one. That search is O(n), which + // may come rather expensive for larger rooms. + QElapsedTimer et; + auto it = std::find(membersMap.cbegin(), membersMap.cend(), u); + if (et.nsecsElapsed() > profilerMinNsecs() / 10) + qCDebug(MEMBERS) << "...done in" << et; + if (it != membersMap.cend()) { + // The assert (still) does more harm than good, it seems +// Q_ASSERT_X(false, __FUNCTION__, +// "Mismatched name in the room members list"); + qCCritical(MEMBERS) << "Mismatched name in the room members list;" + " avoiding the list corruption"; + membersMap.remove(it.key(), u); + } } - membersMap.remove(username, u); - // If there was one namesake besides the removed user, signal member renaming - // for it because it doesn't need to be disambiguated anymore. - // TODO: Think about left users. if (namesake) emit q->memberRenamed(namesake); } 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::difference_type Room::Private::moveEventsToTimeline( - RoomEventsRange events, EventsPlacement placement) +Room::Timeline::size_type +Room::Private::moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement) { Q_ASSERT(!events.empty()); // Historical messages arrive in newest-to-oldest order, so the process for - // them is symmetric to the one for new messages. - auto index = timeline.empty() ? -int(placement) : - placement == Older ? timeline.front().index() : - timeline.back().index(); + // them is almost symmetric to the one for new messages. New messages get + // appended from index 0; old messages go backwards from index -1. + auto index = timeline.empty() + ? -((placement + 1) / 2) /* 1 -> -1; -1 -> 0 */ + : placement == Older ? timeline.front().index() + : timeline.back().index(); auto baseIndex = index; - for (auto&& e: events) - { + for (auto&& e : events) { const auto eId = e->id(); Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline"); - Q_ASSERT_X(!eId.isEmpty(), __FUNCTION__, - makeErrorStr(*e, - "Event with empty id cannot be in the timeline")); - Q_ASSERT_X(!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); + Q_ASSERT_X( + !eId.isEmpty(), __FUNCTION__, + makeErrorStr(*e, "Event with empty id cannot be in the timeline")); + Q_ASSERT_X( + !eventsIndex.contains(eId), __FUNCTION__, + makeErrorStr(*e, "Event is already in the timeline; " + "incoming events were not properly deduplicated")); + 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) * int(placement); + const auto insertedSize = (index - baseIndex) * placement; Q_ASSERT(insertedSize == int(events.size())); - return insertedSize; + return Timeline::size_type(insertedSize); +} + +QString Room::memberName(const QString& mxId) const +{ + // See https://github.com/matrix-org/matrix-doc/issues/1375 + if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) { + if (rme->newDisplayName()) + return *rme->newDisplayName(); + if (rme->prevContent() && rme->prevContent()->displayName) + return *rme->prevContent()->displayName; + } + return {}; } QString Room::roomMembername(const User* u) const { + Q_ASSERT(u != nullptr); + return disambiguatedMemberName(u->id()); +} + +QString Room::roomMembername(const QString& userId) const +{ + return disambiguatedMemberName(userId); +} + +inline QString makeFullUserName(const QString& displayName, const QString& mxId) +{ + return displayName % " (" % mxId % ')'; +} + +QString Room::disambiguatedMemberName(const QString& mxId) const +{ // See the CS spec, section 11.2.2.3 - const auto username = u->name(this); + const auto username = memberName(mxId); if (username.isEmpty()) - return u->id(); + return mxId; auto namesakesIt = qAsConst(d->membersMap).find(username); // We expect a user to be a member of the room - but technically it is - // possible to invoke roomMemberName() even for non-members. In such case + // possible to invoke this function even for non-members. In such case // we return the full name, just in case. if (namesakesIt == d->membersMap.cend()) - return u->fullName(this); + return makeFullUserName(username, mxId); - auto nextUserIt = namesakesIt + 1; - if (nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) + auto nextUserIt = namesakesIt; + if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) return username; // No disambiguation necessary - // Check if we can get away just attaching the bridge postfix - // (extension to the spec) - QVector<QString> bridges; - for (; namesakesIt != d->membersMap.cend() && namesakesIt.key() == username; - ++namesakesIt) - { - const auto bridgeName = (*namesakesIt)->bridged(); - if (bridges.contains(bridgeName)) // Two accounts on the same bridge - return u->fullName(this); // Disambiguate fully - // Don't bother sorting, not so many bridges out there - bridges.push_back(bridgeName); - } + return makeFullUserName(username, mxId); // Disambiguate fully +} - return u->rawName(this); // Disambiguate using the bridge postfix only +QString Room::safeMemberName(const QString& userId) const +{ + return sanitized(disambiguatedMemberName(userId)); } -QString Room::roomMembername(const QString& userId) const +QString Room::htmlSafeMemberName(const QString& userId) const { - return roomMembername(user(userId)); + return safeMemberName(userId).toHtmlEscaped(); } -void Room::updateData(SyncRoomData&& data) +QUrl Room::memberAvatarUrl(const QString &mxId) const { - if( d->prevBatch.isEmpty() ) + // See https://github.com/matrix-org/matrix-doc/issues/1375 + if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) { + if (rme->newAvatarUrl()) + return *rme->newAvatarUrl(); + if (rme->prevContent() && rme->prevContent()->avatarUrl) + return *rme->prevContent()->avatarUrl; + } + return {}; +} + +Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, + 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) +{ + qCDebug(MAIN) << "--- Updating room" << id() << "/" << objectName(); + bool firstUpdate = d->baseState.empty(); + + if (d->prevBatch.isEmpty()) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); - QElapsedTimer et; et.start(); - for (auto&& event: data.accountData) - processAccountDataEvent(move(event)); + 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)); - bool emitNamesChanged = false; - if (!data.state.empty()) - { - et.restart(); - for (const auto& e: data.state) - emitNamesChanged |= processStateEvent(*e); + for (auto&& ephemeralEvent : data.ephemeral) + roomChanges |= processEphemeralEvent(move(ephemeralEvent)); - if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents():" - << data.state.size() << "event(s)," << et; - } - if (!data.timeline.empty()) - { - et.restart(); - // State changes can arrive in a timeline event; so check those. - for (const auto& e: data.timeline) - emitNamesChanged |= processStateEvent(*e); - if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" - << data.timeline.size() << "event(s)," << et; - } - if (emitNamesChanged) + for (auto&& event : data.accountData) + roomChanges |= processAccountDataEvent(move(event)); + + roomChanges |= d->updateStatsFromSyncData(data, fromCache); + + if (roomChanges & Change::Topic) + emit topicChanged(); + + if (roomChanges & (Change::Name | Change::Aliases)) emit namesChanged(this); - d->updateDisplayname(); - if (!data.timeline.empty()) - { - et.restart(); - d->addNewMessageEvents(move(data.timeline)); - if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; - } - for( auto&& ephemeralEvent: data.ephemeral ) - processEphemeralEvent(move(ephemeralEvent)); + d->postprocessChanges(roomChanges, !fromCache); + if (firstUpdate) + emit baseStateLoaded(); + qCDebug(MAIN) << "--- Finished updating room" << id() << "/" << objectName(); +} - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count - if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) - { - qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; - d->unreadMessages = data.unreadCount; - emit unreadMessagesChanged(this); - } +void Room::Private::postprocessChanges(Changes changes, bool saveState) +{ + if (!changes) + return; - if( data.highlightCount != d->highlightCount ) - { - d->highlightCount = data.highlightCount; - emit highlightCountChanged(this); - } - if( data.notificationCount != d->notificationCount ) - { - d->notificationCount = data.notificationCount; - emit notificationCountChanged(this); + if (changes & Change::Members) + emit q->memberListChanged(); + + if (changes + & (Change::Name | Change::Aliases | Change::Members | Change::Summary)) + updateDisplayname(); + + if (changes & Change::PartiallyReadStats) { + QT_IGNORE_DEPRECATIONS( + 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" << Qt::hex << uint(changes) + << "in" << q->objectName(); + emit q->changed(changes); + if (saveState) + connection->saveRoomState(q); } -QString Room::Private::sendEvent(RoomEventPtr&& event) +RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); + if (event->roomId().isEmpty()) + event->setRoomId(id); + if (event->senderId().isEmpty()) + event->setSender(connection->userId()); auto* pEvent = rawPtr(event); - emit q->pendingEventAboutToAdd(); + emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); - return doSendEvent(pEvent); + return pEvent; +} + +QString Room::Private::sendEvent(RoomEventPtr&& event) +{ + if (!q->successorId().isEmpty()) { + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; + } + + return doSendEvent(addAsPending(std::move(event))); } QString Room::Private::doSendEvent(const RoomEvent* pEvent) { - auto txnId = pEvent->transactionId(); + const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. - if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest, - id, pEvent->matrixType(), txnId, pEvent->contentJson())) - { - Room::connect(call, &BaseJob::started, q, - [this,pEvent,txnId] { - auto it = findAsPending(pEvent); - if (it == unsyncedEvents.end()) - { - qWarning(EVENTS) << "Pending event for transaction" << txnId - << "not found - got synced so soon?"; - return; - } - it->setDeparted(); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - }); - Room::connect(call, &BaseJob::failure, q, - std::bind(&Room::Private::onEventSendingFailure, - this, pEvent, txnId, call)); - Room::connect(call, &BaseJob::success, q, - [this,call,pEvent,txnId] { - // Find an event by the pointer saved in the lambda (the pointer - // may be dangling by now but we can still search by it). - auto it = findAsPending(pEvent); - if (it == unsyncedEvents.end()) - { - qDebug(EVENTS) << "Pending event for transaction" << txnId - << "already merged"; - return; + const RoomEvent* _event = pEvent; + std::unique_ptr<EncryptedEvent> encryptedEvent; + + if (q->usesEncryption()) { +#ifndef Quotient_E2EE_ENABLED + qWarning() << "This build of libQuotient does not support E2EE."; + return {}; +#else + if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { + createMegolmSession(); + } + // Send the session to other people + connection->sendSessionKeyToDevices( + id, currentOutboundMegolmSession->sessionId(), + currentOutboundMegolmSession->sessionKey(), getDevicesWithoutKey(), + currentOutboundMegolmSession->sessionMessageIndex()); + + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); + currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); + encryptedEvent = makeEvent<EncryptedEvent>( + encrypted, q->connection()->olmAccount()->identityKeys().curve25519, + q->connection()->deviceId(), + currentOutboundMegolmSession->sessionId()); + encryptedEvent->setTransactionId(connection->generateTxnId()); + encryptedEvent->setRoomId(id); + encryptedEvent->setSender(connection->userId()); + if(pEvent->contentJson().contains("m.relates_to"_ls)) { + encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject()); + } + // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out + _event = encryptedEvent.get(); +#endif + } + + if (auto call = + connection->callApi<SendMessageJob>(BackgroundRequest, id, + _event->matrixType(), txnId, + _event->contentJson())) { + Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { + auto it = q->findPendingEvent(txnId); + if (it == unsyncedEvents.end()) { + qWarning(EVENTS) << "Pending event for transaction" << txnId + << "not found - got synced so soon?"; + return; + } + it->setDeparted(); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); + }); + Room::connect(call, &BaseJob::result, q, [this, txnId, call] { + if (!call->status().good()) { + onEventSendingFailure(txnId, call); + return; + } + auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + if (it->deliveryStatus() != EventStatus::ReachedServer) { + it->setReachedServer(call->eventId()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } + } else + qDebug(EVENTS) << "Pending event for transaction" << txnId + << "already merged"; - it->setReachedServer(call->eventId()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - }); + emit q->messageSent(txnId, call->eventId()); + }); } else - onEventSendingFailure(pEvent, txnId); + onEventSendingFailure(txnId); return txnId; } -Room::PendingEvents::iterator Room::Private::findAsPending( - const RoomEvent* rawEvtPtr) +void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) { - const auto comp = - [rawEvtPtr] (const auto& pe) { return pe.event() == rawEvtPtr; }; - - return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp); -} - -void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, - const QString& txnId, BaseJob* call) -{ - auto it = findAsPending(pEvent); - if (it == unsyncedEvents.end()) - { + auto it = q->findPendingEvent(txnId); + if (it == unsyncedEvents.end()) { qCritical(EVENTS) << "Pending event for transaction" << txnId << "could not be sent"; return; } - it->setSendingFailed(call - ? call->statusCaption() % ": " % call->errorString() - : tr("The call could not be started")); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + it->setSendingFailed(call ? call->statusCaption() % ": " % call->errorString() + : tr("The call could not be started")); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } QString Room::retryMessage(const QString& txnId) { - auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); + const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); - qDebug(EVENTS) << "Retrying transaction" << txnId; + qCDebug(EVENTS) << "Retrying transaction" << txnId; + const auto& transferIt = d->fileTransfers.constFind(txnId); + if (transferIt != d->fileTransfers.cend()) { + Q_ASSERT(transferIt->isUpload); + if (transferIt->status == FileTransferInfo::Completed) { + qCDebug(MESSAGES) + << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; + } else { + if (isJobPending(transferIt->job)) { + qCDebug(MESSAGES) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, + tr("File upload will be retried")); + } + uploadFile(txnId, QUrl::fromLocalFile( + transferIt->localFileInfo.absoluteFilePath())); + // FIXME: Content type is no more passed here but it should + } + } + if (it->deliveryStatus() == EventStatus::ReachedServer) { + qCWarning(MAIN) + << "The previous attempt has reached the server; two" + " events are likely to be in the timeline after retry"; + } it->resetStatus(); + emit pendingEventChanged(int(it - d->unsyncedEvents.begin())); return d->doSendEvent(it->event()); } +// Using a function defers actual tr() invocation to the moment when +// translations are initialised +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(), - [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); + [txnId](const auto& evt) { + return evt->transactionId() == txnId; + }); Q_ASSERT(it != d->unsyncedEvents.end()); - qDebug(EVENTS) << "Discarding transaction" << txnId; - emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); + qCDebug(EVENTS) << "Discarding transaction" << txnId; + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) { + Q_ASSERT(transferIt->isUpload); + if (isJobPending(transferIt->job)) { + transferIt->status = FileTransferInfo::Cancelled; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, FileTransferCancelledMsg()); + } else if (transferIt->status == FileTransferInfo::Completed) { + qCWarning(MAIN) + << "File for transaction" << txnId + << "has been uploaded but the message was discarded"; + } + } + emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); } @@ -1251,51 +2162,148 @@ QString Room::postPlainText(const QString& plainText) } QString Room::postHtmlMessage(const QString& plainText, const QString& html, - MessageEventType type) + MessageEventType type) { - return d->sendEvent<RoomMessageEvent>(plainText, type, - new EventContent::TextContent(html, QStringLiteral("text/html"))); + return d->sendEvent<RoomMessageEvent>( + plainText, type, + new EventContent::TextContent(html, QStringLiteral("text/html"))); } QString Room::postHtmlText(const QString& plainText, const QString& html) { - return postHtmlMessage(plainText, html, MessageEventType::Text); + return postHtmlMessage(plainText, html); +} + +QString Room::postReaction(const QString& eventId, const QString& key) +{ + return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key)); +} + +QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) +{ + 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. + q->uploadFile(txnId, localUrl); + // Below, the upload job is used as a context object to clean up connections + const auto& transferJob = fileTransfers.value(txnId).job; + connect(q, &Room::fileTransferCompleted, transferJob, + [this, txnId](const QString& tId, const QUrl&, + const FileSourceInfo& fileMetadata) { + if (tId != txnId) + return; + + const auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + it->setFileUploaded(fileMetadata); + 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" << getUrlFromSourceInfo(fileMetadata) + << "but the event referring to it was " + "cancelled"; + } + }); + 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) { - if (usesEncryption()) - { - qCCritical(MAIN) << "Room" << displayName() - << "enforces encryption; sending encrypted messages is not supported yet"; - } return d->sendEvent(RoomEventPtr(event)); } QString Room::postJson(const QString& matrixType, const QJsonObject& eventContent) { - return d->sendEvent(loadEvent<RoomEvent>(basicEventJson(matrixType, eventContent))); + return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent)); +} + +SetRoomStateWithKeyJob* Room::setState(const StateEvent& evt) +{ + return setState(evt.matrixType(), evt.stateKey(), evt.contentJson()); +} + +SetRoomStateWithKeyJob* Room::setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) +{ + return d->requestSetState(evtType, stateKey, contentJson); } void Room::setName(const QString& newName) { - d->requestSetState(RoomNameEvent(newName)); + setState<RoomNameEvent>(newName); } void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState(RoomCanonicalAliasEvent(newAlias)); + setState<RoomCanonicalAliasEvent>(newAlias, altAliases()); +} + +void Room::setPinnedEvents(const QStringList& events) +{ + setState<RoomPinnedEvent>(events); +} +void Room::setLocalAliases(const QStringList& aliases) +{ + setState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases); } void Room::setTopic(const QString& newTopic) { - d->requestSetState(RoomTopicEvent(newTopic)); + setState<RoomTopicEvent>(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) { - if (le->type() != re->type()) + if (le->metaType() != re->metaType()) return false; if (!re->id().isEmpty()) @@ -1313,60 +2321,78 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) return le->contentJson() == re->contentJson(); } -bool Room::supportsCalls() const +bool Room::supportsCalls() const { return joinedCount() == 2; } + +void Room::checkVersion() { - return d->membersMap.size() == 2; + const auto defaultVersion = connection()->defaultRoomVersion(); + const auto stableVersions = connection()->stableRoomVersions(); + Q_ASSERT(!defaultVersion.isEmpty()); + // This method is only called after the base state has been loaded + // or the server capabilities have been loaded. + emit stabilityUpdated(defaultVersion, stableVersions); + if (!stableVersions.contains(version())) { + qCDebug(STATE) << this << "version is" << version() + << "which the server doesn't count as stable"; + if (canSwitchVersions()) + qCDebug(STATE) + << "The current user has enough privileges to fix it"; + } } void Room::inviteCall(const QString& callId, const int lifetime, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallInviteEvent(callId, lifetime, sdp)); + d->sendEvent<CallInviteEvent>(callId, lifetime, sdp); } void Room::sendCallCandidates(const QString& callId, const QJsonArray& candidates) { Q_ASSERT(supportsCalls()); - postEvent(new CallCandidatesEvent(callId, candidates)); + d->sendEvent<CallCandidatesEvent>(callId, candidates); } -void Room::answerCall(const QString& callId, const int lifetime, +void Room::answerCall(const QString& callId, [[maybe_unused]] int lifetime, const QString& sdp) { - Q_ASSERT(supportsCalls()); - postEvent(new CallAnswerEvent(callId, lifetime, sdp)); + qCWarning(MAIN) << "To client developer: drop lifetime parameter from " + "Room::answerCall(), it is no more accepted"; + answerCall(callId, sdp); } void Room::answerCall(const QString& callId, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallAnswerEvent(callId, sdp)); + d->sendEvent<CallAnswerEvent>(callId, sdp); } void Room::hangupCall(const QString& callId) { Q_ASSERT(supportsCalls()); - postEvent(new CallHangupEvent(callId)); + d->sendEvent<CallHangupEvent>(callId); } -void Room::getPreviousContent(int limit) +void Room::getPreviousContent(int limit, const QString& filter) { - d->getPreviousContent(limit); + d->getPreviousContent(limit, filter); } -void Room::Private::getPreviousContent(int limit) +void Room::Private::getPreviousContent(int limit, const QString &filter) { - if( !isJobRunning(eventsHistoryJob) ) - { - eventsHistoryJob = - connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); - connect( eventsHistoryJob, &BaseJob::success, q, [=] { - prevBatch = eventsHistoryJob->end(); - addHistoricalMessageEvents(eventsHistoryJob->chunk()); - }); - } + if (isJobPending(eventsHistoryJob)) + return; + + eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, "b", prevBatch, + "", limit, filter); + emit q->eventsHistoryJobChanged(); + connect(eventsHistoryJob, &BaseJob::success, q, [this] { + prevBatch = eventsHistoryJob->end(); + addHistoricalMessageEvents(eventsHistoryJob->chunk()); + }); + connect(eventsHistoryJob, &QObject::destroyed, q, + &Room::eventsHistoryJobChanged); } void Room::inviteToRoom(const QString& memberId) @@ -1376,12 +2402,8 @@ void Room::inviteToRoom(const QString& memberId) LeaveRoomJob* Room::leaveRoom() { - return connection()->callApi<LeaveRoomJob>(id()); -} - -SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const -{ - return d->requestSetState(memberId, event); + // FIXME, #63: It should be RoomManager, not Connection + return connection()->leaveRoom(this); } void Room::kickMember(const QString& memberId, const QString& reason) @@ -1401,8 +2423,8 @@ void Room::unban(const QString& userId) void Room::redactEvent(const QString& eventId, const QString& reason) { - connection()->callApi<RedactEventJob>( - id(), eventId, connection()->generateTxnId(), reason); + connection()->callApi<RedactEventJob>(id(), QUrl::toPercentEncoding(eventId), + connection()->generateTxnId(), reason); } void Room::uploadFile(const QString& id, const QUrl& localFilename, @@ -1411,19 +2433,35 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); + FileSourceInfo fileMetadata; +#ifdef Quotient_E2EE_ENABLED + QTemporaryFile tempFile; + if (usesEncryption()) { + tempFile.open(); + QFile file(localFilename.toLocalFile()); + file.open(QFile::ReadOnly); + QByteArray data; + std::tie(fileMetadata, data) = encryptFile(file.readAll()); + tempFile.write(data); + tempFile.close(); + fileName = QFileInfo(tempFile).absoluteFilePath(); + } +#endif auto job = connection()->uploadFile(fileName, overrideContentType); - if (isJobRunning(job)) - { - d->fileTransfers.insert(id, { job, fileName }); + if (isJobPending(job)) { + d->fileTransfers[id] = { job, fileName, true }; connect(job, &BaseJob::uploadProgress, this, - [this,id] (qint64 sent, qint64 total) { + [this, id](qint64 sent, qint64 total) { d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this,id,localFilename,job] { - d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, job->contentUri()); - }); + connect(job, &BaseJob::success, this, + [this, id, localFilename, job, fileMetadata]() mutable { + // The lambda is mutable to change encryptedFileMetadata + d->fileTransfers[id].status = FileTransferInfo::Completed; + setUrlInSourceInfo(fileMetadata, QUrl(job->contentUri())); + emit fileTransferCompleted(id, localFilename, fileMetadata); + }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); emit newFileTransfer(id, localFilename); @@ -1433,70 +2471,85 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, void Room::downloadFile(const QString& eventId, const QUrl& localFilename) { - auto ongoingTransfer = d->fileTransfers.find(eventId); - if (ongoingTransfer != d->fileTransfers.end() && - ongoingTransfer->status == FileTransferInfo::Started) - { - qCWarning(MAIN) << "Download for" << eventId - << "already started; to restart, cancel it first"; + if (auto ongoingTransfer = d->fileTransfers.constFind(eventId); + ongoingTransfer != d->fileTransfers.cend() + && ongoingTransfer->status == FileTransferInfo::Started) { + qCWarning(MAIN) << "Transfer for" << eventId + << "is ongoing; download won't start"; return; } Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); const auto* event = d->getEventWithFile(eventId); - if (!event) - { + if (!event) { qCCritical(MAIN) << eventId << "is not in the local timeline or has no file content"; Q_ASSERT(false); return; } - const auto fileUrl = event->content()->fileInfo()->url; + const auto* const fileInfo = event->content()->fileInfo(); + if (!fileInfo->isValid()) { + qCWarning(MAIN) << "Event" << eventId + << "has an empty or malformed mxc URL; won't download"; + return; + } + const auto fileUrl = fileInfo->url(); auto filePath = localFilename.toLocalFile(); - if (filePath.isEmpty()) - { - // Build our own file path, starting with temp directory and eventId. - filePath = eventId; - filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') % - '#' % d->fileNameToDownload(event); + if (filePath.isEmpty()) { // Setup default file path + filePath = + fileInfo->url().path().mid(1) % '_' % d->fileNameToDownload(event); + + if (filePath.size() > 200) // If too long, elide in the middle + filePath.replace(128, filePath.size() - 192, "---"); + + filePath = QDir::tempPath() % '/' % filePath; + qDebug(MAIN) << "File path:" << filePath; } - auto job = connection()->downloadFile(fileUrl, filePath); - if (isJobRunning(job)) - { - // If there was a previous transfer (completed or failed), remove it. - d->fileTransfers.remove(eventId); - d->fileTransfers.insert(eventId, { job, job->targetFileName() }); + DownloadFileJob *job = nullptr; +#ifdef Quotient_E2EE_ENABLED + if (auto* fileMetadata = + std::get_if<EncryptedFileMetadata>(&fileInfo->source)) { + job = connection()->downloadFile(fileUrl, *fileMetadata, filePath); + } else { +#endif + job = connection()->downloadFile(fileUrl, filePath); +#ifdef Quotient_E2EE_ENABLED + } +#endif + if (isJobPending(job)) { + // If there was a previous transfer (completed or failed), overwrite it. + d->fileTransfers[eventId] = { job, job->targetFileName() }; connect(job, &BaseJob::downloadProgress, this, - [this,eventId] (qint64 received, qint64 total) { - d->fileTransfers[eventId].update(received, total); - emit fileTransferProgress(eventId, received, total); - }); - connect(job, &BaseJob::success, this, [this,eventId,fileUrl,job] { - d->fileTransfers[eventId].status = FileTransferInfo::Completed; - emit fileTransferCompleted(eventId, fileUrl, - QUrl::fromLocalFile(job->targetFileName())); - }); + [this, eventId](qint64 received, qint64 total) { + d->fileTransfers[eventId].update(received, total); + emit fileTransferProgress(eventId, received, total); + }); + connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { + d->fileTransfers[eventId].status = FileTransferInfo::Completed; + emit fileTransferCompleted( + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); + }); connect(job, &BaseJob::failure, this, - std::bind(&Private::failedTransfer, d, - eventId, job->errorString())); + std::bind(&Private::failedTransfer, d, eventId, + job->errorString())); + emit newFileTransfer(eventId, localFilename); } else d->failedTransfer(eventId); } void Room::cancelFileTransfer(const QString& id) { - auto it = d->fileTransfers.find(id); - if (it == d->fileTransfers.end()) - { - qCWarning(MAIN) << "No information on file transfer" << id - << "in room" << d->id; + 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 (isJobRunning(it->job)) + 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 @@ -1506,15 +2559,16 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const // Multiple-remove (by different criteria), single-erase // 1. Check for duplicates against the timeline. - auto dupsBegin = remove_if(events.begin(), events.end(), - [&] (const RoomEventPtr& e) - { return eventsIndex.contains(e->id()); }); + auto dupsBegin = + remove_if(events.begin(), events.end(), [&](const RoomEventPtr& e) { + return eventsIndex.contains(e->id()); + }); // 2. Check for duplicates within the batch if there are still events. for (auto eIt = events.begin(); distance(eIt, dupsBegin) > 1; ++eIt) - dupsBegin = remove_if(eIt + 1, dupsBegin, - [&] (const RoomEventPtr& e) - { return e->id() == (*eIt)->id(); }); + dupsBegin = remove_if(eIt + 1, dupsBegin, [&](const RoomEventPtr& e) { + return e->id() == (*eIt)->id(); + }); if (dupsBegin == events.end()) return; @@ -1523,6 +2577,26 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const events.erase(dupsBegin, events.end()); } +void Room::Private::decryptIncomingEvents(RoomEvents& events) +{ +#ifdef Quotient_E2EE_ENABLED + QElapsedTimer et; + et.start(); + size_t totalDecrypted = 0; + for (auto& eptr : events) + if (const auto& eeptr = eventCast<EncryptedEvent>(eptr)) { + if (auto decrypted = q->decryptMessage(*eeptr)) { + ++totalDecrypted; + auto&& oldEvent = exchange(eptr, move(decrypted)); + eptr->setOriginalEvent(::move(oldEvent)); + } else + undecryptedEvents[eeptr->sessionId()] += eeptr->id(); + } + if (totalDecrypted > 5 || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) << "Decrypted" << totalDecrypted << "events in" << et; +#endif +} + /** Make a redacted event * * This applies the redaction procedure as defined by the CS API specification @@ -1532,42 +2606,45 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { - auto originalJson = target.originalJsonObject(); - static const QStringList keepKeys = - { EventIdKey, TypeKey, QStringLiteral("room_id"), - QStringLiteral("sender"), QStringLiteral("state_key"), - QStringLiteral("prev_content"), ContentKey, - QStringLiteral("origin_server_ts") }; - - std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap - { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } } -// , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } -// , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } -// , { RoomPowerLevels::typeId(), -// { QStringLiteral("ban"), QStringLiteral("events"), -// QStringLiteral("events_default"), QStringLiteral("kick"), -// QStringLiteral("redact"), QStringLiteral("state_default"), -// QStringLiteral("users"), QStringLiteral("users_default") } } - , { RoomAliasesEvent::typeId(), { QStringLiteral("alias") } } - }; - for (auto it = originalJson.begin(); it != originalJson.end();) - { + auto originalJson = target.fullJson(); + // clang-format off + static const QStringList keepKeys { + EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey, + QStringLiteral("hashes"), QStringLiteral("signatures"), + QStringLiteral("depth"), QStringLiteral("prev_events"), + QStringLiteral("prev_state"), QStringLiteral("auth_events"), + QStringLiteral("origin"), QStringLiteral("origin_server_ts"), + QStringLiteral("membership") }; + // clang-format on + + 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") } }, + // TODO: Replace with RoomJoinRules::TypeId etc. once available + { "m.room.join_rules"_ls, { QStringLiteral("join_rule") } }, + { "m.room.history_visibility"_ls, + { QStringLiteral("history_visibility") } } + }; + for (auto it = originalJson.begin(); it != originalJson.end();) { if (!keepKeys.contains(it.key())) it = originalJson.erase(it); // TODO: shred the value else ++it; } auto keepContentKeys = - find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), - [&target](const auto& t) { return target.type() == t.first; } ); - if (keepContentKeys == keepContentKeysMap.end()) - { + find_if(begin(keepContentKeysMap), end(keepContentKeysMap), + [&target](const auto& t) { return target.type() == t.first; }); + if (keepContentKeys == end(keepContentKeysMap)) { originalJson.remove(ContentKeyL); originalJson.remove(PrevContentKeyL); } else { auto content = originalJson.take(ContentKeyL).toObject(); - for (auto it = content.begin(); it != content.end(); ) - { + for (auto it = content.begin(); it != content.end();) { if (!keepContentKeys->second.contains(it.key())) it = content.erase(it); else @@ -1576,7 +2653,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); @@ -1586,25 +2663,101 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) { // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. - const auto pIdx = eventsIndex.find(redaction.redactedEvent()); - if (pIdx == eventsIndex.end()) + const auto pIdx = eventsIndex.constFind(redaction.redactedEvent()); + if (pIdx == eventsIndex.cend()) return false; Q_ASSERT(q->isValidIndex(*pIdx)); auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; - if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) - { - qCDebug(MAIN) << "Redaction" << redaction.id() - << "of event" << ti->id() << "already done, skipping"; + if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { + qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" + << ti->id() << "already done, skipping"; return true; } - // Make a new event from the redacted JSON, exchange events, - // notify everyone and delete the old event + // Make a new event from the redacted JSON and put it in the timeline + // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); - q->onRedaction(*oldEvent, *ti.event()); - qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + if (oldEvent->isStateEvent()) { + // Check whether the old event was a part of current state; if it was, + // update the current state to the redacted event object. + const auto currentStateEvt = + currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); + Q_ASSERT(currentStateEvt); + if (currentStateEvt == oldEvent.get()) { + // Historical states can't be in currentState + Q_ASSERT(ti.index() >= 0); + qCDebug(STATE).nospace() + << "Redacting state " << oldEvent->matrixType() << "/" + << oldEvent->stateKey(); + // Retarget the current state to the newly made event. + if (q->processStateEvent(*ti)) + emit q->namesChanged(q); + updateDisplayname(); + } + } + if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) { + const auto& targetEvtId = reaction->relation().eventId; + const std::pair lookupKey { targetEvtId, EventRelation::AnnotationType }; + if (relations.contains(lookupKey)) { + relations[lookupKey].removeOne(reaction); + emit q->updatedEvent(targetEvtId); + } + } + q->onRedaction(*oldEvent, *ti); + emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + // By now, all references to oldEvent must have been updated to ti.event() + return true; +} + +/** Make a replaced event + * + * Takes \p target and returns a copy of it with content taken from + * \p replacement. Disposal of the original event after that is on the caller. + */ +RoomEventPtr makeReplaced(const RoomEvent& target, + const RoomMessageEvent& replacement) +{ + 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(); + relations["m.replace"_ls] = replacement.id(); + unsignedData.insert(QStringLiteral("m.relations"), relations); + originalJson.insert(UnsignedKey, unsignedData); + + return loadEvent<RoomEvent>(originalJson); +} + +bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) +{ + // Can't use findInTimeline because it returns a const iterator, and + // we need to change the underlying TimelineItem. + const auto pIdx = eventsIndex.constFind(newEvent.replacedEvent()); + if (pIdx == eventsIndex.cend()) + return false; + + Q_ASSERT(q->isValidIndex(*pIdx)); + + auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; + if (ti->replacedBy() == newEvent.id()) { + qCDebug(STATE) << "Event" << ti->id() << "is already replaced with" + << newEvent.id(); + return true; + } + + // Make a new event from the redacted JSON and put it in the timeline + // instead of the redacted one. oldEvent will be deleted on return. + auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent)); + qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -1615,522 +2768,753 @@ Connection* Room::connection() const return d->connection; } -User* Room::localUser() const -{ - return connection()->user(); -} +User* Room::localUser() const { return connection()->user(); } -inline bool isRedaction(const RoomEventPtr& ep) +/// Whether the event is a redaction or a replacement +inline bool isEditing(const RoomEventPtr& ep) { Q_ASSERT(ep); - return is<RedactionEvent>(*ep); + if (is<RedactionEvent>(*ep)) + return true; + if (auto* msgEvent = eventCast<RoomMessageEvent>(ep)) + return !msgEvent->replacedEvent().isEmpty(); + + return false; } -void Room::Private::addNewMessageEvents(RoomEvents&& events) +Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return; + return Change::None; - // Pre-process redactions so that events that get redacted in the same - // batch landed in the timeline already redacted. - // NB: We have to store redaction events to the timeline too - see #220. - auto redactionIt = std::find_if(events.begin(), events.end(), isRedaction); - for(const auto& eptr: RoomEventsRange(redactionIt, events.end())) - if (auto* r = eventCast<RedactionEvent>(eptr)) - { - // Try to find the target in the timeline, then in the batch. - if (processRedaction(*r)) - continue; - auto targetIt = std::find_if(events.begin(), redactionIt, - [id=r->redactedEvent()] (const RoomEventPtr& ep) { - return ep->id() == id; - }); - if (targetIt != redactionIt) - *targetIt = makeRedacted(**targetIt, *r); - else - qCDebug(MAIN) << "Redaction" << r->id() - << "ignored: target event" << r->redactedEvent() - << "is not found"; - // If the target event comes later, it comes already redacted. - } + decryptIncomingEvents(events); + + QElapsedTimer et; + et.start(); - auto timelineSize = timeline.size(); - auto totalInserted = 0; - for (auto it = events.begin(); it != events.end();) { - auto nextPendingPair = findFirstOf(it, events.end(), - unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent); - auto nextPending = nextPendingPair.first; + // Pre-process redactions and edits so that events that get + // redacted/replaced in the same batch landed in the timeline already + // treated. + // NB: We have to store redacting/replacing events to the timeline too - + // see #220. + auto it = std::find_if(events.begin(), events.end(), isEditing); + for (const auto& eptr : RoomEventsRange(it, events.end())) { + if (auto* r = eventCast<RedactionEvent>(eptr)) { + // Try to find the target in the timeline, then in the batch. + if (processRedaction(*r)) + continue; + if (auto targetIt = std::find_if(events.begin(), events.end(), + [id = r->redactedEvent()](const RoomEventPtr& ep) { + return ep->id() == id; + }); targetIt != events.end()) + *targetIt = makeRedacted(**targetIt, *r); + else + qCDebug(STATE) + << "Redaction" << r->id() << "ignored: target event" + << r->redactedEvent() << "is not found"; + // If the target event comes later, it comes already redacted. + } + if (auto* msg = eventCast<RoomMessageEvent>(eptr); + msg && !msg->replacedEvent().isEmpty()) { + if (processReplacement(*msg)) + continue; + auto targetIt = std::find_if(events.begin(), it, + [id = msg->replacedEvent()](const RoomEventPtr& ep) { + return ep->id() == id; + }); + if (targetIt != it) + *targetIt = makeReplaced(**targetIt, *msg); + else // FIXME: hide the replacing event when target arrives later + qCDebug(EVENTS) + << "Replacing event" << msg->id() + << "ignored: target event" << msg->replacedEvent() + << "is not found"; + // Same as with redactions above, the replaced event coming + // later will come already with the new content. + } + } + } - if (it != nextPending) - { - RoomEventsRange eventsSpan { it, nextPending }; + // State changes arrive as a part of timeline; the current room state gets + // updated before merging events to the timeline because that's what + // clients historically expect. This may eventually change though if we + // postulate that the current state is only current between syncs but not + // within a sync. + Changes roomChanges {}; + for (const auto& eptr : events) + roomChanges |= q->processStateEvent(*eptr); + + auto timelineSize = timeline.size(); + size_t totalInserted = 0; + for (auto it = events.begin(); it != events.end();) { + auto nextPendingPair = + findFirstOf(it, events.end(), unsyncedEvents.begin(), + unsyncedEvents.end(), isEchoEvent); + const auto& remoteEcho = nextPendingPair.first; + const auto& localEcho = nextPendingPair.second; + + if (it != remoteEcho) { + RoomEventsRange eventsSpan { it, remoteEcho }; 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()); + emit q->addedMessages(firstInserted->index(), + timeline.back().index()); } - if (nextPending == events.end()) + if (remoteEcho == events.end()) break; - it = nextPending + 1; - emit q->pendingEventAboutToMerge(nextPending->get(), - nextPendingPair.second - unsyncedEvents.begin()); - qDebug(EVENTS) << "Merging pending event from transaction" - << (*nextPending)->transactionId() << "into" - << (*nextPending)->id(); - unsyncedEvents.erase(nextPendingPair.second); - if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) - { + it = remoteEcho + 1; + auto* nextPendingEvt = remoteEcho->get(); + const auto pendingEvtIdx = int(localEcho - unsyncedEvents.begin()); + if (localEcho->deliveryStatus() != EventStatus::ReachedServer) { + localEcho->setReachedServer(nextPendingEvt->id()); + emit q->pendingEventChanged(pendingEvtIdx); + } + emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); + qCDebug(MESSAGES) << "Merging pending event from transaction" + << nextPendingEvt->transactionId() << "into" + << nextPendingEvt->id(); + auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); + if (transfer.status != FileTransferInfo::None) + fileTransfers.insert(nextPendingEvt->id(), transfer); + // After emitting pendingEventAboutToMerge() above we cannot rely + // on the previously obtained localEcho staying valid + // because a signal handler may send another message, thereby altering + // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at + // its back so we can rely on the index staying valid at least. + unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); + if (auto insertedSize = moveEventsToTimeline({ 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) - if (auto* evt = it->viewAs<CallEventBase>()) + for (auto it = from; it != syncEdge(); ++it) + if (const auto* evt = it->viewAs<CallEvent>()) emit q->callEvent(q, evt); - if (totalInserted > 0) - { - qCDebug(MAIN) - << "Room" << q->objectName() << "received" << 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. - auto firstWriter = q->user((*from)->senderId()); - if (q->readMarker(firstWriter) != timeline.crend()) - { - promoteReadMarker(firstWriter, rev_iter_t(from) - 1); - qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() - << "to" << *q->readMarker(firstWriter); + if (totalInserted > 0) { + 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; + emit q->updatedEvent(relation.eventId); + } } - updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); + qCDebug(MESSAGES) << "Room" << q->objectName() << "received" + << totalInserted << "new events; the last event is now" + << timeline.back(); + + roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from)); + + // 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; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { - QElapsedTimer et; et.start(); const auto timelineSize = timeline.size(); dropDuplicateEvents(events); - RoomEventsRange normalEvents { - events.begin(), events.end() //remove_if(events.begin(), events.end(), isRedaction) - }; - if (normalEvents.empty()) + if (events.empty()) return; - emit q->aboutToAddHistoricalMessages(normalEvents); - const auto insertedSize = moveEventsToTimeline(normalEvents, Older); - const auto from = timeline.crend() - insertedSize; + decryptIncomingEvents(events); + + QElapsedTimer et; + et.start(); + 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 + // incorporated. + for (const auto& eptr : events) { + const auto& e = *eptr; + if (e.isStateEvent() + && !currentState.contains(e.matrixType(), e.stateKey())) { + changes |= q->processStateEvent(e); + } + } + + emit q->aboutToAddHistoricalMessages(events); + const auto insertedSize = moveEventsToTimeline(events, Older); + const auto from = historyEdge() - insertedSize; - qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize - << "past events; the oldest event is now" << timeline.front(); + 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()); - if (from <= q->readMarker()) - updateUnreadCount(from, timeline.crend()); - + 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); + } + } Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():" - << insertedSize << "event(s)," << et; -} - -bool Room::processStateEvent(const RoomEvent& e) -{ - return visit(e - , [this] (const RoomNameEvent& evt) { - d->name = evt.name(); - qCDebug(MAIN) << "Room name updated:" << d->name; + 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 Change::None; + + // Find a value (create an empty one if necessary) and get a reference + // to it, anticipating a change further in the function. + auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; + // Prepare for the state change + // clang-format off + const bool proceed = switchOnType(e + , [this, curStateEvent](const RoomMemberEvent& rme) { + // clang-format on + auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent); + auto* u = user(rme.userId()); + if (!u) { // Some terribly malformed user id? + qCCritical(MAIN) << "Could not get a user object for" + << rme.userId(); + return false; // Stay low and hope for the best... + } + const auto prevMembership = oldRme ? oldRme->membership() + : Membership::Leave; + switch (prevMembership) { + case Membership::Invite: + if (rme.membership() != prevMembership) { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case Membership::Join: + if (rme.membership() == Membership::Join) { + // rename/avatar change or no-op + if (rme.newDisplayName()) { + emit memberAboutToRename(u, *rme.newDisplayName()); + d->removeMemberFromMap(u); + } + if (!rme.newDisplayName() && !rme.newAvatarUrl()) { + qCWarning(MEMBERS) + << "No-op membership event for" << rme.userId() + << "- retaining the state"; + qCWarning(MEMBERS) << "The event dump:" << rme; + return false; + } + } else { + if (rme.membership() == Membership::Invite) + qCWarning(MAIN) + << "Membership change from Join to Invite:" << rme; + // whatever the new membership, it's no more Join + d->removeMemberFromMap(u); + emit userRemoved(u); + } + break; + 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 Membership::Undefined: + ; // A warning will be dropped in the post-processing block below + } return true; + // clang-format off } - , [this] (const RoomAliasesEvent& evt) { - d->aliases = evt.aliases(); - qCDebug(MAIN) << "Room aliases updated:" << d->aliases; + , [this, curStateEvent]( const EncryptionEvent& ee) { + // clang-format on + auto* oldEncEvt = + static_cast<const EncryptionEvent*>(curStateEvent); + if (ee.algorithm().isEmpty()) { + qWarning(STATE) + << "The encryption event for room" << objectName() + << "doesn't have 'algorithm' specified - ignoring"; + return false; + } + if (oldEncEvt + && oldEncEvt->encryption() != EncryptionType::Undefined) { + qCWarning(STATE) << "The room is already encrypted but a new" + " room encryption event arrived - ignoring"; + return false; + } return true; + // clang-format off } - , [this] (const RoomCanonicalAliasEvent& evt) { - d->canonicalAlias = evt.alias(); - if (!d->canonicalAlias.isEmpty()) - setObjectName(d->canonicalAlias); - qCDebug(MAIN) << "Room canonical alias updated:" - << d->canonicalAlias; - return true; + , true); // By default, go forward with the state change + // clang-format on + 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 = + std::exchange(curStateEvent, static_cast<const StateEvent*>(&e)); + Q_ASSERT(!oldStateEvent + || (oldStateEvent->matrixType() == e.matrixType() + && oldStateEvent->stateKey() == e.stateKey())); + if (is<RoomMemberEvent>(e)) + qCDebug(MEMBERS) << "Updated room member state:" << e; + else + qCDebug(STATE) << "Updated room state:" << e; + + // Update internal structures as per the change and work out the return value + + // clang-format off + const auto result = switchOnType(e + , [] (const RoomNameEvent&) { + return Change::Name; } - , [this] (const RoomTopicEvent& evt) { - d->topic = evt.topic(); - qCDebug(MAIN) << "Room topic updated:" << d->topic; - emit topicChanged(); - return false; + , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { + // clang-format on + setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); + const auto* oldCae = + static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent); + QStringList previousAltAliases {}; + if (oldCae) { + previousAltAliases = oldCae->altAliases(); + if (!oldCae->alias().isEmpty()) + previousAltAliases.push_back(oldCae->alias()); + } + + auto newAliases = cae.altAliases(); + if (!cae.alias().isEmpty()) + newAliases.push_front(cae.alias()); + + connection()->updateRoomAliases(id(), previousAltAliases, + newAliases); + return Change::Aliases; + // clang-format off + } + , [this] (const RoomPinnedEvent&) { + emit pinnedEventsChanged(); + return Change::Other; + } + , [] (const RoomTopicEvent&) { + return Change::Topic; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) - { - qCDebug(MAIN) << "Room avatar URL updated:" - << evt.url().toString(); emit avatarChanged(); - } - return false; + return Change::Avatar; } - , [this] (const RoomMemberEvent& evt) { + , [this,oldStateEvent] (const RoomMemberEvent& evt) { + // clang-format on auto* u = user(evt.userId()); - u->processEvent(evt, this); - if (u == localUser() && memberJoinState(u) == JoinState::Invite - && evt.isDirect()) - connection()->addToDirectChats(this, user(evt.senderId())); - - if( evt.membership() == MembershipType::Join ) - { - if (memberJoinState(u) != JoinState::Join) - { + const auto* oldMemberEvent = + static_cast<const RoomMemberEvent*>(oldStateEvent); + const auto prevMembership = oldMemberEvent + ? oldMemberEvent->membership() + : Membership::Leave; + switch (evt.membership()) { + case Membership::Join: + if (prevMembership != Membership::Join) { d->insertMemberIntoMap(u); - connect(u, &User::nameAboutToChange, this, - [=] (QString newName, QString, const Room* context) { - if (context == this) - emit memberAboutToRename(u, newName); - }); - connect(u, &User::nameChanged, this, - [=] (QString, QString oldName, const Room* context) { - if (context == this) - d->renameMember(u, oldName); - }); emit userAdded(u); + } else { + if (evt.newDisplayName()) { + d->insertMemberIntoMap(u); + emit memberRenamed(u); + } + if (evt.newAvatarUrl()) + emit memberAvatarChanged(u); } + break; + 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 Membership::Knock: + case Membership::Ban: + case Membership::Leave: + if (!d->membersLeft.contains(u)) + d->membersLeft.append(u); + break; + case Membership::Undefined: + qCWarning(MEMBERS) << "Ignored undefined membership type"; } - else if( evt.membership() == MembershipType::Leave ) - { - if (memberJoinState(u) == JoinState::Join) - { - if (!d->membersLeft.contains(u)) - d->membersLeft.append(u); - d->removeMemberFromMap(u->name(this), u); - emit userRemoved(u); - } - } - return false; + return Change::Members; + // clang-format off } - , [this] (const EncryptionEvent& evt) { - d->encryptionAlgorithm = evt.algorithm(); - qCDebug(MAIN) << "Encryption switched on in room" << id() - << "with algorithm" << d->encryptionAlgorithm; + , [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 false; + return Change::Other; } - ); + , [this] (const RoomTombstoneEvent& evt) { + const auto successorId = evt.successorRoomId(); + if (auto* successor = connection()->room(successorId)) + emit upgraded(evt.serverMessage(), successor); + else + connectUntil(connection(), &Connection::loadedRoomState, this, + [this,successorId,serverMsg=evt.serverMessage()] + (Room* newRoom) { + if (newRoom->id() != successorId) + return false; + emit upgraded(serverMsg, newRoom); + return true; + }); + + return Change::Other; + // clang-format off + } + , Change::Other); + // clang-format on + Q_ASSERT(result != Change::None); + return result; } -void Room::processEphemeralEvent(EventPtr&& event) -{ - QElapsedTimer et; et.start(); - if (auto* evt = eventCast<TypingEvent>(event)) - { - d->usersTyping.clear(); - for( const QString& userId: qAsConst(evt->users()) ) - { - auto u = user(userId); - if (memberJoinState(u) == JoinState::Join) - d->usersTyping.append(u); - } - if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" - << evt->users().size() << "users," << et; - emit typingChanged(); - } - if (auto* evt = eventCast<ReceiptEvent>(event)) - { - int totalReceipts = 0; - for( const auto &p: qAsConst(evt->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) - 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()) - d->setLastReadEvent(u, p.evtId); +Room::Changes Room::processEphemeralEvent(EventPtr&& event) +{ + Changes changes {}; + QElapsedTimer et; + et.start(); + switchOnType(*event, + [this, &et](const TypingEvent& evt) { + const auto& users = evt.users(); + d->usersTyping.clear(); + d->usersTyping.reserve(users.size()); // Assume all are members + for (const auto& userId : users) + if (isMember(userId)) + d->usersTyping.append(user(userId)); + + if (d->usersTyping.size() > 3 + || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) + << "Processing typing events from" << users.size() + << "user(s) in" << objectName() << "took" << et; + emit typingChanged(); + }, + [this, &changes, &et](const ReceiptEvent& evt) { + const auto& receiptsJson = evt.contentJson(); + QVector<QString> updatedUserIds; + // Most often (especially for bigger batches), receipts are + // scattered across events (an anecdotal evidence showed 1.2-1.3 + // receipts per event on average). + updatedUserIds.reserve(receiptsJson.size() * 2); + for (auto eventIt = receiptsJson.begin(); + eventIt != receiptsJson.end(); ++eventIt) { + const auto evtId = eventIt.key(); + const auto newMarker = findInTimeline(evtId); + if (newMarker == historyEdge()) + qDebug(EPHEMERAL) + << "Event" << evtId + << "is not found; saving read receipt(s) anyway"; + const auto reads = + eventIt.value().toObject().value("m.read"_ls).toObject(); + for (auto userIt = reads.begin(); userIt != reads.end(); + ++userIt) { + ReadReceipt rr{ evtId, + fromJson<QDateTime>( + userIt->toObject().value("ts"_ls)) }; + const auto userId = userIt.key(); + if (userId == connection()->userId()) { + // Local user is special, and will get a signal about + // its read receipt separately from (and before) a + // signal on everybody else. No particular reason, just + // less cumbersome code. + changes |= d->setLocalLastReadReceipt(newMarker, rr); + } else if (d->setLastReadReceipt(userId, newMarker, rr)) { + changes |= Change::Other; + updatedUserIds.push_back(userId); + } } } - } - if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 || - et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" - << evt->eventsWithReceipts().size() - << "event(s) with" << totalReceipts << "receipt(s)," << et; - } + if (updatedUserIds.size() > 10 + || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) + << "Processing" << updatedUserIds.size() + << "non-local receipt(s) on" << receiptsJson.size() + << "event(s) in" << objectName() << "took" << et; + if (!updatedUserIds.empty()) + emit lastReadEventChanged(updatedUserIds); + }); + return changes; } -void Room::processAccountDataEvent(EventPtr&& event) +Room::Changes Room::processAccountDataEvent(EventPtr&& event) { - if (auto* evt = eventCast<TagEvent>(event)) + Changes changes {}; + if (auto* evt = eventCast<TagEvent>(event)) { d->setTags(evt->tags()); - - if (auto* evt = eventCast<ReadMarkerEvent>(event)) - { - auto readEventId = evt->event_id(); - qCDebug(MAIN) << "Server-side read marker at" << readEventId; - d->serverReadMarker = readEventId; - const auto newMarker = findInTimeline(readEventId); - if (newMarker != timelineEdge()) - d->markMessagesAsRead(newMarker); - else - d->setLastReadEvent(localUser(), readEventId); + changes |= Change::Tags; } + + if (auto* evt = eventCast<const ReadMarkerEvent>(event)) + changes |= d->setFullyReadMarker(evt->eventId()); + // For all account data events auto& currentData = d->accountData[event->matrixType()]; // A polymorphic event-specific comparison might be a bit more // efficient; maaybe do it another day - if (!currentData || currentData->contentJson() != event->contentJson()) - { + if (!currentData || currentData->contentJson() != event->contentJson()) { emit accountDataAboutToChange(event->matrixType()); currentData = move(event); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); + qCDebug(STATE) << "Updated account data of type" + << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); + // 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 changes; } -QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const +template <typename ContT> +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const ContT& users) const { - // This is part 3(i,ii,iii) in the room displayname algorithm described - // in the CS spec (see also Room::Private::updateDisplayname() ). - // The spec requires to sort users lexicographically by state_key (user id) - // and use disambiguated display names of two topmost users excluding - // the current one to render the name of the room. - - // std::array is the leanest C++ container - std::array<User*, 2> first_two = { {nullptr, nullptr} }; + // To calculate room display name the spec requires to sort users + // lexicographically by state_key (user id) and use disambiguated + // display names of two topmost users excluding the current one to render + // the name of the room. The below code selects 3 topmost users, + // slightly extending the spec. + users_shortlist_t shortlist {}; // Prefill with nullptrs std::partial_sort_copy( - userlist.begin(), userlist.end(), - first_two.begin(), first_two.end(), + users.begin(), users.end(), shortlist.begin(), shortlist.end(), [this](const User* u1, const User* u2) { - // Filter out the "me" user so that it never hits the room name + // localUser(), if it's in the list, is sorted + // below all others return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); - } - ); - - // Spec extension. A single person in the chat but not the local user - // (the local user is invited). - if (userlist.size() == 1 && !isLocalUser(first_two.front()) && - joinState == JoinState::Invite) - return tr("Invitation from %1") - .arg(q->roomMembername(first_two.front())); - - // i. One-on-one chat. first_two[1] == localUser() in this case. - if (userlist.size() == 2) - return q->roomMembername(first_two[0]); - - // ii. Two users besides the current one. - if (userlist.size() == 3) - return tr("%1 and %2") - .arg(q->roomMembername(first_two[0]), - q->roomMembername(first_two[1])); - - // iii. More users. - if (userlist.size() > 3) - return tr("%1 and %Ln other(s)", "", userlist.size() - 3) - .arg(q->roomMembername(first_two[0])); + }); + return shortlist; +} - // userlist.size() < 2 - apparently, there's only current user in the room - return QString(); +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const QStringList& userIds) const +{ + QList<User*> users; + users.reserve(userIds.size()); + for (const auto& h : userIds) + users.push_back(q->user(h)); + return buildShortlist(users); } QString Room::Private::calculateDisplayname() const { - // CS spec, section 11.2.2.5 Calculating the display name for a room + // CS spec, section 13.2.2.5 Calculating the display name for a room // Numbers below refer to respective parts in the spec. // 1. Name (from m.room.name) - if (!name.isEmpty()) { - return name; + auto dispName = q->name(); + if (!dispName.isEmpty()) { + return dispName; } // 2. Canonical alias - if (!canonicalAlias.isEmpty()) - return canonicalAlias; - - // Using m.room.aliases in naming is explicitly discouraged by the spec - //if (!aliases.empty() && !aliases.at(0).isEmpty()) - // return aliases.at(0); - - // 3. Room members - QString topMemberNames = roomNameFromMemberNames(membersMap.values()); - if (!topMemberNames.isEmpty()) - return topMemberNames; - - // 4. Users that previously left the room - topMemberNames = roomNameFromMemberNames(membersLeft); - if (!topMemberNames.isEmpty()) - return tr("Empty room (was: %1)").arg(topMemberNames); + dispName = q->canonicalAlias(); + if (!dispName.isEmpty()) + return dispName; + + // 3. m.room.aliases - only local aliases, subject for further removal + const auto aliases = q->aliases(); + if (!aliases.isEmpty()) + return aliases.front(); + + // 4. m.heroes and m.room.member + // From here on, we use a more general algorithm than the spec describes + // in order to provide back-compatibility with pre-MSC688 servers. + + // Supplementary code: build the shortlist of users whose names + // will be used to construct the room name. Takes into account MSC688's + // "heroes" if available. + const bool localUserIsIn = joinState == JoinState::Join; + const bool emptyRoom = + membersMap.isEmpty() + || (membersMap.size() == 1 && isLocalUser(*membersMap.cbegin())); + const bool nonEmptySummary = summary.heroes && !summary.heroes->empty(); + auto shortlist = nonEmptySummary ? buildShortlist(*summary.heroes) + : !emptyRoom ? buildShortlist(membersMap) + : users_shortlist_t {}; + + // When the heroes list is there, we can rely on it. If the heroes list is + // missing, the below code gathers invited, or, if there are no invitees, + // left members. + if (!shortlist.front() && localUserIsIn) + shortlist = buildShortlist(usersInvited); + + if (!shortlist.front()) + shortlist = buildShortlist(membersLeft); + + QStringList names; + for (const auto* u : shortlist) { + if (u == nullptr || isLocalUser(u)) + break; + // Only disambiguate if the room is not empty + names.push_back(u->displayname(emptyRoom ? nullptr : q)); + } - // 5. Fail miserably + const auto usersCountExceptLocal = + !emptyRoom + ? q->joinedCount() - int(joinState == JoinState::Join) + : !usersInvited.empty() + ? usersInvited.count() + : membersLeft.size() - int(joinState == JoinState::Leave); + if (usersCountExceptLocal > int(shortlist.size())) + names << tr( + "%Ln other(s)", + "Used to make a room name from user names: A, B and _N others_", + usersCountExceptLocal - int(shortlist.size())); + const auto namesList = QLocale().createSeparatedList(names); + + // Room members + if (!emptyRoom) + return namesList; + + // (Spec extension) Invited users + if (!usersInvited.empty()) + return tr("Empty room (invited: %1)").arg(namesList); + + // Users that previously left the room + if (!membersLeft.isEmpty()) + return tr("Empty room (was: %1)").arg(namesList); + + // Fail miserably return tr("Empty room (%1)").arg(id); } void Room::Private::updateDisplayname() { auto swappedName = calculateDisplayname(); - if (swappedName != displayname) - { + if (swappedName != displayname) { emit q->displaynameAboutToChange(q); swap(displayname, swappedName); - qDebug(MAIN) << q->objectName() << "has changed display name from" + qCDebug(MAIN) << q->objectName() << "has changed display name from" << swappedName << "to" << displayname; emit q->displaynameChanged(q, swappedName); } } -void appendStateEvent(QJsonArray& events, const QString& type, - const QJsonObject& content, const QString& stateKey = {}) -{ - if (!content.isEmpty() || !stateKey.isEmpty()) - { - auto json = basicEventJson(type, content); - json.insert(QStringLiteral("state_key"), stateKey); - events.append(json); - } -} - -#define ADD_STATE_EVENT(events, type, name, content) \ - appendStateEvent((events), QStringLiteral(type), \ - {{ QStringLiteral(name), content }}); - -void appendEvent(QJsonArray& events, const QString& type, - const QJsonObject& content) -{ - if (!content.isEmpty()) - events.append(basicEventJson(type, content)); -} - -template <typename EvtT> -void appendEvent(QJsonArray& events, const EvtT& event) -{ - appendEvent(events, EvtT::matrixTypeId(), event.toJson()); -} - QJsonObject Room::Private::toJson() const { - QElapsedTimer et; et.start(); + QElapsedTimer et; + et.start(); QJsonObject result; + addParam<IfNotEmpty>(result, QStringLiteral("summary"), summary); { QJsonArray stateEvents; - ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name); - ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic); - ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url", - avatar.url().toString()); - ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases", - QJsonArray::fromStringList(aliases)); - ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias", - canonicalAlias); - ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm", - encryptionAlgorithm); - - for (const auto *m : membersMap) - appendStateEvent(stateEvents, QStringLiteral("m.room.member"), - { { QStringLiteral("membership"), QStringLiteral("join") } - , { QStringLiteral("displayname"), m->rawName(q) } - , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() } - }, m->id()); - - const auto stateObjName = joinState == JoinState::Invite ? - QStringLiteral("invite_state") : QStringLiteral("state"); + for (const auto* evt : currentState) { + Q_ASSERT(evt->isStateEvent()); + if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) + || evt->contentJson().isEmpty()) + continue; + + auto json = evt->fullJson(); + auto unsignedJson = evt->unsignedJson(); + unsignedJson.remove(QStringLiteral("prev_content")); + json[UnsignedKeyL] = unsignedJson; + stateEvents.append(json); + } + + const auto stateObjName = joinState == JoinState::Invite + ? QStringLiteral("invite_state") + : QStringLiteral("state"); result.insert(stateObjName, - QJsonObject {{ QStringLiteral("events"), stateEvents }}); + QJsonObject { { QStringLiteral("events"), stateEvents } }); } - QJsonArray accountDataEvents; - if (!accountData.empty()) - { - for (const auto& e: accountData) - appendEvent(accountDataEvents, e.first, e.second->contentJson()); + if (!accountData.empty()) { + QJsonArray accountDataEvents; + for (const auto& e : accountData) { + if (!e.second->contentJson().isEmpty()) + accountDataEvents.append(e.second->fullJson()); + } + result.insert(QStringLiteral("account_data"), + QJsonObject { + { QStringLiteral("events"), accountDataEvents } }); } - result.insert(QStringLiteral("account_data"), - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); - - QJsonObject unreadNotifObj - { { SyncRoomData::UnreadCountKey, unreadMessages } }; - if (highlightCount > 0) - unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount); - if (notificationCount > 0) - unreadNotifObj.insert(QStringLiteral("notification_count"), notificationCount); + 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(QStringLiteral("unread_notifications"), unreadNotifObj); + 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; } -QJsonObject Room::toJson() const -{ - return d->toJson(); -} +QJsonObject Room::toJson() const { return d->toJson(); } -MemberSorter Room::memberSorter() const -{ - return MemberSorter(this); -} +MemberSorter Room::memberSorter() const { return MemberSorter(this); } -bool MemberSorter::operator()(User *u1, User *u2) const +bool MemberSorter::operator()(User* u1, User* u2) const { - return operator()(u1, room->roomMembername(u2)); + 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->roomMembername(u1); + 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>(EncryptionType::MegolmV1AesSha2); +} @@ -1,463 +1,1068 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> +// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> +// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "jobs/syncjob.h" -#include "events/roommessageevent.h" -#include "events/accountdataevents.h" +#include "connection.h" +#include "roomstateview.h" #include "eventitem.h" -#include "joinstate.h" +#include "quotient_common.h" + +#include "csapi/message_pagination.h" + +#include "events/accountdataevents.h" +#include "events/encryptedevent.h" +#include "events/roomkeyevent.h" +#include "events/roommessageevent.h" +#include "events/roomcreateevent.h" +#include "events/roomtombstoneevent.h" +#include "events/eventrelation.h" +#include <QtCore/QJsonObject> #include <QtGui/QImage> -#include <memory> #include <deque> +#include <memory> #include <utility> -namespace QMatrixClient -{ - class Event; - class RoomMemberEvent; - class Connection; - class User; - class MemberSorter; - class LeaveRoomJob; - class SetRoomStateWithKeyJob; - class RedactEventJob; - - class FileTransferInfo +namespace Quotient { +class Event; +class Avatar; +class SyncRoomData; +class RoomMemberEvent; +class User; +class MemberSorter; +class LeaveRoomJob; +class SetRoomStateWithKeyJob; +class RedactEventJob; + +/** The data structure used to expose file transfer information to views + * + * This is specifically tuned to work with QML exposing all traits as + * Q_PROPERTY values. + */ +class QUOTIENT_API FileTransferInfo { + Q_GADGET + Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) + Q_PROPERTY(bool active READ active CONSTANT) + Q_PROPERTY(bool started READ started CONSTANT) + Q_PROPERTY(bool completed READ completed CONSTANT) + Q_PROPERTY(bool failed READ failed CONSTANT) + Q_PROPERTY(int progress MEMBER progress CONSTANT) + Q_PROPERTY(int total MEMBER total CONSTANT) + Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) + Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) +public: + enum Status { None, Started, Completed, Failed, Cancelled }; + Status status = None; + bool isUpload = false; + int progress = 0; + int total = -1; + QUrl localDir {}; + QUrl localPath {}; + + bool started() const { return status == Started; } + bool completed() const { return status == Completed; } + bool active() const { return started() || completed(); } + bool failed() const { return status == Failed; } +}; + +//! \brief Data structure for a room member's read receipt +//! \sa Room::lastReadReceipt +class QUOTIENT_API 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 { - Q_GADGET - Q_PROPERTY(bool active READ active CONSTANT) - Q_PROPERTY(bool completed READ completed CONSTANT) - Q_PROPERTY(bool failed READ failed CONSTANT) - Q_PROPERTY(int progress MEMBER progress CONSTANT) - Q_PROPERTY(int total MEMBER total CONSTANT) - Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) - Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) - public: - enum Status { None, Started, Completed, Failed }; - Status status = None; - int progress = 0; - int total = -1; - QUrl localDir { }; - QUrl localPath { }; - - bool active() const - { return status == Started || status == Completed; } - bool completed() const { return status == Completed; } - bool failed() const { return status == Failed; } + 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(Type) + + Type type = None; + +private: + Q_GADGET + Q_PROPERTY(Type type MEMBER type CONSTANT) +}; + +class QUOTIENT_API Room : public QObject { + Q_OBJECT + Q_PROPERTY(Connection* connection READ connection CONSTANT) + Q_PROPERTY(User* localUser READ localUser CONSTANT) + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) + Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated) + Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) + Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) + Q_PROPERTY(QString name READ name NOTIFY namesChanged) + Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) + Q_PROPERTY(QStringList altAliases READ altAliases NOTIFY namesChanged) + Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) + Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) + Q_PROPERTY(QStringList pinnedEventIds READ pinnedEventIds WRITE setPinnedEvents + NOTIFY pinnedEventsChanged) + Q_PROPERTY(QString displayNameForHtml READ displayNameForHtml NOTIFY displaynameChanged) + Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) + Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged + STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) + Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) + + Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) + Q_PROPERTY(QStringList memberNames READ safeMemberNames NOTIFY memberListChanged) + Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) + Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) + Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) + + Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY + displayedChanged) + Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE + setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) + 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 + 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 STORED false) + Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged STORED false) + + Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY + eventsHistoryJobChanged) + +public: + using Timeline = std::deque<TimelineItem>; + using PendingEvents = std::vector<PendingEventItem>; + using RelatedEvents = QVector<const RoomEvent*>; + using rev_iter_t = Timeline::const_reverse_iterator; + using timeline_iter_t = Timeline::const_iterator; + + //! \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 }; + QUO_DECLARE_FLAGS(Changes, Change) - class Room: public QObject - { - Q_OBJECT - Q_PROPERTY(Connection* connection READ connection CONSTANT) - Q_PROPERTY(User* localUser READ localUser CONSTANT) - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(QString name READ name NOTIFY namesChanged) - Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) - Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) - Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged) - Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) - Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) - - Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) - Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) - Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) - - Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) - Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) - Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) - - Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) - Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) - Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) - Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) - Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) - Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) - - public: - using Timeline = std::deque<TimelineItem>; - using PendingEvents = std::vector<PendingEventItem>; - using rev_iter_t = Timeline::const_reverse_iterator; - using timeline_iter_t = Timeline::const_iterator; - - Room(Connection* connection, QString id, JoinState initialJoinState); - ~Room() override; - - // Property accessors - - Connection* connection() const; - User* localUser() const; - const QString& id() const; - QString name() const; - QStringList aliases() const; - QString canonicalAlias() const; - QString displayName() const; - QString topic() const; - QString avatarMediaId() const; - QUrl avatarUrl() const; - Q_INVOKABLE JoinState joinState() const; - Q_INVOKABLE QList<User*> usersTyping() const; - QList<User*> membersLeft() const; - - Q_INVOKABLE QList<User*> users() const; - QStringList memberNames() const; - int memberCount() const; - int timelineSize() const; - bool usesEncryption() const; - - /** - * Returns a square room avatar with the given size and requests it - * from the network if needed - * \return a pixmap with the avatar or a placeholder if there's none - * available yet - */ - Q_INVOKABLE QImage avatar(int dimension); - /** - * Returns a room avatar with the given dimensions and requests it - * from the network if needed - * \return a pixmap with the avatar or a placeholder if there's none - * available yet - */ - Q_INVOKABLE QImage avatar(int width, int height); - - /** - * \brief Get a user object for a given user id - * This is the recommended way to get a user object in a room - * context. The actual object type may be changed in further - * versions to provide room-specific user information (display name, - * avatar etc.). - * \note The method will return a valid user regardless of - * the membership. - */ - Q_INVOKABLE User* user(const QString& userId) const; - - /** - * \brief Check the join state of a given user in this room - * - * \note Banned and invited users are not tracked for now (Leave - * will be returned for them). - * - * \return either of Join, Leave, depending on the given - * user's state in the room - */ - Q_INVOKABLE JoinState memberJoinState(User* user) const; - - /** - * Get a disambiguated name for a given user in - * the context of the room - */ - Q_INVOKABLE QString roomMembername(const User* u) const; - /** - * Get a disambiguated name for a user with this id in - * the context of the room - */ - Q_INVOKABLE QString roomMembername(const QString& userId) const; - - const Timeline& messageEvents() const; - const PendingEvents& pendingEvents() const; - /** - * A convenience method returning the read marker to - * the before-oldest message - */ - rev_iter_t timelineEdge() const; - Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; - Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; - Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const; - - rev_iter_t findInTimeline(TimelineItem::index_t index) const; - rev_iter_t findInTimeline(const QString& evtId) const; - - bool displayed() const; - void setDisplayed(bool displayed = true); - QString firstDisplayedEventId() const; - rev_iter_t firstDisplayedMarker() const; - void setFirstDisplayedEventId(const QString& eventId); - void setFirstDisplayedEvent(TimelineItem::index_t index); - QString lastDisplayedEventId() const; - rev_iter_t lastDisplayedMarker() const; - void setLastDisplayedEventId(const QString& eventId); - void setLastDisplayedEvent(TimelineItem::index_t index); - - rev_iter_t readMarker(const User* user) const; - rev_iter_t readMarker() const; - 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 - 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. - */ - int unreadCount() const; - - Q_INVOKABLE int notificationCount() const; - Q_INVOKABLE void resetNotificationCount(); - Q_INVOKABLE int highlightCount() const; - Q_INVOKABLE void resetHighlightCount(); - - /** Check whether the room has account data of the given type - * Tags and read markers are not supported by this method _yet_. - */ - bool hasAccountData(const QString& type) const; - - /** Get a generic account data event of the given type - * This returns a generic hashmap for any room account data event - * stored on the server. Tags and read markers cannot be retrieved - * using this method _yet_. - */ - const EventPtr& accountData(const QString& type) const; - - QStringList tagNames() const; - TagsMap tags() const; - TagRecord tag(const QString& name) const; - - /** Add a new tag to this room - * If this room already has this tag, nothing happens. If it's a new - * tag for the room, the respective tag record is added to the set - * of tags and the new set is sent to the server to update other - * clients. - */ - void addTag(const QString& name, const TagRecord& record = {}); - Q_INVOKABLE void addTag(const QString& name, float order); - - /// Remove a tag from the room - Q_INVOKABLE void removeTag(const QString& name); - - /** Overwrite the room's tags - * This completely replaces the existing room's tags with a set - * of new ones and updates the new set on the server. Unlike - * most other methods in Room, this one sends a signal about changes - * immediately, not waiting for confirmation from the server - * (because tags are saved in account data rather than in shared - * room state). - */ - void setTags(TagsMap newTags); - - /// Check whether the list of tags has m.favourite - bool isFavourite() const; - /// Check whether the list of tags has m.lowpriority - bool isLowPriority() const; - - /// Check whether this room is a direct chat - Q_INVOKABLE bool isDirectChat() const; - - /// Get the list of users this room is a direct chat with - QList<User*> directChatUsers() const; - - Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); - Q_INVOKABLE QUrl urlToDownload(const QString& eventId); - Q_INVOKABLE QString fileNameToDownload(const QString& eventId); - Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; - - /** Pretty-prints plain text into HTML - * As of now, it's exactly the same as QMatrixClient::prettyPrint(); - * in the future, it will also linkify room aliases, mxids etc. - * using the room context. - */ - QString prettyPrint(const QString& plainText) const; - - MemberSorter memberSorter() const; - - Q_INVOKABLE void inviteCall(const QString& callId, - const int lifetime, const QString& sdp); - Q_INVOKABLE void sendCallCandidates(const QString& callId, - const QJsonArray& candidates); - Q_INVOKABLE void answerCall(const QString& callId, const int lifetime, - const QString& sdp); - Q_INVOKABLE void answerCall(const QString& callId, - const QString& sdp); - Q_INVOKABLE void hangupCall(const QString& callId); - Q_INVOKABLE bool supportsCalls() const; - - public slots: - QString postMessage(const QString& plainText, MessageEventType type); - QString postPlainText(const QString& plainText); - QString postHtmlMessage(const QString& plainText, - const QString& html, MessageEventType type); - QString postHtmlText(const QString& plainText, const QString& html); - /** Post a pre-created room message event - * - * Takes ownership of the event, deleting it once the matching one - * arrives with the sync - * \return transaction id associated with the event. - */ - QString postEvent(RoomEvent* event); - QString postJson(const QString& matrixType, - const QJsonObject& eventContent); - QString retryMessage(const QString& txnId); - void discardMessage(const QString& txnId); - void setName(const QString& newName); - void setCanonicalAlias(const QString& newAlias); - void setTopic(const QString& newTopic); - - void getPreviousContent(int limit = 10); - - void inviteToRoom(const QString& memberId); - LeaveRoomJob* leaveRoom(); - SetRoomStateWithKeyJob* setMemberState( - const QString& memberId, const RoomMemberEvent& event) const; - void kickMember(const QString& memberId, const QString& reason = {}); - void ban(const QString& userId, const QString& reason = {}); - void unban(const QString& userId); - void redactEvent(const QString& eventId, - const QString& reason = {}); - - void uploadFile(const QString& id, const QUrl& localFilename, - const QString& overrideContentType = {}); - // If localFilename is empty a temporary file is created - void downloadFile(const QString& eventId, - const QUrl& localFilename = {}); - void cancelFileTransfer(const QString& id); - - /// Mark all messages in the room as read - void markAllMessagesAsRead(); - - signals: - void aboutToAddHistoricalMessages(RoomEventsRange events); - void aboutToAddNewMessages(RoomEventsRange events); - void addedMessages(int fromIndex, int toIndex); - void pendingEventAboutToAdd(); - void pendingEventAdded(); - void pendingEventAboutToMerge(RoomEvent* serverEvent, - int pendingEventIndex); - void pendingEventMerged(); - void pendingEventAboutToDiscard(int pendingEventIndex); - void pendingEventDiscarded(); - void pendingEventChanged(int pendingEventIndex); - - /** - * \brief The room name, the canonical alias or other aliases changed - * - * Not triggered when displayname changes. - */ - void namesChanged(Room* room); - void displaynameAboutToChange(Room* room); - void displaynameChanged(Room* room, QString oldName); - void topicChanged(); - void avatarChanged(); - void userAdded(User* user); - void userRemoved(User* user); - void memberAboutToRename(User* user, QString newName); - void memberRenamed(User* user); - void memberListChanged(); - void encryption(); - - void joinStateChanged(JoinState oldState, JoinState newState); - void typingChanged(); - - void highlightCountChanged(Room* room); - void notificationCountChanged(Room* room); - - void displayedChanged(bool displayed); - void firstDisplayedEventChanged(); - void lastDisplayedEventChanged(); - void lastReadEventChanged(User* user); - void readMarkerMoved(QString fromEventId, QString toEventId); - void readMarkerForUserMoved(User* user, QString fromEventId, QString toEventId); - void unreadMessagesChanged(Room* room); - - void accountDataAboutToChange(QString type); - void accountDataChanged(QString type); - void tagsAboutToChange(); - void tagsChanged(); - - void replacedEvent(const RoomEvent* newEvent, - const RoomEvent* oldEvent); - - void newFileTransfer(QString id, QUrl localFile); - 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); - - void callEvent(Room* room, const RoomEvent* event); - /// The room is about to be deleted - void beforeDestruction(Room*); - - public: // Used by Connection - not a part of the client API - QJsonObject toJson() const; - void updateData(SyncRoomData&& data ); - - // Clients should use Connection::joinRoom() and Room::leaveRoom() - // to change the room state - void setJoinState( JoinState state ); - - protected: - /// Returns true if any of room names/aliases has changed - virtual bool processStateEvent(const RoomEvent& e); - virtual void processEphemeralEvent(EventPtr&& event); - virtual void processAccountDataEvent(EventPtr&& event); - virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } - virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { } - virtual void onRedaction(const RoomEvent& /*prevEvent*/, - const RoomEvent& /*after*/) { } - - private: - class Private; - Private* d; + Room(Connection* connection, QString id, JoinState initialJoinState); + ~Room() override; + + // Property accessors + + Connection* connection() const; + User* localUser() const; + const QString& id() const; + QString version() const; + bool isUnstable() const; + QString predecessorId() const; + /// Room predecessor + /** This function validates that the predecessor has a tombstone and + * the tombstone refers to the current room. If that's not the case, + * or if the predecessor is in a join state not matching \p stateFilter, + * the function returns nullptr. + */ + Room* predecessor(JoinStates statesFilter = JoinState::Invite + | JoinState::Join) const; + QString successorId() const; + /// Room successor + /** This function validates that the successor room's creation event + * refers to the current room. If that's not the case, or if the successor + * is in a join state not matching \p stateFilter, it returns nullptr. + */ + Room* successor(JoinStates statesFilter = JoinState::Invite + | JoinState::Join) const; + QString name() 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; + const Avatar& avatarObject() const; + Q_INVOKABLE JoinState joinState() const; + Q_INVOKABLE QList<Quotient::User*> usersTyping() const; + QList<User*> membersLeft() const; + + Q_INVOKABLE QList<Quotient::User*> users() const; + Q_DECL_DEPRECATED_X("Use safeMemberNames() or htmlSafeMemberNames() instead") // + QStringList memberNames() const; + QStringList safeMemberNames() const; + QStringList htmlSafeMemberNames() const; + int timelineSize() const; + bool usesEncryption() const; + RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); + void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderId, const QString& olmSessionId); + int joinedCount() const; + int invitedCount() const; + int totalMemberCount() const; + + GetRoomEventsJob* eventsHistoryJob() const; + + /** + * Returns a square room avatar with the given size and requests it + * from the network if needed + * \return a pixmap with the avatar or a placeholder if there's none + * available yet + */ + Q_INVOKABLE QImage avatar(int dimension); + /** + * Returns a room avatar with the given dimensions and requests it + * from the network if needed + * \return a pixmap with the avatar or a placeholder if there's none + * available yet + */ + Q_INVOKABLE QImage avatar(int width, int height); + + /** + * \brief Get a user object for a given user id + * This is the recommended way to get a user object in a room + * context. The actual object type may be changed in further + * versions to provide room-specific user information (display name, + * avatar etc.). + * \note The method will return a valid user regardless of + * the membership. + */ + Q_INVOKABLE Quotient::User* user(const QString& userId) const; + + /** + * \brief Check the join state of a given user in this room + * + * \note Banned and invited users are not tracked separately for now (Leave + * will be returned for them). + * + * \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 + 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 + Q_DECL_DEPRECATED_X("Use safeMemberName() instead") + Q_INVOKABLE QString roomMembername(const QString& userId) const; + + /*! + * \brief Get a disambiguated name for the member with the given MXID + * + * This function should only be used for non-UI code; consider using + * safeMemberName() or htmlSafeMemberName() for displayed strings. + */ + Q_INVOKABLE QString disambiguatedMemberName(const QString& mxId) const; + + /*! Get a display-safe member name in the context of this room + * + * Display-safe means disambiguated and without RLO/LRO markers + * (see https://github.com/quotient-im/Quaternion/issues/545). + */ + Q_INVOKABLE QString safeMemberName(const QString& userId) const; + + /*! Get an HTML-safe member name in the context of this room + * + * This function adds HTML escaping on top of safeMemberName() safeguards. + */ + Q_INVOKABLE QString htmlSafeMemberName(const QString& userId) const; + + //! \brief Get an avatar for the member with the given MXID + QUrl memberAvatarUrl(const QString& mxId) const; + + const Timeline& messageEvents() const; + const PendingEvents& pendingEvents() const; + + /// Check whether all historical messages are already loaded + /** + * \return true if the "oldest" event in the timeline is + * a room creation event and there's no further history + * to load; false otherwise + */ + bool allHistoryLoaded() const; + /** + * A convenience method returning the read marker to the position + * before the "oldest" event; same as messageEvents().crend() + */ + rev_iter_t historyEdge() const; + /** + * A convenience method returning the iterator beyond the latest + * arrived event; same as messageEvents().cend() + */ + Timeline::const_iterator syncEdge() const; + Q_INVOKABLE Quotient::TimelineItem::index_t minTimelineIndex() const; + Q_INVOKABLE Quotient::TimelineItem::index_t maxTimelineIndex() const; + Q_INVOKABLE bool + isValidIndex(Quotient::TimelineItem::index_t timelineIndex) const; + + rev_iter_t findInTimeline(TimelineItem::index_t index) const; + rev_iter_t findInTimeline(const QString& evtId) const; + PendingEvents::iterator findPendingEvent(const QString& txnId); + PendingEvents::const_iterator findPendingEvent(const QString& txnId) const; + + const RelatedEvents relatedEvents(const QString& evtId, + EventRelation::reltypeid_t relType) const; + const RelatedEvents relatedEvents(const RoomEvent& evt, + EventRelation::reltypeid_t relType) const; + + const RoomCreateEvent* creation() const; + const RoomTombstoneEvent* tombstone() const; + + bool displayed() const; + /// Mark the room as currently displayed to the user + /** + * Marking the room displayed causes the room to obtain the full + * list of members if it's been lazy-loaded before; in the future + * it may do more things bound to "screen time" of the room, e.g. + * measure that "screen time". + */ + void setDisplayed(bool displayed = true); + QString firstDisplayedEventId() const; + rev_iter_t firstDisplayedMarker() const; + void setFirstDisplayedEventId(const QString& eventId); + void setFirstDisplayedEvent(TimelineItem::index_t index); + QString lastDisplayedEventId() const; + rev_iter_t lastDisplayedMarker() const; + 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; + + //! \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; + + //! \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; + + //! \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(); + + //! \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 + * Tags and read markers are not supported by this method _yet_. + */ + bool hasAccountData(const QString& type) const; + + /** Get a generic account data event of the given type + * This returns a generic hash map for any room account data event + * stored on the server. Tags and read markers cannot be retrieved + * using this method _yet_. + */ + const EventPtr& accountData(const QString& type) const; + + QStringList tagNames() const; + TagsMap tags() const; + TagRecord tag(const QString& name) const; + + /** Add a new tag to this room + * If this room already has this tag, nothing happens. If it's a new + * tag for the room, the respective tag record is added to the set + * of tags and the new set is sent to the server to update other + * clients. + */ + void addTag(const QString& name, const TagRecord& record = {}); + Q_INVOKABLE void addTag(const QString& name, float order); + + /// Remove a tag from the room + Q_INVOKABLE void removeTag(const QString& name); + + /// The scope to apply an action on + /*! This enumeration is used to pick a strategy to propagate certain + * actions on the room to its predecessors and successors. + */ + enum ActionScope { + ThisRoomOnly, ///< Do not apply to predecessors and successors + WithinSameState, ///< Apply to predecessors and successors in the same + ///< state as the current one + OmitLeftState, ///< Apply to all reachable predecessors and successors + ///< except those in Leave state + WholeSequence ///< Apply to all reachable predecessors and successors }; - class MemberSorter + /** Overwrite the room's tags + * This completely replaces the existing room's tags with a set + * of new ones and updates the new set on the server. Unlike + * most other methods in Room, this one sends a signal about changes + * immediately, not waiting for confirmation from the server + * (because tags are saved in account data rather than in shared + * room state). + * \param applyOn setting this to Room::OnAllConversations will set tags + * on this and all _known_ predecessors and successors; + * by default only the current room is changed + */ + void setTags(TagsMap newTags, ActionScope applyOn = ThisRoomOnly); + + /// Check whether the list of tags has m.favourite + bool isFavourite() const; + /// Check whether the list of tags has m.lowpriority + bool isLowPriority() const; + /// Check whether this room is for server notices (MSC1452) + bool isServerNoticeRoom() const; + + /// Check whether this room is a direct chat + Q_INVOKABLE bool isDirectChat() const; + + /// 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; + + /// Get a file name for downloading for a given event id + /*! + * The event MUST be RoomMessageEvent and have content + * for downloading. \sa RoomMessageEvent::hasContent + */ + Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; + + /// Get information on file upload/download + /*! + * \param id uploads are identified by the corresponding event's + * transactionId (because uploads are done before + * the event is even sent), while downloads are using + * the normal event id for identifier. + */ + Q_INVOKABLE Quotient::FileTransferInfo + fileTransferInfo(const QString& id) const; + + /// Get the URL to the actual file source in a unified way + /*! + * For uploads it will return a URL to a local file; for downloads + * the URL will be taken from the corresponding room event. + */ + Q_INVOKABLE QUrl fileSource(const QString& id) const; + + /** Pretty-prints plain text into HTML + * As of now, it's exactly the same as Quotient::prettyPrint(); + * in the future, it will also linkify room aliases, mxids etc. + * using the room context. + */ + Q_INVOKABLE QString prettyPrint(const QString& plainText) const; + + MemberSorter memberSorter() const; + + Q_INVOKABLE bool supportsCalls() const; + + /// Whether the current user is allowed to upgrade the room + Q_INVOKABLE bool canSwitchVersions() const; + + /// Get a state event with the given event type and state key + /*! This method returns a (potentially empty) state event corresponding + * to the pair of event type \p evtType and state key \p stateKey. + */ + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // + const StateEvent* getCurrentState(const QString& evtType, + const QString& stateKey = {}) const; + + /// Get a state event with the given event type and state key + /*! This is a typesafe overload that accepts a C++ event type instead of + * its Matrix name. + */ + template <typename EvT> + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // + const EvT* getCurrentState(const QString& stateKey = {}) const { - public: - explicit MemberSorter(const Room* r) : room(r) { } + QT_IGNORE_DEPRECATIONS(const auto* evt = eventCast<const EvT>( + getCurrentState(EvT::TypeId, stateKey));) + Q_ASSERT(evt); + Q_ASSERT(evt->matrixType() == EvT::TypeId + && evt->stateKey() == stateKey); + return evt; + } - bool operator()(User* u1, User* u2) const; - bool operator()(User* u1, const QString& u2name) const; + /// \brief Get the current room state + RoomStateView currentState() const; - template <typename ContT, typename ValT> - typename ContT::size_type lowerBoundIndex(const ContT& c, - const ValT& v) const - { - return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); - } + //! Send a request to update the room state with the given event + SetRoomStateWithKeyJob* setState(const StateEvent& evt); - private: - const Room* room; - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) + //! \brief Set a state event of the given type with the given arguments + //! + //! This typesafe overload attempts to send a state event with the type + //! \p EvT and the content defined by \p args. Specifically, the function + //! constructs a temporary object of type \p EvT with its content + //! list-initialised from \p args, and sends a request to the homeserver + //! using the Matrix event type defined by \p EvT and the event content + //! produced via EvT::contentJson(). + //! + //! \note This call is not suitable for events that assume non-empty + //! stateKey, such as member events; for those you have to create + //! a temporary event object yourself and use the setState() overload + //! that accepts StateEvent const-ref. + template <typename EvT, typename... ArgTs> + auto setState(ArgTs&&... args) + { + return setState(EvT(std::forward<ArgTs>(args)...)); + } + +public Q_SLOTS: + /** Check whether the room should be upgraded */ + void checkVersion(); + + QString postMessage(const QString& plainText, MessageEventType type); + QString postPlainText(const QString& plainText); + QString postHtmlMessage(const QString& plainText, const QString& html, + MessageEventType type = MessageEventType::Text); + 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 + * arrives with the sync + * \return transaction id associated with the event. + */ + QString postEvent(RoomEvent* event); + QString postJson(const QString& matrixType, const QJsonObject& eventContent); + QString retryMessage(const QString& txnId); + void discardMessage(const QString& txnId); + + //! Send a request to update the room state based on freeform inputs + SetRoomStateWithKeyJob* setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson); + void setName(const QString& newName); + void setCanonicalAlias(const QString& newAlias); + void setPinnedEvents(const QStringList& events); + /// Set room aliases on the user's current server + void setLocalAliases(const QStringList& aliases); + void setTopic(const QString& newTopic); + + /// You shouldn't normally call this method; it's here for debugging + void refreshDisplayName(); + + void getPreviousContent(int limit = 10, const QString &filter = {}); + + void inviteToRoom(const QString& memberId); + LeaveRoomJob* leaveRoom(); + void kickMember(const QString& memberId, const QString& reason = {}); + void ban(const QString& userId, const QString& reason = {}); + void unban(const QString& userId); + void redactEvent(const QString& eventId, const QString& reason = {}); + + void uploadFile(const QString& id, const QUrl& localFilename, + const QString& overrideContentType = {}); + // If localFilename is empty a temporary file is created + void downloadFile(const QString& eventId, const QUrl& localFilename = {}); + void cancelFileTransfer(const QString& id); + + //! \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) + void switchVersion(QString newVersion); + + void inviteCall(const QString& callId, const int lifetime, + const QString& sdp); + void sendCallCandidates(const QString& callId, const QJsonArray& candidates); + [[deprecated("Lifetime argument is no more passed; " + "use 2-arg Room::answerCall() instead")]] + void answerCall(const QString& callId, int lifetime, const QString& sdp); + 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 + /** + * The initial set is what comes from the initial sync for the room. + * This includes all basic things like RoomCreateEvent, + * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents + * etc. This is a per-room reflection of Connection::loadedRoomState + * \sa Connection::loadedRoomState + */ + void baseStateLoaded(); + void eventsHistoryJobChanged(); + void aboutToAddHistoricalMessages(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(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 + /// with its local counterpart + /** NB: Requires a sync loop to be emitted */ + void pendingEventAboutToMerge(Quotient::RoomEvent* serverEvent, + int pendingEventIndex); + /// The remote and local copies of the event have been merged + /** NB: Requires a sync loop to be emitted */ + void pendingEventMerged(); + /// An event will be removed from the list of pending events + void pendingEventAboutToDiscard(int pendingEventIndex); + /// An event has just been removed from the list of pending events + void pendingEventDiscarded(); + /// The status of a pending event has changed + /** \sa PendingEventItem::deliveryStatus */ + void pendingEventChanged(int pendingEventIndex); + /// The server accepted the message + /** This is emitted when an event sending request has successfully + * completed. This does not mean that the event is already in the + * local timeline, only that the server has accepted it. + * \param txnId transaction id assigned by the client during sending + * \param eventId event id assigned by the server upon acceptance + * \sa postEvent, postPlainText, postMessage, postHtmlMessage + * \sa pendingEventMerged, aboutToAddNewMessages + */ + void messageSent(QString txnId, QString eventId); + + /** A common signal for various kinds of changes in the room + * Aside from all changes in the room state + * @param changes a set of flags describing what changes occurred + * upon the last sync + * \sa Changes + */ + void changed(Quotient::Room::Changes changes); + /** + * \brief The room name, the canonical alias or other aliases changed + * + * Not triggered when display name changes. + */ + void namesChanged(Quotient::Room* room); + void displaynameAboutToChange(Quotient::Room* room); + void displaynameChanged(Quotient::Room* room, QString oldName); + void pinnedEventsChanged(); + void topicChanged(); + void avatarChanged(); + void userAdded(Quotient::User* user); + void userRemoved(Quotient::User* user); + void memberAboutToRename(Quotient::User* user, QString newName); + void memberRenamed(Quotient::User* user); + void memberAvatarChanged(Quotient::User* user); + /// The list of members has changed + /** Emitted no more than once per sync, this is a good signal to + * for cases when some action should be done upon any change in + * the member list. If you need per-item granularity you should use + * userAdded, userRemoved and memberAboutToRename / memberRenamed + * instead. + */ + void memberListChanged(); + /// The previously lazy-loaded members list is now loaded entirely + /// \sa setDisplayed + void allMembersLoaded(); + void encryption(); + + void joinStateChanged(Quotient::JoinState oldState, + Quotient::JoinState newState); + void typingChanged(); + + void highlightCountChanged(); ///< \sa highlightCount + void notificationCountChanged(); ///< \sa notificationCount + + void displayedChanged(bool displayed); + void firstDisplayedEventChanged(); + void lastDisplayedEventChanged(); + //! The event the m.read receipt points to has changed for the listed users + //! \sa lastReadReceipt + void lastReadEventChanged(QVector<QString> userIds); + 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); + void tagsAboutToChange(); + void tagsChanged(); + + void updatedEvent(QString eventId); + void replacedEvent(const Quotient::RoomEvent* newEvent, + const Quotient::RoomEvent* oldEvent); + + void newFileTransfer(QString id, QUrl localFile); + void fileTransferProgress(QString id, qint64 progress, qint64 total); + void fileTransferCompleted(QString id, QUrl localFile, + FileSourceInfo fileMetadata); + void fileTransferFailed(QString id, QString errorMessage = {}); + // fileTransferCancelled() is no more here; use fileTransferFailed() and + // check the transfer status instead + + void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event); + + /// The room's version stability may have changed + void stabilityUpdated(QString recommendedDefault, + QStringList stableVersions); + /// This room has been upgraded and won't receive updates any more + void upgraded(QString serverMessage, Quotient::Room* successor); + /// An attempted room upgrade has failed + void upgradeFailed(QString errorMessage); + + /// The room is about to be deleted + void beforeDestruction(Quotient::Room*); + +protected: + virtual Changes processStateEvent(const RoomEvent& e); + virtual Changes processEphemeralEvent(EventPtr&& event); + virtual Changes processAccountDataEvent(EventPtr&& event); + virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) {} + virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) {} + virtual void onRedaction(const RoomEvent& /*prevEvent*/, + const RoomEvent& /*after*/) + {} + virtual QJsonObject toJson() const; + virtual void updateData(SyncRoomData&& data, bool fromCache = false); + virtual Notification checkForNotifications(const TimelineItem& ti); + +private: + friend class Connection; + + class Private; + Private* d; + + // This is called from Connection, reflecting a state change that + // arrived from the server. Clients should use + // Connection::joinRoom() and Room::leaveRoom() to change the state. + void setJoinState(JoinState state); +}; + +class QUOTIENT_API MemberSorter { +public: + explicit MemberSorter(const Room* r) : room(r) {} + + bool operator()(User* u1, User* u2) 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 + { + return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); + } + +private: + const Room* room; +}; +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::FileTransferInfo) +Q_DECLARE_METATYPE(Quotient::ReadReceipt) +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes) diff --git a/lib/roomstateview.cpp b/lib/roomstateview.cpp new file mode 100644 index 00000000..be0f7c6c --- /dev/null +++ b/lib/roomstateview.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roomstateview.h" + +using namespace Quotient; + +const StateEvent* RoomStateView::get(const QString& evtType, + const QString& stateKey) const +{ + return value({ evtType, stateKey }); +} + +bool RoomStateView::contains(const QString& evtType, + const QString& stateKey) const +{ + return contains({ evtType, stateKey }); +} + +QJsonObject RoomStateView::contentJson(const QString& evtType, + const QString& stateKey) const +{ + return queryOr(evtType, stateKey, &Event::contentJson, QJsonObject()); +} + +const QVector<const StateEvent*> RoomStateView::eventsOfType( + const QString& evtType) const +{ + auto vals = QVector<const StateEvent*>(); + for (auto it = cbegin(); it != cend(); ++it) + if (it.key().first == evtType) + vals.append(it.value()); + + return vals; +} diff --git a/lib/roomstateview.h b/lib/roomstateview.h new file mode 100644 index 00000000..c5261a1e --- /dev/null +++ b/lib/roomstateview.h @@ -0,0 +1,211 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/stateevent.h" + +#include <QtCore/QHash> + +namespace Quotient { + +class Room; + +// NB: Both concepts below expect EvT::needsStateKey to exist so you can't +// express one via negation of the other (there's still an invalid case of +// a non-state event where needsStateKey is not even defined). + +template <typename FnT, class EvT = std::decay_t<fn_arg_t<FnT>>> +concept Keyed_State_Fn = EvT::needsStateKey; + +template <typename FnT, class EvT = std::decay_t<fn_arg_t<FnT>>> +concept Keyless_State_Fn = !EvT::needsStateKey; + +class QUOTIENT_API RoomStateView + : private QHash<StateEventKey, const StateEvent*> { + Q_GADGET +public: + const QHash<StateEventKey, const StateEvent*>& events() const + { + return *this; + } + + //! \brief Get a state event with the given event type and state key + //! \return A state event corresponding to the pair of event type + //! \p evtType and state key \p stateKey, or nullptr if there's + //! no such \p evtType / \p stateKey combination in the current + //! state. + //! \warning In libQuotient 0.7 the return type changed to an OmittableCref + //! which is effectively a nullable const reference wrapper. You + //! have to check that it has_value() before using. Alternatively + //! you can now use queryCurrentState() to access state safely. + //! \sa getCurrentStateContentJson + const StateEvent* get(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get a state event with the given event type and state key + //! + //! This is a typesafe overload that accepts a C++ event type instead of + //! its Matrix name. It is only defined for events with state key (i.e., + //! derived from KeyedStateEvent). + template <Keyed_State_Event EvT> + const EvT* get(const QString& stateKey = {}) const + { + if (const auto* evt = get(EvT::TypeId, stateKey)) { + Q_ASSERT(evt->matrixType() == EvT::TypeId + && evt->stateKey() == stateKey); + return eventCast<const EvT>(evt); + } + return nullptr; + } + + //! \brief Get a state event with the given event type + //! + //! This is a typesafe overload that accepts a C++ event type instead of + //! its Matrix name. This overload only defined for events that do not use + //! state key (i.e., derived from KeylessStateEvent). + template <Keyless_State_Event EvT> + const EvT* get() const + { + if (const auto* evt = get(EvT::TypeId)) { + Q_ASSERT(evt->matrixType() == EvT::TypeId); + return eventCast<const EvT>(evt); + } + return nullptr; + } + + using QHash::contains; + + bool contains(const QString& evtType, const QString& stateKey = {}) const; + + template <Keyed_State_Event EvT> + bool contains(const QString& stateKey = {}) const + { + return contains(EvT::TypeId, stateKey); + } + + template <Keyless_State_Event EvT> + bool contains() const + { + return contains(EvT::TypeId); + } + + template <Keyed_State_Event EvT> + auto content(const QString& stateKey, + typename EvT::content_type defaultValue = {}) const + { + // EventBase<>::content is special in that it returns a const-ref, + // and lift() inside queryOr() can't wrap that in a temporary Omittable. + if (const auto evt = get<EvT>(stateKey)) + return evt->content(); + return std::move(defaultValue); + } + + template <Keyless_State_Event EvT> + auto content(typename EvT::content_type defaultValue = {}) const + { + // Same as above + if (const auto evt = get<EvT>()) + return evt->content(); + return defaultValue; + } + + //! \brief Get the content of the current state event with the given + //! event type and state key + //! \return An empty object if there's no event in the current state with + //! this event type and state key; the contents of the event + //! <tt>'content'</tt> object otherwise + Q_INVOKABLE QJsonObject contentJson(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get all state events in the room of a certain type. + //! + //! This method returns all known state events that have occured in + //! the room of the given type. + const QVector<const StateEvent*> eventsOfType(const QString& evtType) const; + + //! \brief Run a function on a state event with the given type and key + //! + //! Use this overload when there's no predefined event type or the event + //! type is unknown at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template <typename FnT> + auto query(const QString& evtType, const QString& stateKey, FnT&& fn) const + { + return lift(std::forward<FnT>(fn), get(evtType, stateKey)); + } + + //! \brief Run a function on a state event with the given type and key + //! + //! This is an overload for keyed state events (those that have + //! `needsStateKey == true`) with type defined at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template <Keyed_State_Fn FnT> + auto query(const QString& stateKey, FnT&& fn) const + { + using EventT = std::decay_t<fn_arg_t<FnT>>; + return lift(std::forward<FnT>(fn), get<EventT>(stateKey)); + } + + //! \brief Run a function on a keyless state event with the given type + //! + //! This is an overload for keyless state events (those having + //! `needsStateKey == false`) with type defined at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template <Keyless_State_Fn FnT> + auto query(FnT&& fn) const + { + using EventT = std::decay_t<fn_arg_t<FnT>>; + return lift(std::forward<FnT>(fn), get<EventT>()); + } + + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails + template <typename FnT, typename FallbackT> + auto queryOr(const QString& evtType, const QString& stateKey, FnT&& fn, + FallbackT&& fallback) const + { + return query(evtType, stateKey, std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails + template <typename FnT, typename FallbackT> + auto queryOr(const QString& stateKey, FnT&& fn, FallbackT&& fallback) const + { + return query(stateKey, std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails + template <typename FnT, typename FallbackT> + auto queryOr(FnT&& fn, FallbackT&& fallback) const + { + return query(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + +private: + friend class Room; +}; +} // namespace Quotient diff --git a/lib/settings.cpp b/lib/settings.cpp index 852e19cb..510d253c 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -1,10 +1,14 @@ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "settings.h" +#include "util.h" #include "logging.h" #include <QtCore/QUrl> -using namespace QMatrixClient; +using namespace Quotient; QString Settings::legacyOrganizationName {}; QString Settings::legacyApplicationName {}; @@ -16,14 +20,27 @@ void Settings::setLegacyNames(const QString& organizationName, legacyApplicationName = applicationName; } +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) { -// qCDebug() << "Setting" << key << "to" << value; QSettings::setValue(key, value); if (legacySettings.contains(key)) legacySettings.remove(key); } +void Settings::remove(const QString& key) +{ + QSettings::remove(key); + if (legacySettings.contains(key)) + legacySettings.remove(key); +} + QVariant Settings::value(const QString& key, const QVariant& defaultValue) const { auto value = QSettings::value(key, legacySettings.value(key, defaultValue)); @@ -42,8 +59,12 @@ bool Settings::contains(const QString& key) const QStringList Settings::childGroups() const { - auto l = QSettings::childGroups(); - return !l.isEmpty() ? l : legacySettings.childGroups(); + auto groups = QSettings::childGroups(); + const auto& legacyGroups = legacySettings.childGroups(); + for (const auto& g: legacyGroups) + if (!groups.contains(g)) + groups.push_back(g); + return groups; } void SettingsGroup::setValue(const QString& key, const QVariant& value) @@ -56,15 +77,13 @@ bool SettingsGroup::contains(const QString& key) const return Settings::contains(groupPath + '/' + key); } -QVariant SettingsGroup::value(const QString& key, const QVariant& defaultValue) const +QVariant SettingsGroup::value(const QString& key, + const QVariant& defaultValue) const { return Settings::value(groupPath + '/' + key, defaultValue); } -QString SettingsGroup::group() const -{ - return groupPath; -} +QString SettingsGroup::group() const { return groupPath; } QStringList SettingsGroup::childGroups() const { @@ -84,40 +103,51 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId) -QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName) -QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) +QUO_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, + setDeviceId) +QUO_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, + setDeviceName) +QUO_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, + setKeepLoggedIn) + +namespace { +constexpr auto HomeserverKey = "homeserver"_ls; +constexpr auto AccessTokenKey = "access_token"_ls; +constexpr auto EncryptionAccountPickleKey = "encryption_account_pickle"_ls; +} QUrl AccountSettings::homeserver() const { - return QUrl::fromUserInput(value("homeserver").toString()); + return QUrl::fromUserInput(value(HomeserverKey).toString()); } void AccountSettings::setHomeserver(const QUrl& url) { - setValue("homeserver", url.toString()); + setValue(HomeserverKey, url.toString()); } -QString AccountSettings::userId() const +QString AccountSettings::userId() const { return group().section('/', -1); } + +void AccountSettings::clearAccessToken() { - return group().section('/', -1); + legacySettings.remove(AccessTokenKey); + legacySettings.remove(QStringLiteral("device_id")); // Force the server to + // re-issue it + remove(AccessTokenKey); } -QString AccountSettings::accessToken() const +QByteArray AccountSettings::encryptionAccountPickle() { - return value("access_token").toString(); + return value("encryption_account_pickle", "").toByteArray(); } -void AccountSettings::setAccessToken(const QString& accessToken) +void AccountSettings::setEncryptionAccountPickle( + const QByteArray& encryptionAccountPickle) { - qCWarning(MAIN) << "Saving access_token to QSettings is insecure." - " Developers, please save access_token separately."; - setValue("access_token", accessToken); + setValue("encryption_account_pickle", encryptionAccountPickle); } -void AccountSettings::clearAccessToken() +void AccountSettings::clearEncryptionAccountPickle() { - legacySettings.remove("access_token"); - legacySettings.remove("device_id"); // Force the server to re-issue it - remove("access_token"); + remove(EncryptionAccountPickleKey); // TODO: Force to re-issue it? } diff --git a/lib/settings.h b/lib/settings.h index 0b3ecaff..ff99d488 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -1,152 +1,154 @@ -/****************************************************************************** - * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "quotient_export.h" + #include <QtCore/QSettings> -#include <QtCore/QVector> #include <QtCore/QUrl> +#include <QtCore/QVector> class QVariant; -namespace QMatrixClient -{ - class Settings: public QSettings - { - Q_OBJECT - public: - /** - * Use this function before creating any Settings objects in order - * to setup a read-only location where configuration has previously - * been stored. This will provide an additional fallback in case of - * renaming the organisation/application. - */ - static void setLegacyNames(const QString& organizationName, - const QString& applicationName = {}); - -#if defined(_MSC_VER) && _MSC_VER < 1900 - // VS 2013 (and probably older) aren't friends with 'using' statements - // that involve private constructors - explicit Settings(QObject* parent = 0) : QSettings(parent) { } -#else - using QSettings::QSettings; -#endif - - Q_INVOKABLE void setValue(const QString &key, - const QVariant &value); - Q_INVOKABLE QVariant value(const QString &key, - const QVariant &defaultValue = {}) const; - - template <typename T> - T get(const QString& key, const T& defaultValue = {}) const - { - const auto qv = value(key, QVariant()); - return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() - : defaultValue; - } - - Q_INVOKABLE bool contains(const QString& key) const; - Q_INVOKABLE QStringList childGroups() const; - - private: - static QString legacyOrganizationName; - static QString legacyApplicationName; - - protected: - QSettings legacySettings { legacyOrganizationName, - legacyApplicationName }; - }; - - class SettingsGroup: public Settings +namespace Quotient { + +class QUOTIENT_API Settings : public QSettings { + Q_OBJECT +public: + /// Add a legacy organisation/application name to migrate settings from + /*! + * Use this function before creating any Settings objects in order + * to set a legacy location where configuration has previously been stored. + * This will provide an additional fallback in case of renaming + * the organisation/application. Values in legacy locations are _removed_ + * when setValue() or remove() is called. + */ + static void setLegacyNames(const QString& organizationName, + const QString& applicationName = {}); + + explicit Settings(QObject* parent = nullptr); + + /// Set the value for a given key + /*! If the key exists in the legacy location, it is removed. */ + Q_INVOKABLE void setValue(const QString& key, const QVariant& value); + + /// Remove the value from both the primary and legacy locations + Q_INVOKABLE void remove(const QString& key); + + /// Obtain a value for a given key + /*! + * If the key doesn't exist in the primary settings location, the legacy + * location is checked. If neither location has the key, + * \p defaultValue is returned. + * + * This function returns a QVariant; use get<>() to get the unwrapped + * value if you know the type upfront. + * + * \sa setLegacyNames, get + */ + Q_INVOKABLE QVariant value(const QString& key, + const QVariant& defaultValue = {}) const; + + /// Obtain a value for a given key, coerced to the given type + /*! + * On top of value(), this function unwraps the QVariant and returns + * its contents assuming the type passed as the template parameter. + * If the type is different from the one stored inside the QVariant, + * \p defaultValue is returned. In presence of legacy settings, + * only the first found value is checked; if its type does not match, + * further checks through legacy settings are not performed and + * \p defaultValue is returned. + */ + template <typename T> + T get(const QString& key, const T& defaultValue = {}) const { - public: - template <typename... ArgTs> - explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs) - : Settings(std::forward<ArgTs>(qsettingsArgs)...) - , groupPath(std::move(path)) - { } - - Q_INVOKABLE bool contains(const QString& key) const; - Q_INVOKABLE QVariant value(const QString &key, - const QVariant &defaultValue = {}) const; - - template <typename T> - T get(const QString& key, const T& defaultValue = {}) const - { - const auto qv = value(key, QVariant()); - return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() - : defaultValue; - } - - Q_INVOKABLE QString group() const; - Q_INVOKABLE QStringList childGroups() const; - Q_INVOKABLE void setValue(const QString &key, - const QVariant &value); - - Q_INVOKABLE void remove(const QString& key); - - private: - QString groupPath; - }; - -#define QMC_DECLARE_SETTING(type, propname, setter) \ - Q_PROPERTY(type propname READ propname WRITE setter) \ - public: \ - type propname() const; \ - void setter(type newValue); \ - private: - -#define QMC_DEFINE_SETTING(classname, type, propname, qsettingname, defaultValue, setter) \ -type classname::propname() const \ -{ \ - return get<type>(QStringLiteral(qsettingname), defaultValue); \ -} \ -\ -void classname::setter(type newValue) \ -{ \ - setValue(QStringLiteral(qsettingname), newValue); \ -} \ - - class AccountSettings: public SettingsGroup + const auto qv = value(key, QVariant()); + return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() : defaultValue; + } + + Q_INVOKABLE bool contains(const QString& key) const; + Q_INVOKABLE QStringList childGroups() const; + +private: + static QString legacyOrganizationName; + static QString legacyApplicationName; + +protected: + QSettings legacySettings { legacyOrganizationName, legacyApplicationName }; +}; + +class QUOTIENT_API SettingsGroup : public Settings { +public: + explicit SettingsGroup(QString path, QObject* parent = nullptr) + : Settings(parent) + , groupPath(std::move(path)) + {} + + Q_INVOKABLE bool contains(const QString& key) const; + Q_INVOKABLE QVariant value(const QString& key, + const QVariant& defaultValue = {}) const; + + template <typename T> + T get(const QString& key, const T& defaultValue = {}) const { - Q_OBJECT - Q_PROPERTY(QString userId READ userId CONSTANT) - QMC_DECLARE_SETTING(QString, deviceId, setDeviceId) - QMC_DECLARE_SETTING(QString, deviceName, setDeviceName) - QMC_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) - /** \deprecated \sa setAccessToken */ - Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) - public: - template <typename... ArgTs> - explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs) - : SettingsGroup("Accounts/" + accountId, qsettingsArgs...) - { } - - QString userId() const; - - QUrl homeserver() const; - void setHomeserver(const QUrl& url); - - /** \deprecated \sa setToken */ - QString accessToken() const; - /** \deprecated Storing accessToken in QSettings is unsafe, - * see QMatrixClient/Quaternion#181 */ - void setAccessToken(const QString& accessToken); - Q_INVOKABLE void clearAccessToken(); - }; -} // namespace QMatrixClient + const auto qv = value(key, QVariant()); + return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() : defaultValue; + } + + Q_INVOKABLE QString group() const; + Q_INVOKABLE QStringList childGroups() const; + Q_INVOKABLE void setValue(const QString& key, const QVariant& value); + + Q_INVOKABLE void remove(const QString& key); + +private: + QString groupPath; +}; + +#define QUO_DECLARE_SETTING(type, propname, setter) \ + Q_PROPERTY(type propname READ propname WRITE setter) \ +public: \ + type propname() const; \ + void setter(type newValue); \ + \ +private: + +#define QUO_DEFINE_SETTING(classname, type, propname, qsettingname, \ + defaultValue, setter) \ + type classname::propname() const \ + { \ + return get<type>(QStringLiteral(qsettingname), defaultValue); \ + } \ + \ + void classname::setter(type newValue) \ + { \ + setValue(QStringLiteral(qsettingname), std::move(newValue)); \ + } + +class QUOTIENT_API AccountSettings : public SettingsGroup { + Q_OBJECT + Q_PROPERTY(QString userId READ userId CONSTANT) + QUO_DECLARE_SETTING(QString, deviceId, setDeviceId) + QUO_DECLARE_SETTING(QString, deviceName, setDeviceName) + QUO_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) + Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle + WRITE setEncryptionAccountPickle) +public: + explicit AccountSettings(const QString& accountId, QObject* parent = nullptr) + : SettingsGroup("Accounts/" + accountId, parent) + {} + + QString userId() const; + + QUrl homeserver() const; + void setHomeserver(const QUrl& url); + + Q_DECL_DEPRECATED_X("Access tokens are not stored in QSettings any more") + Q_INVOKABLE void clearAccessToken(); + + QByteArray encryptionAccountPickle(); + void setEncryptionAccountPickle(const QByteArray& encryptionAccountPickle); + Q_INVOKABLE void clearEncryptionAccountPickle(); +}; +} // namespace Quotient diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp new file mode 100644 index 00000000..93e252cc --- /dev/null +++ b/lib/ssosession.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "ssosession.h" + +#include "connection.h" +#include "csapi/sso_login_redirect.h" + +#include <QtNetwork/QTcpServer> +#include <QtNetwork/QTcpSocket> +#include <QtCore/QCoreApplication> +#include <QtCore/QStringBuilder> + +using namespace Quotient; + +class SsoSession::Private { +public: + Private(SsoSession* q, QString initialDeviceName = {}, + QString deviceId = {}, Connection* connection = nullptr) + : initialDeviceName(std::move(initialDeviceName)) + , deviceId(std::move(deviceId)) + , connection(connection) + { + auto* server = new QTcpServer(q); + server->listen(); + // The "/returnToApplication" part is just a hint for the end-user, + // the callback will work without it equally well. + callbackUrl = QStringLiteral("http://localhost:%1/returnToApplication") + .arg(server->serverPort()); + ssoUrl = connection->getUrlForApi<RedirectToSSOJob>(callbackUrl); + + QObject::connect(server, &QTcpServer::newConnection, q, [this, q, server] { + qCDebug(MAIN) << "SSO callback initiated"; + socket = server->nextPendingConnection(); + server->close(); + QObject::connect(socket, &QTcpSocket::readyRead, socket, [this] { + requestData.append(socket->readAll()); + if (!socket->atEnd() && !requestData.endsWith("\r\n\r\n")) { + qDebug(MAIN) << "Incomplete request, waiting for more data"; + return; + } + processCallback(); + }); + QObject::connect(socket, &QTcpSocket::disconnected, socket, + &QTcpSocket::deleteLater); + QObject::connect(socket, &QObject::destroyed, q, + &QObject::deleteLater); + }); + qCDebug(MAIN) << "SSO session constructed"; + } + ~Private() { qCDebug(MAIN) << "SSO session deconstructed"; } + Q_DISABLE_COPY_MOVE(Private) + + void processCallback(); + void sendHttpResponse(const QByteArray& code, const QByteArray& msg); + void onError(const QByteArray& code, const QString& errorMsg); + + QString initialDeviceName; + QString deviceId; + Connection* connection; + QString callbackUrl {}; + QUrl ssoUrl {}; + QTcpSocket* socket = nullptr; + QByteArray requestData {}; +}; + +SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName, + const QString& deviceId) + : QObject(connection) + , d(makeImpl<Private>(this, initialDeviceName, deviceId, connection)) +{} + +QUrl SsoSession::ssoUrl() const { return d->ssoUrl; } + +QUrl SsoSession::callbackUrl() const { return QUrl(d->callbackUrl); } + +void SsoSession::Private::processCallback() +{ + // https://matrix.org/docs/guides/sso-for-client-developers + // Inspired by Clementine's src/internet/core/localredirectserver.cpp + // (see at https://github.com/clementine-player/Clementine/) + const auto& requestParts = requestData.split(' '); + if (requestParts.size() < 2 || requestParts[1].isEmpty()) { + onError("400 Bad Request", tr("Malformed single sign-on callback")); + return; + } + const auto& QueryItemName = QStringLiteral("loginToken"); + QUrlQuery query { QUrl(requestParts[1]).query() }; + if (!query.hasQueryItem(QueryItemName)) { + onError("400 Bad Request", tr("No login token in SSO callback")); + return; + } + qCDebug(MAIN) << "Found the token in SSO callback, logging in"; + connection->loginWithToken(query.queryItemValue(QueryItemName).toLatin1(), + initialDeviceName, deviceId); + connect(connection, &Connection::connected, socket, [this] { + const auto msg = + tr("The application '%1' has successfully logged in as a user %2 " + "with device id %3. This window can be closed. Thank you.\r\n") + .arg(QCoreApplication::applicationName(), connection->userId(), + connection->deviceId()); + sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8()); + socket->disconnectFromHost(); + }); + connect(connection, &Connection::loginError, socket, [this] { + onError("401 Unauthorised", tr("Login failed")); + }); +} + +void SsoSession::Private::sendHttpResponse(const QByteArray& code, + const QByteArray& msg) +{ + socket->write("HTTP/1.0 "); + socket->write(code); + socket->write("\r\n" + "Content-type: text/html;charset=UTF-8\r\n" + "\r\n\r\n"); + socket->write(msg); + socket->write("\r\n"); +} + +void SsoSession::Private::onError(const QByteArray& code, + const QString& errorMsg) +{ + qCWarning(MAIN).nospace() << errorMsg; + sendHttpResponse(code, "<h3>" + errorMsg.toUtf8() + "</h3>"); + // [kitsune] Yeah, I know, dirty. Maybe the "right" way would be to have + // an intermediate signal but that seems just a fight for purity. + emit connection->loginError(errorMsg, requestData); + socket->disconnectFromHost(); +} diff --git a/lib/ssosession.h b/lib/ssosession.h new file mode 100644 index 00000000..e6a3f8fb --- /dev/null +++ b/lib/ssosession.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "util.h" + +#include <QtCore/QUrl> +#include <QtCore/QObject> + +namespace Quotient { +class Connection; + +/*! Single sign-on (SSO) session encapsulation + * + * This class is responsible for setting up of a new SSO session, providing + * a URL to be opened (usually, in a web browser) and handling the callback + * response after completing the single sign-on, all the way to actually + * logging the user in. It does NOT open and render the SSO URL, it only does + * the necessary backstage work. + * + * Clients only need to open the URL; the rest is done for them. + * Client code can look something like: + * \code + * QDesktopServices::openUrl( + * connection->prepareForSso(initialDeviceName)->ssoUrl()); + * \endcode + */ +class QUOTIENT_API SsoSession : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl ssoUrl READ ssoUrl CONSTANT) + Q_PROPERTY(QUrl callbackUrl READ callbackUrl CONSTANT) +public: + SsoSession(Connection* connection, const QString& initialDeviceName, + const QString& deviceId = {}); + ~SsoSession() override = default; + + QUrl ssoUrl() const; + QUrl callbackUrl() const; + +private: + class Private; + ImplPtr<Private> d; +}; +} // namespace Quotient diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp new file mode 100644 index 00000000..ec7203af --- /dev/null +++ b/lib/syncdata.cpp @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "syncdata.h" + +#include "logging.h" + +#include <QtCore/QFile> +#include <QtCore/QFileInfo> + +using namespace Quotient; + +bool RoomSummary::isEmpty() const +{ + return !joinedMemberCount && !invitedMemberCount && !heroes; +} + +bool RoomSummary::merge(const RoomSummary& other) +{ + // Using bitwise OR to prevent computation shortcut. + return static_cast<bool>( + static_cast<int>(joinedMemberCount.merge(other.joinedMemberCount)) + | static_cast<int>(invitedMemberCount.merge(other.invitedMemberCount)) + | static_cast<int>(heroes.merge(other.heroes))); +} + +QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (rs.joinedMemberCount) + sl << QStringLiteral("joined: %1").arg(*rs.joinedMemberCount); + if (rs.invitedMemberCount) + sl << QStringLiteral("invited: %1").arg(*rs.invitedMemberCount); + if (rs.heroes) + sl << QStringLiteral("heroes: [%1]").arg(rs.heroes->join(',')); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; +} + +void JsonObjectConverter<RoomSummary>::dumpTo(QJsonObject& jo, + const RoomSummary& rs) +{ + addParam<IfNotEmpty>(jo, QStringLiteral("m.joined_member_count"), + rs.joinedMemberCount); + addParam<IfNotEmpty>(jo, QStringLiteral("m.invited_member_count"), + rs.invitedMemberCount); + addParam<IfNotEmpty>(jo, QStringLiteral("m.heroes"), rs.heroes); +} + +void JsonObjectConverter<RoomSummary>::fillFrom(const QJsonObject& jo, + RoomSummary& rs) +{ + fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount); + fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount); + fromJson(jo["m.heroes"_ls], rs.heroes); +} + +template <typename EventsArrayT, typename StrT> +inline EventsArrayT load(const QJsonObject& batches, StrT keyName) +{ + return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); +} + +SyncRoomData::SyncRoomData(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>(roomJson, "ephemeral"_ls); + [[fallthrough]]; + case JoinState::Leave: { + 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(); + + break; + } + default: /* nothing on top of state */; + } + + 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); +} + +QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (!devicesList.changed.isEmpty()) + sl << QStringLiteral("changed: %1").arg(devicesList.changed.join(", ")); + if (!devicesList.left.isEmpty()) + sl << QStringLiteral("left %1").arg(devicesList.left.join(", ")); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; +} + +void JsonObjectConverter<DevicesList>::dumpTo(QJsonObject& jo, + const DevicesList& rs) +{ + addParam<IfNotEmpty>(jo, QStringLiteral("changed"), + rs.changed); + addParam<IfNotEmpty>(jo, QStringLiteral("left"), + rs.left); +} + +void JsonObjectConverter<DevicesList>::fillFrom(const QJsonObject& jo, + DevicesList& rs) +{ + fromJson(jo["changed"_ls], rs.changed); + fromJson(jo["left"_ls], rs.left); +} + +SyncData::SyncData(const QString& cacheFileName) +{ + QFileInfo cacheFileInfo { cacheFileName }; + auto json = loadJson(cacheFileName); + auto requiredVersion = MajorCacheVersion; + auto actualVersion = + json.value("cache_version"_ls).toObject().value("major"_ls).toInt(); + if (actualVersion == requiredVersion) + parseJson(json, cacheFileInfo.absolutePath() + '/'); + else + qCWarning(MAIN) << "Major version of the cache file is" << actualVersion + << "but" << requiredVersion + << "is required; discarding the cache"; +} + +SyncDataList SyncData::takeRoomData() { return move(roomData); } + +QString SyncData::fileNameForRoom(QString roomId) +{ + roomId.replace(':', '_'); + return roomId + ".json"; +} + +Events SyncData::takePresenceData() { return std::move(presenceData); } + +Events SyncData::takeAccountData() { return std::move(accountData); } + +Events SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } + +std::pair<int, int> SyncData::cacheVersion() +{ + return { MajorCacheVersion, 2 }; +} + +DevicesList SyncData::takeDevicesList() { return std::move(devicesList); } + +QJsonObject SyncData::loadJson(const QString& fileName) +{ + QFile roomFile { fileName }; + if (!roomFile.exists()) { + qCWarning(MAIN) << "No state cache file" << fileName; + return {}; + } + if (!roomFile.open(QIODevice::ReadOnly)) { + qCWarning(MAIN) << "Failed to open state cache file" + << roomFile.fileName(); + return {}; + } + auto data = roomFile.readAll(); + + const auto json = data.startsWith('{') + ? QJsonDocument::fromJson(data).object() + : QCborValue::fromCbor(data).toJsonValue().toObject(); + if (json.isEmpty()) { + qCWarning(MAIN) << "State cache in" << fileName + << "is broken or empty, discarding"; + } + return json; +} + +void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) +{ + QElapsedTimer et; + et.start(); + + nextBatch_ = json.value("next_batch"_ls).toString(); + presenceData = load<Events>(json, "presence"_ls); + accountData = load<Events>(json, "account_data"_ls); + toDeviceEvents = load<Events>(json, "to_device"_ls); + + fromJson(json.value("device_one_time_keys_count"_ls), + deviceOneTimeKeysCount_); + + if(json.contains("device_lists")) { + fromJson(json.value("device_lists"), devicesList); + } + + auto rooms = json.value("rooms"_ls).toObject(); + auto totalRooms = 0; + auto totalEvents = 0; + 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(roomData.size() + static_cast<size_t>(rs.size())); + for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { + QJsonObject roomJson; + if (!baseDir.isEmpty()) { + // Loading data from the local cache, with room objects saved in + // individual files rather than inline + roomJson = loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) { + unresolvedRoomIds.push_back(roomIt.key()); + continue; + } + } else // When loading from /sync response, everything is inline + roomJson = roomIt->toObject(); + + roomData.emplace_back(roomIt.key(), joinState, roomJson); + const auto& r = roomData.back(); + totalEvents += r.state.size() + r.ephemeral.size() + + r.accountData.size() + r.timeline.size(); + } + totalRooms += rs.size(); + } + if (!unresolvedRoomIds.empty()) + qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(','); + if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" + << totalRooms << "room(s)," << totalEvents + << "event(s) in" << et; +} diff --git a/lib/syncdata.h b/lib/syncdata.h new file mode 100644 index 00000000..9358ec8f --- /dev/null +++ b/lib/syncdata.h @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#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 + * changed values are sent from the server so if nothing is in the payload + * the respective member will be omitted. In particular, `heroes.omitted()` + * means that nothing has come from the server; heroes.value().isEmpty() + * means a peculiar case of a room with the only member - the current user. + */ +struct QUOTIENT_API RoomSummary { + Omittable<int> joinedMemberCount; + Omittable<int> invitedMemberCount; + Omittable<QStringList> heroes; //< mxids of users to take part in the room + // name + + bool isEmpty() const; + /// Merge the contents of another RoomSummary object into this one + /// \return true, if the current object has changed; false otherwise + bool merge(const RoomSummary& other); +}; +QDebug operator<<(QDebug dbg, const RoomSummary& rs); + +template <> +struct JsonObjectConverter<RoomSummary> { + static void dumpTo(QJsonObject& jo, const RoomSummary& rs); + static void fillFrom(const QJsonObject& jo, RoomSummary& rs); +}; + +/// Information on e2e device updates. Note: only present on an +/// incremental sync. +struct DevicesList { + /// List of users who have updated their device identity keys, or who + /// now share an encrypted room with the client since the previous + /// sync response. + QStringList changed; + + /// List of users with whom we do not share any encrypted rooms + /// anymore since the previous sync response. + QStringList left; +}; + +QDebug operator<<(QDebug dhg, const DevicesList& devicesList); + +template <> +struct JsonObjectConverter<DevicesList> { + static void dumpTo(QJsonObject &jo, const DevicesList &dev); + static void fillFrom(const QJsonObject& jo, DevicesList& rs); +}; + +class SyncRoomData { +public: + QString roomId; + JoinState joinState; + RoomSummary summary; + StateEvents state; + RoomEvents timeline; + Events ephemeral; + Events accountData; + + bool timelineLimited; + QString timelinePrevBatch; + Omittable<int> partiallyReadCount; + Omittable<int> unreadCount; + Omittable<int> highlightCount; + + SyncRoomData(QString roomId, JoinState joinState, + const QJsonObject& roomJson); + SyncRoomData(SyncRoomData&&) = default; + SyncRoomData& operator=(SyncRoomData&&) = default; +}; + +// QVector cannot work with non-copyable objects, std::vector can. +using SyncDataList = std::vector<SyncRoomData>; + +class SyncData { +public: + SyncData() = default; + explicit SyncData(const QString& cacheFileName); + /** Parse sync response into room events + * \param json response from /sync or a room state cache + * \return the list of rooms with missing cache files; always + * empty when parsing response from /sync + */ + void parseJson(const QJsonObject& json, const QString& baseDir = {}); + + Events takePresenceData(); + Events takeAccountData(); + Events takeToDeviceEvents(); + const QHash<QString, int>& deviceOneTimeKeysCount() const + { + return deviceOneTimeKeysCount_; + } + SyncDataList takeRoomData(); + DevicesList takeDevicesList(); + + QString nextBatch() const { return nextBatch_; } + + QStringList unresolvedRooms() const { return unresolvedRoomIds; } + + static constexpr int MajorCacheVersion = 11; + static std::pair<int, int> cacheVersion(); + static QString fileNameForRoom(QString roomId); + +private: + QString nextBatch_; + Events presenceData; + Events accountData; + Events toDeviceEvents; + SyncDataList roomData; + QStringList unresolvedRoomIds; + QHash<QString, int> deviceOneTimeKeysCount_; + DevicesList devicesList; + + static QJsonObject loadJson(const QString& fileName); +}; +} // namespace Quotient diff --git a/lib/uri.cpp b/lib/uri.cpp new file mode 100644 index 00000000..91751df0 --- /dev/null +++ b/lib/uri.cpp @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "uri.h" + +#include "util.h" +#include "logging.h" + +#include <QtCore/QRegularExpression> + +using namespace Quotient; + +namespace { + +struct ReplacePair { QLatin1String uriString; char sigil; }; +/// \brief Defines bi-directional mapping of path prefixes and sigils +/// +/// When there are two prefixes for the same sigil, the first matching +/// entry for a given sigil is used. +const ReplacePair replacePairs[] = { + { "u/"_ls, '@' }, + { "user/"_ls, '@' }, + { "roomid/"_ls, '!' }, + { "r/"_ls, '#' }, + { "room/"_ls, '#' }, + // The notation for bare event ids is not proposed in MSC2312 but there's + // https://github.com/matrix-org/matrix-doc/pull/2644 + { "e/"_ls, '$' }, + { "event/"_ls, '$' } +}; + +} + +Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) +{ + if (primaryId.isEmpty()) + primaryType_ = Empty; + else { + setScheme("matrix"); + QString pathToBe; + primaryType_ = Invalid; + if (primaryId.size() < 2) // There should be something after sigil + return; + for (const auto& p: replacePairs) + if (primaryId[0] == p.sigil) { + primaryType_ = Type(p.sigil); + auto safePrimaryId = primaryId.mid(1); + safePrimaryId.replace('/', "%2F"); + pathToBe = p.uriString + safePrimaryId; + break; + } + if (!secondaryId.isEmpty()) { + if (secondaryId.size() < 2) { + primaryType_ = Invalid; + return; + } + auto safeSecondaryId = secondaryId.mid(1); + safeSecondaryId.replace('/', "%2F"); + pathToBe += "/event/" + safeSecondaryId; + } + setPath(pathToBe, QUrl::TolerantMode); + } + if (!query.isEmpty()) + setQuery(query); +} + +static inline auto encodedPath(const QUrl& url) +{ + return url.path(QUrl::EncodeDelimiters | QUrl::EncodeUnicode); +} + +static QString pathSegment(const QUrl& url, int which) +{ + return QUrl::fromPercentEncoding( + encodedPath(url).section('/', which, which).toUtf8()); +} + +static auto decodeFragmentPart(QStringView part) +{ + return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); +} + +static inline auto matrixToUrlRegexInit() +{ + // See https://matrix.org/docs/spec/appendices#matrix-to-navigation + const QRegularExpression MatrixToUrlRE { + "^/(?<main>[^:]+:[^/?]+)(/(?<sec>(\\$|%24)[^?]+))?(\\?(?<query>.+))?$" + }; + Q_ASSERT(MatrixToUrlRE.isValid()); + return MatrixToUrlRE; +} + +Uri::Uri(QUrl url) : QUrl(std::move(url)) +{ + // NB: don't try to use `url` from here on, it's moved-from and empty + if (isEmpty()) + return; // primaryType_ == Empty + + primaryType_ = Invalid; + if (!QUrl::isValid()) // MatrixUri::isValid() checks primaryType_ + return; + + 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.split('/'); + switch (splitPath.size()) { + case 2: + break; + case 4: + if (splitPath[2] == "event" || splitPath[2] == "e") + break; + [[fallthrough]]; + default: + return; // Invalid + } + + for (const auto& p: replacePairs) + if (urlPath.startsWith(p.uriString)) { + primaryType_ = Type(p.sigil); + return; // The only valid return path for matrix: URIs + } + qCDebug(MAIN) << "The matrix: URI is not recognised:" + << toDisplayString(); + return; + } + + primaryType_ = NonMatrix; // Default, unless overridden by the code below + if (scheme() == "https" && authority() == "matrix.to") { + static const auto MatrixToUrlRE = matrixToUrlRegexInit(); + // matrix.to accepts both literal sigils (as well as & and ? used in + // its "query" substitute) and their %-encoded forms; + // so force QUrl to decode everything. + auto f = fragment(QUrl::EncodeUnicode); + if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch()) + *this = Uri { decodeFragmentPart(m.capturedView(u"main")), + decodeFragmentPart(m.capturedView(u"sec")), + decodeFragmentPart(m.capturedView(u"query")) }; + } +} + +Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {} + +Uri Uri::fromUserInput(const QString& uriOrId) +{ + if (uriOrId.isEmpty()) + return {}; // type() == None + + // A quick check if uriOrId is a plain Matrix id + // Bare event ids cannot be resolved without a room scope as per the current + // spec but there's a movement towards making them navigable (see, e.g., + // https://github.com/matrix-org/matrix-doc/pull/2644) - so treat them + // as valid + if (QStringLiteral("!@#+$").contains(uriOrId[0])) + return Uri { uriOrId.toUtf8() }; + + return Uri { QUrl::fromUserInput(uriOrId) }; +} + +Uri::Type Uri::type() const { return primaryType_; } + +Uri::SecondaryType Uri::secondaryType() const +{ + const auto& type2 = pathSegment(*this, 2); + return type2 == "event" || type2 == "e" ? EventId : NoSecondaryId; +} + +QUrl Uri::toUrl(UriForm form) const +{ + if (!isValid()) + return {}; + + if (form == CanonicalUri || type() == NonMatrix) + return SLICE(*this, QUrl); + + QUrl url; + url.setScheme("https"); + url.setHost("matrix.to"); + url.setPath("/"); + auto fragment = '/' + primaryId(); + if (const auto& secId = secondaryId(); !secId.isEmpty()) + fragment += '/' + secId; + if (const auto& q = query(); !q.isEmpty()) + fragment += '?' + q; + url.setFragment(fragment); + return url; +} + +QString Uri::primaryId() const +{ + if (primaryType_ == Empty || primaryType_ == Invalid) + return {}; + + auto idStem = pathSegment(*this, 1); + if (!idStem.isEmpty()) + idStem.push_front(char(primaryType_)); + return idStem; +} + +QString Uri::secondaryId() const +{ + auto idStem = pathSegment(*this, 3); + if (!idStem.isEmpty()) + idStem.push_front(char(secondaryType())); + return idStem; +} + +static const auto ActionKey = QStringLiteral("action"); + +QString Uri::action() const +{ + return type() == NonMatrix || !isValid() + ? QString() + : QUrlQuery { query() }.queryItemValue(ActionKey); +} + +void Uri::setAction(const QString& newAction) +{ + if (!isValid()) { + qCWarning(MAIN) << "Cannot set an action on an invalid Quotient::Uri"; + return; + } + QUrlQuery q { query() }; + q.removeQueryItem(ActionKey); + q.addQueryItem(ActionKey, newAction); + setQuery(q); +} + +QStringList Uri::viaServers() const +{ + return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"), + QUrl::EncodeReserved); +} + +bool Uri::isValid() const +{ + return primaryType_ != Empty && primaryType_ != Invalid; +} diff --git a/lib/uri.h b/lib/uri.h new file mode 100644 index 00000000..78cd27c8 --- /dev/null +++ b/lib/uri.h @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "quotient_common.h" + +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> + +namespace Quotient { + +/*! \brief A wrapper around a Matrix URI or identifier + * + * This class encapsulates a Matrix resource identifier, passed in either of + * 3 forms: a plain Matrix identifier (sigil, localpart, serverpart or, for + * modern event ids, sigil and base64 hash); an MSC2312 URI (aka matrix: URI); + * or a matrix.to URL. The input can be either encoded (serverparts with + * punycode, the rest with percent-encoding) or unencoded (in this case it is + * the caller's responsibility to resolve all possible ambiguities). + * + * The class provides functions to check the validity of the identifier, + * its type, and obtain components, also in either unencoded (for displaying) + * or encoded (for APIs) form. + */ +class QUOTIENT_API Uri : private QUrl { + Q_GADGET +public: + enum Type : char { + Invalid = char(-1), + Empty = 0x0, + UserId = '@', + RoomId = '!', + RoomAlias = '#', + Group = '+', + BareEventId = '$', // https://github.com/matrix-org/matrix-doc/pull/2644 + NonMatrix = ':' + }; + Q_ENUM(Type) + enum SecondaryType : char { NoSecondaryId = 0x0, EventId = '$' }; + Q_ENUM(SecondaryType) + + enum UriForm : short { CanonicalUri, MatrixToUri }; + Q_ENUM(UriForm) + + /// Construct an empty Matrix URI + Uri() = default; + /*! \brief Decode a user input string to a Matrix identifier + * + * Accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and + * matrix.to URLs. In case of URIs/URLs, it uses QUrl's TolerantMode + * parser to decode common mistakes/irregularities (see QUrl documentation + * for more details). + */ + Uri(const QString& uriOrId); + + /// Construct a Matrix URI from components + explicit Uri(QByteArray primaryId, QByteArray secondaryId = {}, + QString query = {}); + /// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI + explicit Uri(QUrl url); + + static Uri fromUserInput(const QString& uriOrId); + static Uri fromUrl(QUrl url); + + /// Get the primary type of the Matrix URI (user id, room id or alias) + /*! Note that this does not include an event as a separate type, since + * events can only be addressed inside of rooms, which, in turn, are + * addressed either by id or alias. If you need to check whether the URI + * is specifically an event URI, use secondaryType() instead. + */ + Q_INVOKABLE Type type() const; + Q_INVOKABLE SecondaryType secondaryType() const; + Q_INVOKABLE QUrl toUrl(UriForm form = CanonicalUri) const; + Q_INVOKABLE QString primaryId() const; + Q_INVOKABLE QString secondaryId() const; + Q_INVOKABLE QString action() const; + Q_INVOKABLE void setAction(const QString& newAction); + Q_INVOKABLE QStringList viaServers() const; + Q_INVOKABLE bool isValid() const; + using QUrl::path, QUrl::query, QUrl::fragment; + using QUrl::isEmpty, QUrl::toDisplayString; + +private: + Type primaryType_ = Empty; +}; + +} diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp new file mode 100644 index 00000000..681e3842 --- /dev/null +++ b/lib/uriresolver.cpp @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "uriresolver.h" + +#include "connection.h" +#include "user.h" + +using namespace Quotient; + +UriResolverBase::~UriResolverBase() = default; + +UriResolveResult UriResolverBase::visitResource(Connection* account, + const Uri& uri) +{ + switch (uri.type()) { + case Uri::NonMatrix: + return visitNonMatrix(uri.toUrl()) ? UriResolved : CouldNotResolve; + case Uri::Invalid: + case Uri::Empty: + return InvalidUri; + default:; + } + + if (!account) + return NoAccount; + + switch (uri.type()) { + case Uri::UserId: { + if (uri.action() == "join") + return IncorrectAction; + auto* user = account->user(uri.primaryId()); + Q_ASSERT(user != nullptr); + return visitUser(user, uri.action()); + } + case Uri::RoomId: + case Uri::RoomAlias: { + auto* room = uri.type() == Uri::RoomId + ? account->room(uri.primaryId()) + : account->roomByAlias(uri.primaryId()); + if (room != nullptr) { + visitRoom(room, uri.secondaryId()); + return UriResolved; + } + if (uri.action() == "join") { + joinRoom(account, uri.primaryId(), uri.viaServers()); + return UriResolved; + } + [[fallthrough]]; + } + default: + return CouldNotResolve; + } +} + +// This template is only instantiated once, for Quotient::visitResource() +template <typename... FnTs> +class StaticUriDispatcher : public UriResolverBase { +public: + StaticUriDispatcher(const FnTs&... fns) : fns_(fns...) {} + +private: + UriResolveResult visitUser(User* user, const QString& action) override + { + return std::get<0>(fns_)(user, action); + } + void visitRoom(Room* room, const QString& eventId) override + { + std::get<1>(fns_)(room, eventId); + } + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override + { + std::get<2>(fns_)(account, roomAliasOrId, viaServers); + } + bool visitNonMatrix(const QUrl& url) override + { + return std::get<3>(fns_)(url); + } + + std::tuple<FnTs...> fns_; +}; +template <typename... FnTs> +StaticUriDispatcher(FnTs&&... fns) -> StaticUriDispatcher<FnTs...>; + +UriResolveResult Quotient::visitResource( + Connection* account, const Uri& uri, + std::function<UriResolveResult(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler) +{ + return StaticUriDispatcher(userHandler, roomEventHandler, joinHandler, + nonMatrixHandler) + .visitResource(account, uri); +} + +UriResolveResult UriDispatcher::visitUser(User *user, const QString &action) +{ + emit userAction(user, action); + return UriResolved; +} + +void UriDispatcher::visitRoom(Room *room, const QString &eventId) +{ + emit roomAction(room, eventId); +} + +void UriDispatcher::joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers) +{ + emit joinAction(account, roomAliasOrId, viaServers); +} + +bool UriDispatcher::visitNonMatrix(const QUrl &url) +{ + emit nonMatrixAction(url); + return true; +} diff --git a/lib/uriresolver.h b/lib/uriresolver.h new file mode 100644 index 00000000..9140046c --- /dev/null +++ b/lib/uriresolver.h @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "uri.h" + +#include <QtCore/QObject> + +#include <functional> + +namespace Quotient { +class Connection; +class Room; +class User; + +/*! \brief Abstract class to resolve the resource and act on it + * + * This class encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * It is a type-safe way of handling a URI with no prior context on its type + * in cases like, e.g., when a user clicks on a URI in the application. + * + * This class provides empty "handlers" for each type of URI to facilitate + * gradual implementation. Derived classes are encouraged to override as many + * of them as possible. + */ +class QUOTIENT_API UriResolverBase { +public: + /*! \brief Resolve the resource and dispatch an action depending on its type + * + * This method: + * 1. Resolves \p uri into an actual object (e.g., Room or User), + * with possible additional data such as event id, in the context of + * \p account. + * 2. If the resolving is successful, depending on the type of the object, + * calls the appropriate virtual function (defined in a derived + * concrete class) to perform an action on the resource (open a room, + * mention a user etc.). + * 3. Returns the result of resolving the resource. + */ + 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 [[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 [[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 [[maybe_unused]], + const QString& roomAliasOrId [[maybe_unused]], + const QStringList& viaServers [[maybe_unused]] = {}) + {} + /// Called by visitResource() when the passed URI has `type() == NonMatrix` + /*! + * Should return true if the URI is considered resolved, false otherwise. + * A basic implementation in a graphical client can look like + * `return QDesktopServices::openUrl(url);` but it's strongly advised to + * ask for a user confirmation beforehand. + */ + virtual bool visitNonMatrix(const QUrl& url [[maybe_unused]]) + { + return false; + } +}; + +/*! \brief Resolve the resource and invoke an action on it, via function objects + * + * This function encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * Unlike UriResolverBase it accepts the list of handlers from + * the caller; internally it's uses a minimal UriResolverBase class + * + * \param account The connection used as a context to resolve the identifier + * + * \param uri A URI that can represent a Matrix entity + * + * \param userHandler Called when the passed URI identifies a Matrix user + * + * \param roomEventHandler Called when the passed URI identifies a room or + * an event in a room + * + * \param joinHandler Called when the passed URI has `action() == "join"` and + * identifies a room that the user defined by + * the Connection argument is not a member of + * + * \param nonMatrixHandler Called when the passed URI has `type() == NonMatrix`; + * should return true if the URI is considered resolved, + * false otherwise + * + * \sa UriResolverBase, UriDispatcher + */ +QUOTIENT_API UriResolveResult +visitResource(Connection* account, const Uri& uri, + std::function<UriResolveResult(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler); + +/*! \brief Check that the resource is resolvable with no action on it */ +inline UriResolveResult checkResource(Connection* account, const Uri& uri) +{ + return visitResource( + account, uri, [](auto, auto) { return UriResolved; }, [](auto, auto) {}, + [](auto, auto, auto) {}, [](auto) { return false; }); +} + +/*! \brief Resolve the resource and invoke an action on it, via Qt signals + * + * This is an implementation of UriResolverBase that is based on + * QObject and uses Qt signals instead of virtual functions to provide an + * open-ended interface for visitors. + * + * This class is aimed primarily at clients where invoking the resolving/action + * and handling the action are happening in decoupled parts of the code; it's + * also useful to operate on Matrix identifiers and URIs from QML/JS code + * that cannot call resolveResource() due to QML/C++ interface limitations. + * + * This class does not restrain the client code to a certain type of + * connections: both direct and queued (or a mix) will work fine. One limitation + * caused by that is there's no way to indicate if a non-Matrix URI has been + * successfully resolved - a signal always returns void. + * + * Note that in case of using (non-blocking) queued connections the code that + * calls resolveResource() should not expect the action to be performed + * synchronously - the returned value is the result of resolving the URI, + * not acting on it. + */ +class QUOTIENT_API UriDispatcher : public QObject, public UriResolverBase { + Q_OBJECT +public: + explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {} + + // It's actually UriResolverBase::visitResource() but with Q_INVOKABLE + Q_INVOKABLE UriResolveResult resolveResource(Connection* account, + const Uri& uri) + { + return UriResolverBase::visitResource(account, uri); + } + +Q_SIGNALS: + /// An action on a user has been requested + void userAction(Quotient::User* user, QString action); + + /// An action on a room has been requested, with optional event id + void roomAction(Quotient::Room* room, QString eventId); + + /// A join action has been requested, with optional 'via' servers + void joinAction(Quotient::Connection* account, QString roomAliasOrId, + QStringList viaServers); + + /// An action on a non-Matrix URL has been requested + void nonMatrixAction(QUrl url); + +private: + UriResolveResult visitUser(User* user, const QString& action) override; + void visitRoom(Room* room, const QString& eventId) override; + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override; + bool visitNonMatrix(const QUrl& url) override; +}; + +} // namespace Quotient + + diff --git a/lib/user.cpp b/lib/user.cpp index bfd23ae2..4c3fc9e2 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -1,365 +1,215 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "user.h" +#include "avatar.h" #include "connection.h" #include "room.h" -#include "avatar.h" + +#include "csapi/content-repo.h" +#include "csapi/profile.h" +#include "csapi/room_state.h" + #include "events/event.h" #include "events/roommemberevent.h" -#include "csapi/room_state.h" -#include "csapi/profile.h" -#include "csapi/content-repo.h" -#include <QtCore/QTimer> -#include <QtCore/QRegularExpression> +#include <QtCore/QElapsedTimer> #include <QtCore/QPointer> +#include <QtCore/QRegularExpression> #include <QtCore/QStringBuilder> -#include <QtCore/QElapsedTimer> +#include <QtCore/QTimer> #include <functional> -using namespace QMatrixClient; -using namespace std::placeholders; +using namespace Quotient; using std::move; -class User::Private -{ - public: - static Avatar makeAvatar(QUrl url) - { - return Avatar(move(url)); - } - - Private(QString userId, Connection* connection) - : userId(move(userId)), connection(connection) - { } - - QString userId; - Connection* connection; - - QString bridged; - QString mostUsedName; - QMultiHash<QString, const Room*> otherNames; - Avatar mostUsedAvatar { makeAvatar({}) }; - std::vector<Avatar> otherAvatars; - auto otherAvatar(QUrl url) - { - return std::find_if(otherAvatars.begin(), otherAvatars.end(), - [&url] (const auto& av) { return av.url() == url; }); - } - QMultiHash<QUrl, const Room*> avatarsToRooms; - - mutable int totalRooms = 0; - - QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, QString oldName); - QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; - void setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl); - - void setAvatarOnServer(QString contentUri, User* q); - +class User::Private { +public: + Private(QString userId) : id(move(userId)), hueF(stringToHueF(id)) { } + + QString id; + qreal hueF; + + QString defaultName; + Avatar defaultAvatar; + // NB: This container is ever-growing. Even if the user no more scrolls + // the timeline that far back, historical avatars are still kept around. + // This is consistent with the rest of Quotient, as room timelines + // are never vacuumed either. This will probably change in the future. + /// Map of mediaId to Avatar objects + static UnorderedMap<QString, Avatar> otherAvatars; }; - -QString User::Private::nameForRoom(const Room* r, const QString& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedName || otherNames.contains(hint, r)) - return hint; - return otherNames.key(r, mostUsedName); -} - -static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; - -void User::Private::setNameForRoom(const Room* r, QString newName, - QString oldName) -{ - Q_ASSERT(oldName != newName); - Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedName = move(newName); - return; - } - otherNames.remove(oldName, r); - if (newName != mostUsedName) - { - // Check if the newName is about to become most used. - if (otherNames.count(newName) >= totalRooms - otherNames.size()) - { - Q_ASSERT(totalRooms > 1); - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - { - qCDebug(MAIN) << "Switching the most used name of user" << userId - << "from" << mostUsedName << "to" << newName; - qCDebug(MAIN) << "The user is in" << totalRooms << "rooms"; - et.start(); - } - - for (auto* r1: connection->roomMap()) - if (nameForRoom(r1) == mostUsedName) - otherNames.insert(mostUsedName, r1); - - mostUsedName = newName; - otherNames.remove(newName); - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used name"; - } - else - otherNames.insert(newName, r); - } -} - -QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r)) - return hint; - auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r); - return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key(); -} - -void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl) -{ - Q_ASSERT(oldUrl != newUrl); - Q_ASSERT(oldUrl == mostUsedAvatar.url() || - avatarsToRooms.contains(oldUrl, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedAvatar.updateUrl(newUrl); - return; - } - avatarsToRooms.remove(oldUrl, r); - if (!avatarsToRooms.contains(oldUrl)) - { - auto it = otherAvatar(oldUrl); - if (it != otherAvatars.end()) - otherAvatars.erase(it); - } - if (newUrl != mostUsedAvatar.url()) - { - // Check if the new avatar is about to become most used. - if (avatarsToRooms.count(newUrl) >= totalRooms - avatarsToRooms.size()) - { - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - { - qCDebug(MAIN) << "Switching the most used avatar of user" << userId - << "from" << mostUsedAvatar.url().toDisplayString() - << "to" << newUrl.toDisplayString(); - et.start(); - } - avatarsToRooms.remove(newUrl); - auto nextMostUsedIt = otherAvatar(newUrl); - Q_ASSERT(nextMostUsedIt != otherAvatars.end()); - std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: connection->roomMap()) - if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) - avatarsToRooms.insert(nextMostUsedIt->url(), r1); - - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used avatar"; - } else { - if (otherAvatar(newUrl) == otherAvatars.end()) - otherAvatars.emplace_back(makeAvatar(newUrl)); - avatarsToRooms.insert(newUrl, r); - } - } -} +decltype(User::Private::otherAvatars) User::Private::otherAvatars {}; User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(move(userId), connection)) + : QObject(connection), d(makeImpl<Private>(move(userId))) { - setObjectName(userId); + setObjectName(id()); + if (connection->userId() == id()) { + // Load profile information for local user. + load(); + } } Connection* User::connection() const { - Q_ASSERT(d->connection); - return d->connection; + Q_ASSERT(parent()); + return static_cast<Connection*>(parent()); } -User::~User() = default; - -QString User::id() const +void User::load() { - return d->userId; + auto* profileJob = + connection()->callApi<GetUserProfileJob>(id()); + connect(profileJob, &BaseJob::result, this, [this, profileJob] { + d->defaultName = profileJob->displayname(); + d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); + emit defaultNameChanged(); + emit defaultAvatarChanged(); + }); } +QString User::id() const { return d->id; } + bool User::isGuest() const { - Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@')); - auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(), - [] (QChar c) { return c.isDigit(); }); - Q_ASSERT(it != d->userId.end()); + 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.cend()); return *it == ':'; } -QString User::name(const Room* room) const -{ - return d->nameForRoom(room); -} - -QString User::rawName(const Room* room) const -{ - return d->bridged.isEmpty() ? name(room) : - name(room) % " (" % d->bridged % ')'; -} +int User::hue() const { return int(hueF() * 359); } -void User::updateName(const QString& newName, const Room* room) -{ - updateName(newName, d->nameForRoom(room), room); -} - -void User::updateName(const QString& newName, const QString& oldName, - const Room* room) +QString User::name(const Room* room) const { - Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room)); - if (newName != oldName) - { - emit nameAboutToChange(newName, oldName, room); - d->setNameForRoom(room, newName, oldName); - setObjectName(displayname()); - emit nameChanged(newName, oldName, room); - } -} - -void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room) -{ - Q_ASSERT(oldUrl == d->mostUsedAvatar.url() || - d->avatarsToRooms.contains(oldUrl, room)); - if (newUrl != oldUrl) - { - d->setAvatarForRoom(room, newUrl, oldUrl); - setObjectName(displayname()); - emit avatarChanged(this, room); - } - + return room ? room->memberName(id()) : d->defaultName; } void User::rename(const QString& newName) { - auto job = connection()->callApi<SetDisplayNameJob>(id(), newName); - connect(job, &BaseJob::success, this, [=] { updateName(newName); }); + const auto actualNewName = sanitized(newName); + if (actualNewName == d->defaultName) + return; // Nothing to do + + connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName), + &BaseJob::success, this, [this, actualNewName] { + // Check again, it could have changed meanwhile + if (actualNewName != d->defaultName) { + d->defaultName = actualNewName; + emit defaultNameChanged(); + } else + qCWarning(MAIN) + << "User" << id() << "already has profile name set to" + << actualNewName; + }); } -void User::rename(const QString& newName, const Room* r) +void User::rename(const QString& newName, Room* r) { - if (!r) - { + if (!r) { qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" "is incorrect; client developer, please fix it"; rename(newName); return; } - Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, - "Attempt to rename a user that's not a room member"); - MemberEventContent evtC; - evtC.displayName = newName; - auto job = r->setMemberState(id(), RoomMemberEvent(move(evtC))); - connect(job, &BaseJob::success, this, [=] { updateName(newName, r); }); + // #481: take the current state and update it with the new name + if (const auto& maybeEvt = r->currentState().get<RoomMemberEvent>(id())) { + auto content = maybeEvt->content(); + if (content.membership == Membership::Join) { + content.displayName = sanitized(newName); + r->setState<RoomMemberEvent>(id(), move(content)); + // The state will be updated locally after it arrives with sync + return; + } + } + qCCritical(MEMBERS) + << "Attempt to rename a non-member in a room context - ignored"; } -bool User::setAvatar(const QString& fileName) +template <typename SourceT> +inline bool User::doSetAvatar(SourceT&& source) { - return avatarObject().upload(connection(), fileName, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); + return d->defaultAvatar.upload( + connection(), source, [this](const QUrl& contentUri) { + auto* j = connection()->callApi<SetAvatarUrlJob>(id(), contentUri); + connect(j, &BaseJob::success, this, + [this, contentUri] { + if (contentUri == d->defaultAvatar.url()) { + d->defaultAvatar.updateUrl(contentUri); + emit defaultAvatarChanged(); + } else + qCWarning(MAIN) << "User" << id() + << "already has avatar URL set to" + << contentUri.toDisplayString(); + }); + }); } -bool User::setAvatar(QIODevice* source) +bool User::setAvatar(const QString& fileName) { - return avatarObject().upload(connection(), source, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); + return doSetAvatar(fileName); } -void User::requestDirectChat() +bool User::setAvatar(QIODevice* source) { - connection()->requestDirectChat(this); + return doSetAvatar(source); } -void User::ignore() +void User::removeAvatar() { - connection()->addToIgnoredUsers(this); + connection()->callApi<SetAvatarUrlJob>(id(), QUrl()); } -void User::unmarkIgnore() -{ - connection()->removeFromIgnoredUsers(this); -} +void User::requestDirectChat() { connection()->requestDirectChat(this); } -void User::Private::setAvatarOnServer(QString contentUri, User* q) -{ - auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri); - connect(j, &BaseJob::success, q, - [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); }); -} +void User::ignore() { connection()->addToIgnoredUsers(this); } -QString User::displayname(const Room* room) const -{ - if (room) - return room->roomMembername(this); +void User::unmarkIgnore() { connection()->removeFromIgnoredUsers(this); } - const auto name = d->nameForRoom(nullptr); - return name.isEmpty() ? d->userId : name; -} +bool User::isIgnored() const { return connection()->isIgnored(this); } -QString User::fullName(const Room* room) const +QString User::displayname(const Room* room) const { - const auto name = d->nameForRoom(room); - return name.isEmpty() ? d->userId : name % " (" % d->userId % ')'; + return room ? room->safeMemberName(id()) + : d->defaultName.isEmpty() ? d->id : d->defaultName; } -QString User::bridged() const +QString User::fullName(const Room* room) const { - return d->bridged; + const auto displayName = name(room); + return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')'); } const Avatar& User::avatarObject(const Room* room) const { - auto it = d->otherAvatar(d->avatarUrlForRoom(room)); - return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar; + if (!room) + return d->defaultAvatar; + + const auto& url = room->memberAvatarUrl(id()); + const auto& mediaId = url.authority() + url.path(); + 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, []{}); + 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(d->connection, width, height, - [=] { emit avatarChanged(this, room); callback(); }); + return avatarObject(room).get(connection(), width, height, callback); } QString User::avatarMediaId(const Room* room) const @@ -372,50 +222,4 @@ QUrl User::avatarUrl(const Room* room) const return avatarObject(room).url(); } -void User::processEvent(const RoomMemberEvent& event, const Room* room) -{ - Q_ASSERT(room); - if (event.membership() != MembershipType::Invite && - event.membership() != MembershipType::Join) - return; - - auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave && - (event.membership() == MembershipType::Join || - event.membership() == MembershipType::Invite); - if (aboutToEnter) - ++d->totalRooms; - - auto newName = event.displayName(); - // `bridged` value uses the same notification signal as the name; - // it is assumed that first setting of the bridge occurs together with - // the first setting of the name, and further bridge updates are - // exceptionally rare (the only reasonable case being that the bridge - // changes the naming convention). For the same reason room-specific - // bridge tags are not supported at all. - QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$"); - auto match = reSuffix.match(newName); - if (match.hasMatch()) - { - if (d->bridged != match.captured(1)) - { - if (!d->bridged.isEmpty()) - qCWarning(MAIN) << "Bridge for user" << id() << "changed:" - << d->bridged << "->" << match.captured(1); - d->bridged = match.captured(1); - } - newName.truncate(match.capturedStart(0)); - } - if (event.prevContent()) - { - // FIXME: the hint doesn't work for bridged users - auto oldNameHint = - d->nameForRoom(room, event.prevContent()->displayName); - updateName(newName, oldNameHint, room); - updateAvatarUrl(event.avatarUrl(), - d->avatarUrlForRoom(room, event.prevContent()->avatarUrl), - room); - } else { - updateName(newName, room); - updateAvatarUrl(event.avatarUrl(), d->avatarUrlForRoom(room), room); - } -} +qreal User::hueF() const { return d->hueF; } @@ -1,148 +1,133 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include <QtCore/QString> -#include <QtCore/QObject> #include "avatar.h" +#include "util.h" + +#include <QtCore/QObject> -namespace QMatrixClient -{ - class Connection; - class Room; - class RoomMemberEvent; - - class User: public QObject - { - Q_OBJECT - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(bool isGuest READ isGuest CONSTANT) - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) - Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) - public: - User(QString userId, Connection* connection); - ~User() override; - - Connection* connection() const; - - /** Get unique stable user id - * User id is generated by the server and is not changed ever. - */ - QString id() const; - - /** Get the name chosen by the user - * 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 - */ - 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 - */ - 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. - * When \p room is non-null, this call is equivalent to - * Room::roomMembername invocation for the user (i.e. the user's - * disambiguated room-specific name is returned). - * \sa name, id, fullName, Room::roomMembername - */ - QString displayname(const Room* room = nullptr) const; - - /** Get user name and id in one string - * The constructed string follows the format 'name (id)' - * which the spec recommends for users disambiguation in - * a room context and in other places. - * \sa displayName, Room::roomMembername - */ - QString fullName(const Room* room = nullptr) const; - - /** - * Returns the name of bridge the user is connected from or empty. - */ - 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 - * may not work with non-Synapse servers. - */ - bool isGuest() const; - - const Avatar& avatarObject(const Room* room = nullptr) const; - Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr); - Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, - const Room* room = nullptr); - QImage avatar(int width, int height, const Room* room, - const Avatar::get_callback_t& callback); - - QString avatarMediaId(const Room* room = nullptr) const; - QUrl avatarUrl(const Room* room = nullptr) const; - - void processEvent(const RoomMemberEvent& event, const Room* r); - - public slots: - /** Set a new name in the global user profile */ - void rename(const QString& newName); - /** Set a new name for the user in one room */ - void rename(const QString& newName, const Room* r); - /** Upload the file and use it as an avatar */ - bool setAvatar(const QString& fileName); - /** Upload contents of the QIODevice and set that as an avatar */ - bool setAvatar(QIODevice* source); - /** Create or find a direct chat with this user - * The resulting chat is returned asynchronously via - * Connection::directChatAvailable() - */ - void requestDirectChat(); - /** Add the user to the ignore list */ - void ignore(); - /** Remove the user from the ignore list */ - void unmarkIgnore(); - - signals: - void nameAboutToChange(QString newName, QString oldName, - const Room* roomContext); - void nameChanged(QString newName, QString oldName, - const Room* roomContext); - void avatarChanged(User* user, const Room* roomContext); - - private slots: - void updateName(const QString& newName, const Room* room = nullptr); - void updateName(const QString& newName, const QString& oldName, - const Room* room = nullptr); - void updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room = nullptr); - - private: - class Private; - QScopedPointer<Private> d; - }; -} -Q_DECLARE_METATYPE(QMatrixClient::User*) +namespace Quotient { +class Connection; +class Room; +class RoomMemberEvent; + +class QUOTIENT_API User : public QObject { + Q_OBJECT + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(bool isGuest READ isGuest CONSTANT) + Q_PROPERTY(int hue READ hue CONSTANT) + Q_PROPERTY(qreal hueF READ hueF CONSTANT) + 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 avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged) +public: + User(QString userId, Connection* connection); + + Connection* connection() const; + + /** Get unique stable user id + * User id is generated by the server and is not changed ever. + */ + QString id() const; + + /** Get the name chosen by the user + * 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 + */ + QString name(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. + * When \p room is non-null, this call is equivalent to + * Room::roomMembername invocation for the user (i.e. the user's + * disambiguated room-specific name is returned). + * \sa name, id, fullName, Room::roomMembername + */ + QString displayname(const Room* room = nullptr) const; + + /** Get user name and id in one string + * The constructed string follows the format 'name (id)' + * which the spec recommends for users disambiguation in + * a room context and in other places. + * \sa displayName, Room::roomMembername + */ + QString fullName(const Room* room = nullptr) 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 + * may not work with non-Synapse servers. + */ + bool isGuest() const; + + /** Hue color component of this user based on id. + * The implementation is based on XEP-0392: + * https://xmpp.org/extensions/xep-0392.html + * Naming and ranges are the same as QColor's hue methods: + * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision + */ + int hue() const; + qreal hueF() const; + + /// Get a reference to a user avatar object for a given room + /*! This reference should be considered short-lived: processing the next + * room member event for this user may (or may not) invalidate it. + */ + const Avatar& avatarObject(const Room* room = nullptr) const; + Q_INVOKABLE QImage avatar(int dimension, + const Quotient::Room* room = nullptr) const; + Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, + const Quotient::Room* room = nullptr) const; + QImage avatar(int width, int height, const Room* room, + const Avatar::get_callback_t& callback) const; + + QString avatarMediaId(const Room* room = nullptr) const; + QUrl avatarUrl(const Room* room = nullptr) const; + +public Q_SLOTS: + /// Set a new name in the global user profile + void rename(const QString& newName); + /// Set a new name for the user in one room + void rename(const QString& newName, Room* r); + /// Upload the file and use it as an avatar + bool setAvatar(const QString& fileName); + /// Upload contents of the QIODevice and set that as an avatar + bool setAvatar(QIODevice* source); + /// Removes the avatar from the profile + void removeAvatar(); + /// Create or find a direct chat with this user + /*! The resulting chat is returned asynchronously via + * Connection::directChatAvailable() + */ + void requestDirectChat(); + /// Add the user to the ignore list + void ignore(); + /// Remove the user from the ignore list + void unmarkIgnore(); + /// Check whether the user is in ignore list + bool isIgnored() const; + /// Force loading displayName and avartar url. This is required in + /// some cases where the you need to use an user independent of the + /// room. + void load(); + +Q_SIGNALS: + void defaultNameChanged(); + void defaultAvatarChanged(); + +private: + class Private; + ImplPtr<Private> d; + + template <typename SourceT> + bool doSetAvatar(SourceT&& source); +}; +} // namespace Quotient diff --git a/lib/util.cpp b/lib/util.cpp index 1773fcfe..359b2959 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -1,63 +1,146 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "util.h" +#include <QtCore/QCryptographicHash> +#include <QtCore/QDataStream> +#include <QtCore/QDir> #include <QtCore/QRegularExpression> +#include <QtCore/QStandardPaths> +#include <QtCore/QStringBuilder> +#include <QtCore/QtEndian> static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption - | QRegularExpression::OptimizeOnFirstUsageOption | QRegularExpression::UseUnicodePropertiesOption; // Converts all that looks like a URL into HTML links -static void linkifyUrls(QString& htmlEscapedText) +void Quotient::linkifyUrls(QString& htmlEscapedText) { + // Note: outer parentheses are a part of C++ raw string delimiters, not of + // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). + // Note2: the next-outer parentheses are \N in the replacement. + + // generic url: // regexp is originally taken from Konsole (https://github.com/KDE/konsole) - // full url: // protocolname:// or www. followed by anything other than whitespaces, // <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, // comma or dot - // Note: outer parentheses are a part of C++ raw string delimiters, not of - // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). - static const QRegularExpression FullUrlRegExp(QStringLiteral( - R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" - ), RegExpOptions); + static const QRegularExpression FullUrlRegExp( + QStringLiteral( + 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] - static const QRegularExpression EmailAddressRegExp(QStringLiteral( - R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))" - ), RegExpOptions); + static const QRegularExpression EmailAddressRegExp( + QStringLiteral(R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"), + RegExpOptions); + // An interim liberal implementation of + // https://matrix.org/docs/spec/appendices.html#identifier-grammar + static const QRegularExpression MxIdRegExp( + QStringLiteral( + R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"), + RegExpOptions); + Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid() + && MxIdRegExp.isValid()); - // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,& + // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," htmlEscapedText.replace(EmailAddressRegExp, - QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)")); + QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)")); htmlEscapedText.replace(FullUrlRegExp, - QStringLiteral(R"(<a href="\1">\1</a>)")); + QStringLiteral(R"(<a href="\1">\1</a>)")); + htmlEscapedText.replace( + MxIdRegExp, + QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)")); } -QString QMatrixClient::prettyPrint(const QString& plainText) +QString Quotient::sanitized(const QString& plainText) { - auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") + - plainText.toHtmlEscaped() + QStringLiteral("</span>"); - pt.replace('\n', QStringLiteral("<br/>")); + auto text = plainText; + text.remove(QChar(0x202e)); // RLO + text.remove(QChar(0x202d)); // LRO + text.remove(QChar(0xfffc)); // Object replacement character + return text; +} +QString Quotient::prettyPrint(const QString& plainText) +{ + auto pt = plainText.toHtmlEscaped(); linkifyUrls(pt); - return pt; + pt.replace('\n', QStringLiteral("<br/>")); + return QStringLiteral("<span style='white-space:pre-wrap'>") + pt + + QStringLiteral("</span>"); +} + +QString Quotient::cacheLocation(const QString& dirName) +{ + const QString cachePath = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) % '/' + % dirName % '/'; + QDir dir; + if (!dir.exists(cachePath)) + dir.mkpath(cachePath); + return cachePath; +} + +qreal Quotient::stringToHueF(const QString& s) +{ + Q_ASSERT(!s.isEmpty()); + QByteArray hash = QCryptographicHash::hash(s.toUtf8(), + QCryptographicHash::Sha1); + QDataStream dataStream(hash.left(2)); + dataStream.setByteOrder(QDataStream::LittleEndian); + quint16 hashValue; + dataStream >> hashValue; + const auto hueF = qreal(hashValue) / std::numeric_limits<quint16>::max(); + Q_ASSERT((0 <= hueF) && (hueF <= 1)); + return hueF; +} + +static const auto ServerPartRegEx = QStringLiteral( + "(\\[[^][:space:]]+]|[-[:alnum:].]+)" // IPv6 address or hostname/IPv4 address + "(?::(\\d{1,5}))?" // Optional port +); + +QString Quotient::serverPart(const QString& mxId) +{ + static QString re = "^[@!#$+].*?:(" // Localpart and colon + % ServerPartRegEx % ")$"; + static QRegularExpression parser( + re, + QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits + Q_ASSERT(parser.isValid()); + return parser.match(mxId).captured(1); +} + +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; +} + +bool Quotient::encryptionSupported() +{ +#ifdef Quotient_E2EE_ENABLED + return true; +#else + return false; +#endif } @@ -1,244 +1,251 @@ -/****************************************************************************** - * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include <QtCore/QPointer> -#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0)) -#include <QtCore/QMetaEnum> -#include <QtCore/QDebug> -#endif +#include "quotient_export.h" + +#include <QtCore/QLatin1String> +#include <QtCore/QHashFunctions> -#include <functional> #include <memory> +#include <unordered_map> -#if __cplusplus >= 201703L -#define FALLTHROUGH [[fallthrough]] -#elif __has_cpp_attribute(clang::fallthrough) -#define FALLTHROUGH [[clang::fallthrough]] -#else -#define FALLTHROUGH // -fallthrough +#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 -// Along the lines of Q_DISABLE_COPY -#define DISABLE_MOVE(_ClassName) \ - _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \ - _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE; +#ifndef Q_DISABLE_COPY_MOVE +#define Q_DISABLE_COPY_MOVE(Class) \ + Q_DISABLE_COPY(Class) \ + Q_DISABLE_MOVE(Class) +#endif -#if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) -// Copy-pasted from Qt 5.10 -template <typename T> -Q_DECL_CONSTEXPR typename std::add_const<T>::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } -// prevent rvalue arguments: -template <typename T> -static void qAsConst(const T &&) Q_DECL_EQ_DELETE; +#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 QMatrixClient -{ - // The below enables pretty-printing of enums in logs -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -#define REGISTER_ENUM(EnumName) Q_ENUM(EnumName) +#if __cpp_conditional_explicit >= 201806L +#define QUO_IMPLICIT explicit(false) #else - // Thanks to Olivier for spelling it and for making Q_ENUM to replace it: - // https://woboq.com/blog/q_enum.html -#define REGISTER_ENUM(EnumName) \ - Q_ENUMS(EnumName) \ - friend QDebug operator<<(QDebug dbg, EnumName val) \ - { \ - static int enumIdx = staticMetaObject.indexOfEnumerator(#EnumName); \ - return dbg << Event::staticMetaObject.enumerator(enumIdx).valueToKey(int(val)); \ - } +#define QUO_IMPLICIT #endif - /** static_cast<> for unique_ptr's */ - template <typename T1, typename PtrT2> - inline auto unique_ptr_cast(PtrT2&& p) +#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ + Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended + +/// \brief Copy an object with slicing +/// +/// Unintended slicing is bad, which why there's a C++ Core Guideline that +/// basically says "don't slice, or if you do, make it explicit". Sonar and +/// clang-tidy have warnings matching this guideline; unfortunately, those +/// warnings trigger even when you have a dedicated method (as the guideline +/// recommends) that makes a slicing copy. +/// +/// This macro is meant for cases when slicing is intended: the static cast +/// silences the static analysis warning, and the macro appearance itself makes +/// it very clear that slicing is wanted here. It is made as a macro +/// (not as a function template) to support the case of private inheritance +/// in which a function template would not be able to cast to the private base +/// (see Uri::toUrl() for an example of just that situation). +#define SLICE(Object, ToType) ToType{static_cast<const ToType&>(Object)} + +namespace Quotient { +/// An equivalent of std::hash for QTypes to enable std::unordered_map<QType, ...> +template <typename T> +struct HashQ { + size_t operator()(const T& s) const Q_DECL_NOEXCEPT { - return std::unique_ptr<T1>(static_cast<T1*>(p.release())); + return qHash(s, uint(qGlobalQHashSeed())); } +}; +/// A wrapper around std::unordered_map compatible with types that have qHash +template <typename KeyT, typename ValT> +using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>; - struct NoneTag {}; - constexpr NoneTag none {}; +constexpr auto operator"" _ls(const char* s, std::size_t size) +{ + return QLatin1String(s, int(size)); +} - /** A crude substitute for `optional` while we're not C++17 - * - * Only works with default-constructible types. - */ - template <typename T> - class Omittable +/** An abstraction over a pair of iterators + * This is a very basic range type over a container with iterators that + * are at least ForwardIterators. Inspired by Ranges TS. + */ +template <typename ArrayT> +class Range { + // Looking forward to C++20 ranges + using iterator = typename ArrayT::iterator; + using const_iterator = typename ArrayT::const_iterator; + using size_type = typename ArrayT::size_type; + +public: + constexpr Range(ArrayT& arr) : from(std::begin(arr)), to(std::end(arr)) {} + constexpr Range(iterator from, iterator to) : from(from), to(to) {} + + constexpr size_type size() const { - static_assert(!std::is_reference<T>::value, - "You cannot make an Omittable<> with a reference type"); - public: - explicit Omittable() : Omittable(none) { } - Omittable(NoneTag) : _value(std::decay_t<T>()), _omitted(true) { } - Omittable(const std::decay_t<T>& val) : _value(val) { } - Omittable(std::decay_t<T>&& val) : _value(std::move(val)) { } - Omittable<T>& operator=(const std::decay_t<T>& val) - { - _value = val; - _omitted = false; - return *this; - } - Omittable<T>& operator=(std::decay_t<T>&& val) - { - _value = std::move(val); - _omitted = false; - return *this; - } - - bool omitted() const { return _omitted; } - const std::decay_t<T>& value() const { Q_ASSERT(!_omitted); return _value; } - std::decay_t<T>& value() { Q_ASSERT(!_omitted); return _value; } - std::decay_t<T>&& release() { _omitted = true; return std::move(_value); } - - operator bool() const { return !omitted(); } - const std::decay<T>* operator->() const { return &value(); } - std::decay_t<T>* operator->() { return &value(); } - const std::decay_t<T>& operator*() const { return value(); } - std::decay_t<T>& operator*() { return value(); } - - private: - T _value; - bool _omitted = false; - }; + Q_ASSERT(std::distance(from, to) >= 0); + return size_type(std::distance(from, to)); + } + constexpr bool empty() const { return from == to; } + constexpr const_iterator begin() const { return from; } + constexpr const_iterator end() const { return to; } + constexpr iterator begin() { return from; } + constexpr iterator end() { return to; } - /** Determine traits of an arbitrary function/lambda/functor - * This only works with arity of 1 (1-argument) for now but is extendable - * to other cases. Also, doesn't work with generic lambdas and function - * objects that have operator() overloaded - * \sa https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 - */ - template <typename T> - struct function_traits : public function_traits<decltype(&T::operator())> - { }; // A generic function object that has (non-overloaded) operator() - - // Specialisation for a function - template <typename ReturnT, typename ArgT> - struct function_traits<ReturnT(ArgT)> - { - using return_type = ReturnT; - using arg_type = ArgT; - }; +private: + iterator from; + iterator to; +}; - // Specialisation for a member function - template <typename ReturnT, typename ClassT, typename ArgT> - struct function_traits<ReturnT(ClassT::*)(ArgT)> - : function_traits<ReturnT(ArgT)> - { }; +template <typename T> +class asKeyValueRange +{ +public: + asKeyValueRange(T& data) + : m_data { data } + {} - // Specialisation for a const member function - template <typename ReturnT, typename ClassT, typename ArgT> - struct function_traits<ReturnT(ClassT::*)(ArgT) const> - : function_traits<ReturnT(ArgT)> - { }; + auto begin() { return m_data.keyValueBegin(); } + auto end() { return m_data.keyValueEnd(); } - template <typename FnT> - using fn_return_t = typename function_traits<FnT>::return_type; +private: + T &m_data; +}; +template <typename T> +asKeyValueRange(T&) -> asKeyValueRange<T>; - template <typename FnT> - using fn_arg_t = typename function_traits<FnT>::arg_type; +/** A replica of std::find_first_of that returns a pair of iterators + * + * Convenient for cases when you need to know which particular "first of" + * [sFirst, sLast) has been found in [first, last). + */ +template <typename InputIt, typename ForwardIt, typename Pred> +inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, + ForwardIt sFirst, + ForwardIt sLast, Pred pred) +{ + for (; first != last; ++first) + for (auto it = sFirst; it != sLast; ++it) + if (pred(*first, *it)) + return std::make_pair(first, it); + + return std::make_pair(last, sLast); +} + +//! \brief An owning implementation pointer +//! +//! This is basically std::unique_ptr<> to hold your pimpl's but without having +//! to define default constructors/operator=() out of line. +//! Thanks to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html +//! for inspiration +template <typename ImplType, typename TypeToDelete = ImplType> +using ImplPtr = std::unique_ptr<ImplType, void (*)(TypeToDelete*)>; + +// Why this works (see also the link above): because this defers the moment +// of requiring sizeof of ImplType to the place where makeImpl is invoked +// (which is located, necessarily, in the .cpp file after ImplType definition). +// The stock unique_ptr deleter (std::default_delete) normally needs sizeof +// at the same spot - as long as you defer definition of the owning type +// constructors and operator='s to the .cpp file as well. Which means you +// have to explicitly declare and define them (even if with = default), +// formally breaking the rule of zero; informally, just adding boilerplate code. +// The custom deleter itself is instantiated at makeImpl invocation - there's +// no way earlier to even know how ImplType will be deleted and whether that +// will need sizeof(ImplType) earlier. In theory it's a tad slower because +// the deleter is called by the pointer; however, the difference will not +// be noticeable (if exist at all) for any class with non-trivial contents. + +//! \brief make_unique for ImplPtr +//! +//! Since std::make_unique is not compatible with ImplPtr, this should be used +//! in constructors of frontend classes to create implementation instances. +template <typename ImplType, typename TypeToDelete = ImplType, typename... ArgTs> +inline ImplPtr<ImplType, TypeToDelete> makeImpl(ArgTs&&... args) +{ + return ImplPtr<ImplType, TypeToDelete> { + new ImplType{std::forward<ArgTs>(args)...}, + [](TypeToDelete* impl) { delete impl; } + }; +} - inline auto operator"" _ls(const char* s, std::size_t size) - { - return QLatin1String(s, int(size)); - } +template <typename ImplType, typename TypeToDelete = ImplType> +inline ImplPtr<ImplType, TypeToDelete> acquireImpl(ImplType* from) +{ + return ImplPtr<ImplType, TypeToDelete> { from, [](TypeToDelete* impl) { + delete impl; + } }; +} - /** An abstraction over a pair of iterators - * This is a very basic range type over a container with iterators that - * are at least ForwardIterators. Inspired by Ranges TS. - */ - template <typename ArrayT> - class Range - { - // Looking forward for Ranges TS to produce something (in C++23?..) - using iterator = typename ArrayT::iterator; - using const_iterator = typename ArrayT::const_iterator; - using size_type = typename ArrayT::size_type; - public: - Range(ArrayT& arr) : from(std::begin(arr)), to(std::end(arr)) { } - Range(iterator from, iterator to) : from(from), to(to) { } - - size_type size() const - { - Q_ASSERT(std::distance(from, to) >= 0); - return size_type(std::distance(from, to)); - } - bool empty() const { return from == to; } - const_iterator begin() const { return from; } - const_iterator end() const { return to; } - iterator begin() { return from; } - iterator end() { return to; } - - private: - iterator from; - iterator to; - }; +template <typename ImplType, typename TypeToDelete = ImplType> +constexpr ImplPtr<ImplType, TypeToDelete> ZeroImpl() +{ + return { nullptr, [](TypeToDelete*) { /* nullptr doesn't need deletion */ } }; +} + +//! \brief Multiplex several functors in one +//! +//! This is a well-known trick to wrap several lambdas into a single functor +//! class that can be passed to std::visit. +//! \sa https://en.cppreference.com/w/cpp/utility/variant/visit +template <typename... FunctorTs> +struct Overloads : FunctorTs... { + using FunctorTs::operator()...; +}; + +template <typename... FunctorTs> +Overloads(FunctorTs&&...) -> Overloads<FunctorTs...>; + +/** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ +QUOTIENT_API void linkifyUrls(QString& htmlEscapedText); + +/** Sanitize the text before showing in HTML + * + * This does toHtmlEscaped() and removes Unicode BiDi marks. + */ +QUOTIENT_API QString sanitized(const QString& plainText); - /** A replica of std::find_first_of that returns a pair of iterators - * - * Convenient for cases when you need to know which particular "first of" - * [sFirst, sLast) has been found in [first, last). - */ - template<typename InputIt, typename ForwardIt, typename Pred> - inline std::pair<InputIt, ForwardIt> findFirstOf( - InputIt first, InputIt last, ForwardIt sFirst, ForwardIt sLast, - Pred pred) - { - for (; first != last; ++first) - for (auto it = sFirst; it != sLast; ++it) - if (pred(*first, *it)) - return std::make_pair(first, it); +/** Pretty-print plain text into HTML + * + * This includes HTML escaping of <,>,",& and calling linkifyUrls() + */ +QUOTIENT_API QString prettyPrint(const QString& plainText); - return std::make_pair(last, sLast); - } +/** Return a path to cache directory after making sure that it exists + * + * The returned path has a trailing slash, clients don't need to append it. + * \param dirName path to cache directory relative to the standard cache path + */ +QUOTIENT_API QString cacheLocation(const QString& dirName); - /** A guard pointer that disconnects an interested object upon destruction - * It's almost QPointer<> except that you have to initialise it with one - * more additional parameter - a pointer to a QObject that will be - * disconnected from signals of the underlying pointer upon the guard's - * destruction. - */ - template <typename T> - class ConnectionsGuard : public QPointer<T> - { - public: - ConnectionsGuard(T* publisher, QObject* subscriber) - : QPointer<T>(publisher), subscriber(subscriber) - { } - ~ConnectionsGuard() - { - if (*this) - (*this)->disconnect(subscriber); - } - ConnectionsGuard(ConnectionsGuard&&) = default; - ConnectionsGuard& operator=(ConnectionsGuard&&) = default; - Q_DISABLE_COPY(ConnectionsGuard) - using QPointer<T>::operator=; - - private: - QObject* subscriber; - }; +/** Hue color component of based of the hash of the string. + * + * The implementation is based on XEP-0392: + * https://xmpp.org/extensions/xep-0392.html + * Naming and range are the same as QColor's hueF method: + * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision + */ +QUOTIENT_API qreal stringToHueF(const QString& s); - /** Pretty-prints plain text into HTML - * This includes HTML escaping of <,>,",& and URLs linkification. - */ - QString prettyPrint(const QString& plainText); -} // namespace QMatrixClient +/** Extract the serverpart from MXID */ +QUOTIENT_API QString serverPart(const QString& mxId); +QUOTIENT_API QString versionString(); +QUOTIENT_API int majorVersion(); +QUOTIENT_API int minorVersion(); +QUOTIENT_API int patchVersion(); +QUOTIENT_API bool encryptionSupported(); +} // namespace Quotient diff --git a/libqmatrixclient.pri b/libquotient.pri index cb90a9fd..1b4bd9c0 100644 --- a/libqmatrixclient.pri +++ b/libquotient.pri @@ -1,10 +1,25 @@ -QT += network -CONFIG += c++14 warn_on rtti_off create_prl object_parallel_to_source +QT += network multimedia +QT -= gui + +CONFIG *= c++20 warn_on rtti_off create_prl object_parallel_to_source win32-msvc* { - QMAKE_CXXFLAGS_WARN_ON += -wd4100 + # Quotient code base does not play well with NMake inference rules + CONFIG *= no_batch + QMAKE_CXXFLAGS_WARN_ON *= -wd4100 -wd4267 + QMAKE_CXXFLAGS *= /std:c++17 # Older Qt doesn't understand c++1z above } else { - QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter + QMAKE_CXXFLAGS_WARN_ON *= -Wno-unused-parameter +} + +DEFINES += QT_NO_JAVA_STYLE_ITERATORS +contains(DEFINES, Quotient_E2EE_ENABLED=.) { + contains(DEFINES, USE_INTREE_LIBQOLM=.) { + include(3rdparty/libQtOlm/libQtOlm.pri) + } else { + CONFIG += link_pkgconfig + PKGCONFIG += QtOlm + } } SRCPATH = $$PWD/lib @@ -13,35 +28,49 @@ INCLUDEPATH += $$SRCPATH HEADERS += \ $$SRCPATH/connectiondata.h \ $$SRCPATH/connection.h \ + $$SRCPATH/ssosession.h \ + $$SRCPATH/encryptionmanager.h \ $$SRCPATH/eventitem.h \ $$SRCPATH/room.h \ $$SRCPATH/user.h \ $$SRCPATH/avatar.h \ + $$SRCPATH/uri.h \ + $$SRCPATH/uriresolver.h \ + $$SRCPATH/syncdata.h \ + $$SRCPATH/quotient_common.h \ $$SRCPATH/util.h \ + $$SRCPATH/qt_connection_util.h \ $$SRCPATH/events/event.h \ $$SRCPATH/events/roomevent.h \ $$SRCPATH/events/stateevent.h \ $$SRCPATH/events/eventcontent.h \ $$SRCPATH/events/roommessageevent.h \ $$SRCPATH/events/simplestateevents.h \ + $$SRCPATH/events/roomcanonicalaliasevent.h \ + $$SRCPATH/events/roomcreateevent.h \ + $$SRCPATH/events/roomtombstoneevent.h \ $$SRCPATH/events/roommemberevent.h \ $$SRCPATH/events/roomavatarevent.h \ $$SRCPATH/events/typingevent.h \ $$SRCPATH/events/receiptevent.h \ + $$SRCPATH/events/reactionevent.h \ $$SRCPATH/events/callanswerevent.h \ $$SRCPATH/events/callcandidatesevent.h \ $$SRCPATH/events/callhangupevent.h \ $$SRCPATH/events/callinviteevent.h \ $$SRCPATH/events/accountdataevents.h \ $$SRCPATH/events/directchatevent.h \ + $$SRCPATH/events/encryptionevent.h \ + $$SRCPATH/events/encryptedevent.h \ + $$SRCPATH/events/roomkeyevent.h \ $$SRCPATH/events/redactionevent.h \ $$SRCPATH/events/eventloader.h \ + $$SRCPATH/events/roompowerlevelsevent.h \ $$SRCPATH/jobs/requestdata.h \ $$SRCPATH/jobs/basejob.h \ $$SRCPATH/jobs/syncjob.h \ $$SRCPATH/jobs/mediathumbnailjob.h \ $$SRCPATH/jobs/downloadfilejob.h \ - $$SRCPATH/jobs/postreadmarkersjob.h \ $$files($$SRCPATH/csapi/*.h, false) \ $$files($$SRCPATH/csapi/definitions/*.h, false) \ $$files($$SRCPATH/csapi/definitions/wellknown/*.h, false) \ @@ -56,34 +85,42 @@ HEADERS += \ SOURCES += \ $$SRCPATH/connectiondata.cpp \ $$SRCPATH/connection.cpp \ + $$SRCPATH/ssosession.cpp \ + $$SRCPATH/encryptionmanager.cpp \ $$SRCPATH/eventitem.cpp \ $$SRCPATH/room.cpp \ $$SRCPATH/user.cpp \ $$SRCPATH/avatar.cpp \ + $$SRCPATH/uri.cpp \ + $$SRCPATH/uriresolver.cpp \ + $$SRCPATH/syncdata.cpp \ $$SRCPATH/util.cpp \ $$SRCPATH/events/event.cpp \ $$SRCPATH/events/roomevent.cpp \ $$SRCPATH/events/stateevent.cpp \ $$SRCPATH/events/eventcontent.cpp \ + $$SRCPATH/events/roomcreateevent.cpp \ + $$SRCPATH/events/roomtombstoneevent.cpp \ $$SRCPATH/events/roommessageevent.cpp \ $$SRCPATH/events/roommemberevent.cpp \ $$SRCPATH/events/typingevent.cpp \ + $$SRCPATH/events/reactionevent.cpp \ $$SRCPATH/events/callanswerevent.cpp \ $$SRCPATH/events/callcandidatesevent.cpp \ $$SRCPATH/events/callhangupevent.cpp \ $$SRCPATH/events/callinviteevent.cpp \ $$SRCPATH/events/receiptevent.cpp \ $$SRCPATH/events/directchatevent.cpp \ + $$SRCPATH/events/encryptionevent.cpp \ + $$SRCPATH/events/encryptedevent.cpp \ + $$SRCPATH/events/roomkeyevent.cpp \ + $$SRCPATH/events/roompowerlevelsevent.cpp \ $$SRCPATH/jobs/requestdata.cpp \ $$SRCPATH/jobs/basejob.cpp \ $$SRCPATH/jobs/syncjob.cpp \ $$SRCPATH/jobs/mediathumbnailjob.cpp \ $$SRCPATH/jobs/downloadfilejob.cpp \ $$files($$SRCPATH/csapi/*.cpp, false) \ - $$files($$SRCPATH/csapi/definitions/*.cpp, false) \ - $$files($$SRCPATH/csapi/definitions/wellknown/*.cpp, false) \ - $$files($$SRCPATH/application-service/definitions/*.cpp, false) \ - $$files($$SRCPATH/identity/definitions/*.cpp, false) \ $$SRCPATH/logging.cpp \ $$SRCPATH/converters.cpp \ $$SRCPATH/settings.cpp \ diff --git a/qmc-example.pro b/qmc-example.pro deleted file mode 100644 index b060bab1..00000000 --- a/qmc-example.pro +++ /dev/null @@ -1,12 +0,0 @@ -TEMPLATE = app - -CONFIG += object_parallel_to_source - -windows { CONFIG += console } - -include(libqmatrixclient.pri) - -SOURCES += examples/qmc-example.cpp - -DISTFILES += \ - .valgrind.qmc-example.supp diff --git a/quotest.pro b/quotest.pro new file mode 100644 index 00000000..891873af --- /dev/null +++ b/quotest.pro @@ -0,0 +1,14 @@ +TEMPLATE = app + +include(libquotient.pri) + +QT += testlib + +CONFIG *= c++1z warn_on object_parallel_to_source + +windows { CONFIG *= console } + +SOURCES += tests/quotest.cpp + +DISTFILES += \ + .valgrind.supp diff --git a/quotest/.valgrind.supp b/quotest/.valgrind.supp new file mode 100644 index 00000000..d65fb52e --- /dev/null +++ b/quotest/.valgrind.supp @@ -0,0 +1,68 @@ +{ + libc_dirty_free_on_exit + Memcheck:Free + fun:free + fun:__libc_freeres + fun:_vgnU_freeres + fun:__run_exit_handlers + fun:exit +} + +{ + QAuthenticator + Memcheck:Leak + match-leak-kinds: possible + ... + fun:_ZN14QAuthenticator6detachEv +} + +{ + QTimer + Memcheck:Leak + match-leak-kinds: possible + fun:_Znwm + fun:_ZN7QObjectC1EPS_ + fun:_ZN6QTimerC1EP7QObject +} + +{ + QSslConfiguration + Memcheck:Leak + match-leak-kinds: possible + fun:_Znwm + ... + fun:_ZN17QSslConfigurationC1Ev +} + +{ + libcrypto_ASN1 + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:ASN1_item_ex_d2i +} + +{ + malloc_from_libcrypto + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:CRYPTO_malloc + ... + obj:/lib/x86_64-linux-gnu/libcrypto.so.* +} + +{ + Slot_activation_from_QtNetwork + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:inflateInit2_ + obj:/*/*/*/libQt5Network.so.* + ... + fun:_ZN11QMetaObject8activateEP7QObjectiiPPv + ... + fun:_ZN11QMetaObject8activateEP7QObjectiiPPv + obj:/*/*/*/libQt5Network.so.* +}
\ No newline at end of file diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt new file mode 100644 index 00000000..ec305620 --- /dev/null +++ b/quotest/CMakeLists.txt @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +# +# SPDX-License-Identifier: BSD-3-Clause + +set(quotest_SRCS quotest.cpp) + +find_package(${Qt} COMPONENTS Concurrent) +add_executable(quotest ${quotest_SRCS}) +target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${Qt}::Concurrent ${PROJECT_NAME}) + +set_target_properties(quotest PROPERTIES + VISIBILITY_INLINES_HIDDEN ON + CXX_VISIBILITY_PRESET hidden +) + +if (MSVC) + target_compile_options(quotest PUBLIC /EHsc /W4 + /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 + /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 + /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) +else() + foreach (FLAG W Wall Wpedantic Wextra Wno-unused-parameter Werror=return-type) + CHECK_CXX_COMPILER_FLAG("-${FLAG}" COMPILER_${FLAG}_SUPPORTED) + if (COMPILER_${FLAG}_SUPPORTED AND + NOT CMAKE_CXX_FLAGS MATCHES "(^| )-?${FLAG}($| )") + target_compile_options(quotest PUBLIC -${FLAG}) + endif () + endforeach () +endif() + +option(${PROJECT_NAME}_INSTALL_TESTS "install quotest application" ON) +add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS + "the library functional test suite") + +if (${PROJECT_NAME}_INSTALL_TESTS) + install(TARGETS quotest RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif () diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp new file mode 100644 index 00000000..624888be --- /dev/null +++ b/quotest/quotest.cpp @@ -0,0 +1,1048 @@ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "connection.h" +#include "room.h" +#include "user.h" +#include "uriresolver.h" +#include "networkaccessmanager.h" +#include "qt_connection_util.h" + +#include "csapi/joining.h" +#include "csapi/leaving.h" +#include "csapi/room_send.h" + +#include "events/reactionevent.h" +#include "events/redactionevent.h" +#include "events/simplestateevents.h" +#include "events/roommemberevent.h" + +#include <QtTest/QSignalSpy> +#include <QtCore/QCoreApplication> +#include <QtCore/QFileInfo> +#include <QtCore/QStringBuilder> +#include <QtCore/QTemporaryFile> +#include <QtCore/QTimer> +#include <QtConcurrent/QtConcurrent> +#include <QtNetwork/QNetworkReply> + +#include <functional> +#include <iostream> + +using namespace Quotient; +using std::clog, std::endl; + +class TestSuite; + +class TestManager : public QCoreApplication { +public: + TestManager(int& argc, char** argv); + +private: + void setupAndRun(); + void onNewRoom(Room* r); + void doTests(); + void conclude(); + void finalize(); + +private: + Connection* c = nullptr; + QString origin; + QString targetRoomName; + TestSuite* testSuite = nullptr; + QByteArrayList running {}, succeeded {}, failed {}; +}; + +using TestToken = decltype(std::declval<QMetaMethod>().name()); +Q_DECLARE_METATYPE(TestToken) + +// For now, the token itself is the test name but that may change. +const char* testName(const TestToken& token) { return token.constData(); } + +/// Test function declaration +/*! + * \return true, if the test finished (successfully or unsuccessfully); + * false, if the test went async and will complete later + */ +#define TEST_DECL(Name) bool Name(const TestToken& thisTest); + +/// The holder for the actual tests +/*! + * This class takes inspiration from Qt Test in terms of tests invocation; + * TestManager instantiates it and runs all public slots (cf. private slots in + * Qt Test) one after another. An important diversion from Qt Test is that + * the tests are assumed to by asynchronous rather than synchronous; so it's + * perfectly normal to have a few tests running at the same time. To avoid + * context clashes a special parameter with the name thisTest is passed to + * each test. Each test must conclude (synchronously or asynchronously) with + * an invocation of FINISH_TEST() macro (or FAIL_TEST() macro that expands to + * FINISH_TEST) that expects thisTest variable to be reachable. If FINISH_TEST() + * is invoked twice with the same thisTest, the second call will cause assertion + * failure; if FINISH_TEST() is not invoked at all, the test will be killed + * by a watchdog after a timeout and marked in the final report as not finished. + */ +class TestSuite : public QObject { + Q_OBJECT +public: + TestSuite(Room* testRoom, QString source, TestManager* parent) + : QObject(parent), targetRoom(testRoom), origin(std::move(source)) + { + qRegisterMetaType<TestToken>(); + Q_ASSERT(testRoom && parent); + } + +signals: + void finishedItem(QByteArray /*name*/, bool /*condition*/); + +public slots: + void doTest(const QByteArray& testName); + +private slots: + TEST_DECL(findRoomByAlias) + TEST_DECL(loadMembers) + TEST_DECL(sendMessage) + TEST_DECL(sendReaction) + TEST_DECL(sendFile) + TEST_DECL(sendCustomEvent) + TEST_DECL(setTopic) + TEST_DECL(changeName) + TEST_DECL(sendAndRedact) + TEST_DECL(showLocalUsername) + TEST_DECL(addAndRemoveTag) + TEST_DECL(markDirectChat) + TEST_DECL(visitResources) + TEST_DECL(prettyPrintTests) + // Add more tests above here + +public: + [[nodiscard]] Room* room() const { return targetRoom; } + [[nodiscard]] Connection* connection() const + { + return targetRoom->connection(); + } + +private: + [[nodiscard]] bool checkFileSendingOutcome(const TestToken& thisTest, + const QString& txnId, + const QString& fileName); + [[nodiscard]] bool checkRedactionOutcome(const QByteArray& thisTest, + const QString& evtIdToRedact); + + template <EventClass<RoomEvent> EventT> + [[nodiscard]] bool validatePendingEvent(const QString& txnId); + [[nodiscard]] bool checkDirectChat() const; + void finishTest(const TestToken& token, bool condition, const char* file, + int line); + +private: + Room* targetRoom; + QString origin; +}; + +#define TEST_IMPL(Name) bool TestSuite::Name(const TestToken& thisTest) + +// Returning true (rather than a void) allows to reuse the convention with +// connectUntil() to break the QMetaObject::Connection upon finishing the test +// item. +#define FINISH_TEST(Condition) \ + return (finishTest(thisTest, (Condition), __FILE__, __LINE__), true) + +#define FAIL_TEST() FINISH_TEST(false) + +void TestSuite::doTest(const QByteArray& testName) +{ + clog << "Starting: " << testName.constData() << endl; + QMetaObject::invokeMethod(this, testName, Qt::DirectConnection, + Q_ARG(TestToken, testName)); +} + +template <EventClass<RoomEvent> EventT> +bool TestSuite::validatePendingEvent(const QString& txnId) +{ + auto it = targetRoom->findPendingEvent(txnId); + return it != targetRoom->pendingEvents().end() + && it->deliveryStatus() == EventStatus::Submitted + && (*it)->transactionId() == txnId && is<EventT>(**it) + && (*it)->matrixType() == EventT::TypeId; +} + +void TestSuite::finishTest(const TestToken& token, bool condition, + const char* file, int line) +{ + const auto& item = testName(token); + if (condition) { + clog << item << " successful" << endl; + if (targetRoom) + targetRoom->postMessage(origin % ": " % item % " successful", + MessageEventType::Notice); + } else { + clog << item << " FAILED at " << file << ":" << line << endl; + if (targetRoom) + targetRoom->postPlainText(origin % ": " % item % " FAILED at " + % file % ", line " % QString::number(line)); + } + + emit finishedItem(item, condition); +} + +TestManager::TestManager(int& argc, char** argv) + : QCoreApplication(argc, argv), c(new Connection(this)) +{ + Q_ASSERT(argc >= 5); + clog << "Connecting to Matrix as " << argv[1] << endl; + c->loginWithPassword(argv[1], argv[2], argv[3]); + targetRoomName = argv[4]; + clog << "Test room name: " << argv[4] << endl; + if (argc > 5) { + origin = argv[5]; + clog << "Origin for the test message: " << origin.toStdString() << endl; + } + + connect(c, &Connection::connected, this, &TestManager::setupAndRun); + connect(c, &Connection::resolveError, this, + [](const QString& error) { + clog << "Failed to resolve the server: " << error.toStdString() + << endl; + QCoreApplication::exit(-2); + }, + Qt::QueuedConnection); + connect(c, &Connection::loginError, this, + [this](const QString& message, const QString& details) { + clog << "Failed to login to " + << c->homeserver().toDisplayString().toStdString() << ": " + << message.toStdString() << endl + << "Details:" << endl + << details.toStdString() << endl; + QCoreApplication::exit(-2); + }, + Qt::QueuedConnection); + connect(c, &Connection::loadedRoomState, this, &TestManager::onNewRoom); + + // Big countdown watchdog + QTimer::singleShot(180000, this, [this] { + clog << "Time is up, stopping the session\n"; + if (testSuite) + conclude(); + else + finalize(); + }); +} + +void TestManager::setupAndRun() +{ + Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); + Q_ASSERT(c->domain() == c->userId().section(':', 1)); + clog << "Connected, server: " + << c->homeserver().toDisplayString().toStdString() << endl; + clog << "Access token: " << c->accessToken().toStdString() << endl; + + c->setLazyLoading(true); + + clog << "Joining " << targetRoomName.toStdString() << endl; + auto joinJob = c->joinRoom(targetRoomName); + // Ensure that the room has been joined and filled with some events + // so that other tests could use that + connect(joinJob, &BaseJob::success, this, [this, joinJob] { + testSuite = new TestSuite(c->room(joinJob->roomId()), origin, this); + // Only start the sync after joining, to make sure the room just + // joined is in it + c->syncLoop(); + connect(c, &Connection::syncDone, this, [this] { + static int i = 0; + clog << "Sync " << ++i << " complete" << endl; + if (auto* r = testSuite->room()) { + clog << "Test room timeline size = " << r->timelineSize(); + if (!r->pendingEvents().empty()) + clog << ", pending size = " << r->pendingEvents().size(); + clog << endl; + } + if (!running.empty()) { + clog << running.size() << " test(s) in the air:"; + for (const auto& test: qAsConst(running)) + clog << " " << testName(test); + clog << endl; + } + if (i == 1) { + testSuite->room()->getPreviousContent(); + connectSingleShot(testSuite->room(), &Room::addedMessages, this, + &TestManager::doTests); + } + }); + }); + connect(joinJob, &BaseJob::failure, this, [this] { + clog << "Failed to join the test room" << endl; + finalize(); + }); +} + +void TestManager::onNewRoom(Room* r) +{ + clog << "New room: " << r->id().toStdString() << endl + << " Name: " << r->name().toStdString() << endl + << " Canonical alias: " << r->canonicalAlias().toStdString() << endl + << endl; + connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { + clog << timeline.size() << " new event(s) in room " + << r->objectName().toStdString() << endl; + }); +} + +void TestManager::doTests() +{ + const auto* metaObj = testSuite->metaObject(); + for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); ++i) { + const auto metaMethod = metaObj->method(i); + if (metaMethod.access() != QMetaMethod::Private + || metaMethod.methodType() != QMetaMethod::Slot) + continue; + + const auto testName = metaMethod.name(); + running.push_back(testName); + // Some tests return the result immediately but we queue everything + // and process all tests asynchronously. + QMetaObject::invokeMethod(testSuite, "doTest", Qt::QueuedConnection, + Q_ARG(QByteArray, testName)); + } + clog << "Tests to do:"; + for (const auto& test: qAsConst(running)) + clog << " " << testName(test); + clog << endl; + connect(testSuite, &TestSuite::finishedItem, this, + [this](const QByteArray& itemName, bool condition) { + if (auto i = running.indexOf(itemName); i != -1) + (condition ? succeeded : failed).push_back(running.takeAt(i)); + else + Q_ASSERT_X(false, itemName, + "Test item is not in running state"); + if (running.empty()) { + clog << "All tests finished" << endl; + conclude(); + } + }); +} + +TEST_IMPL(findRoomByAlias) +{ + auto* roomByAlias = connection()->roomByAlias(targetRoom->canonicalAlias(), + JoinState::Join); + FINISH_TEST(roomByAlias == targetRoom); +} + +TEST_IMPL(loadMembers) +{ + // It's not exactly correct because an arbitrary server might not support + // lazy loading; but in the absence of capabilities framework we assume + // it does. + if (targetRoom->users().size() >= targetRoom->joinedCount()) { + clog << "Lazy loading doesn't seem to be enabled" << endl; + FAIL_TEST(); + } + targetRoom->setDisplayed(); + connect(targetRoom, &Room::allMembersLoaded, this, [this, thisTest] { + FINISH_TEST(targetRoom->users().size() >= targetRoom->joinedCount()); + }); + return false; +} + +TEST_IMPL(sendMessage) +{ + auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); + if (!validatePendingEvent<RoomMessageEvent>(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, txnId](const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + FINISH_TEST(is<RoomMessageEvent>(*evt) && !evt->id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); + }); + return false; +} + +TEST_IMPL(sendReaction) +{ + clog << "Reacting to the newest message in the room" << endl; + Q_ASSERT(targetRoom->timelineSize() > 0); + const auto targetEvtId = targetRoom->messageEvents().back()->id(); + const auto key = QStringLiteral("+1"); + const auto txnId = targetRoom->postReaction(targetEvtId, key); + if (!validatePendingEvent<ReactionEvent>(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + + connectUntil(targetRoom, &Room::updatedEvent, this, + [this, thisTest, txnId, key, targetEvtId](const QString& actualTargetEvtId) { + if (actualTargetEvtId != targetEvtId) + return false; + const auto reactions = targetRoom->relatedEvents( + targetEvtId, EventRelation::AnnotationType); + // It's a test room, assuming no interference there should + // be exactly one reaction + if (reactions.size() != 1) + FAIL_TEST(); + + const auto* evt = + eventCast<const ReactionEvent>(reactions.back()); + FINISH_TEST(is<ReactionEvent>(*evt) && !evt->id().isEmpty() + && evt->relation().key == key + && evt->transactionId() == txnId); + // TODO: Test removing the reaction + }); + return false; +} + +TEST_IMPL(sendFile) +{ + auto* tf = new QTemporaryFile; + if (!tf->open()) { + clog << "Failed to create a temporary file" << endl; + FAIL_TEST(); + } + tf->write("Test"); + tf->close(); + QFileInfo tfi { *tf }; + // QFileInfo::fileName brings only the file name; QFile::fileName brings + // the full path + const auto tfName = tfi.fileName(); + clog << "Sending file " << tfName.toStdString() << endl; + const auto txnId = targetRoom->postFile( + "Test file", new EventContent::FileContent(tfi)); + if (!validatePendingEvent<RoomMessageEvent>(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + tf->deleteLater(); + FAIL_TEST(); + } + + // Using tf as a context object to clean away both connections + // once either of them triggers. + connectUntil(targetRoom, &Room::fileTransferCompleted, tf, + [this, thisTest, txnId, tf, tfName](const QString& id) { + auto fti = targetRoom->fileTransferInfo(id); + Q_ASSERT(fti.status == FileTransferInfo::Completed); + + if (id != txnId) + return false; + + tf->deleteLater(); + return checkFileSendingOutcome(thisTest, txnId, tfName); + }); + connectUntil(targetRoom, &Room::fileTransferFailed, tf, + [this, thisTest, txnId, tf](const QString& id, const QString& error) { + if (id != txnId) + return false; + + targetRoom->postPlainText(origin % ": File upload failed: " % error); + tf->deleteLater(); + FAIL_TEST(); + }); + 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) +{ + auto it = targetRoom->findPendingEvent(txnId); + if (it == targetRoom->pendingEvents().end()) { + clog << "Pending file event dropped before upload completion" << endl; + FAIL_TEST(); + } + if (it->deliveryStatus() != EventStatus::FileUploaded) { + clog << "Pending file event status upon upload completion is " + << it->deliveryStatus() << " != FileUploaded(" + << EventStatus::FileUploaded << ')' << endl; + FAIL_TEST(); + } + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, txnId, fileName](const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + clog << "File event " << txnId.toStdString() + << " arrived in the timeline" << endl; + // This part tests switchOnType() + return switchOnType( + *evt, + [&](const RoomMessageEvent& e) { + // 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 + && testDownload(targetRoom->connection()->makeMediaUrl( + e.content()->fileInfo()->url()))); + }, + [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); + }); + return true; +} + +DEFINE_SIMPLE_EVENT(CustomEvent, RoomEvent, "quotest.custom", int, testValue, + "test_value") + +TEST_IMPL(sendCustomEvent) +{ + auto txnId = targetRoom->postEvent(new CustomEvent(42)); + if (!validatePendingEvent<CustomEvent>(txnId)) { + clog << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + connectUntil( + targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, txnId](const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + return switchOnType(*evt, + [this, thisTest, &evt](const CustomEvent& e) { + FINISH_TEST(!evt->id().isEmpty() && e.testValue() == 42); + }, + [this, thisTest] (const RoomEvent&) { FAIL_TEST(); }); + }); + return false; + +} + +TEST_IMPL(setTopic) +{ + const auto newTopic = connection()->generateTxnId(); // Just a way to make + // a unique id + targetRoom->setTopic(newTopic); + connectUntil(targetRoom, &Room::topicChanged, this, + [this, thisTest, newTopic] { + if (targetRoom->topic() == newTopic) + FINISH_TEST(true); + + clog << "Requested topic was " << newTopic.toStdString() << ", " + << targetRoom->topic().toStdString() << " arrived instead" + << endl; + return false; + }); + return false; +} + +TEST_IMPL(changeName) +{ + connectSingleShot(targetRoom, &Room::allMembersLoaded, this, [this, thisTest] { + auto* const localUser = connection()->user(); + const auto& newName = connection()->generateTxnId(); // See setTopic() + clog << "Renaming the user to " << newName.toStdString() + << " in the target room" << endl; + localUser->rename(newName, targetRoom); + connectUntil( + targetRoom, &Room::aboutToAddNewMessages, this, + [this, thisTest, localUser, newName](const RoomEventsRange& evts) { + for (const auto& e : evts) { + if (const auto* rme = eventCast<const RoomMemberEvent>(e)) { + if (rme->stateKey() != localUser->id() + || !rme->isRename()) + continue; + if (!rme->newDisplayName() + || *rme->newDisplayName() != newName) + FAIL_TEST(); + // State events coming in the timeline are first + // processed to change the room state and then as + // timeline messages; aboutToAddNewMessages is triggered + // when the state is already updated, so check that + if (targetRoom->currentState().get<RoomMemberEvent>( + localUser->id()) + != rme) + FAIL_TEST(); + clog << "Member rename successful, renaming the account" + << endl; + const auto newN = newName.mid(0, 5); + localUser->rename(newN); + connectUntil(localUser, &User::defaultNameChanged, this, + [this, thisTest, localUser, newN] { + targetRoom->localUser()->rename({}); + FINISH_TEST(localUser->name() == newN); + }); + return true; + } + } + return false; + }); + }); + return false; +} + +TEST_IMPL(showLocalUsername) +{ + auto* const localUser = connection()->user(); + FINISH_TEST(!localUser->name().contains("@")); +} + +TEST_IMPL(sendAndRedact) +{ + clog << "Sending a message to redact" << endl; + auto txnId = targetRoom->postPlainText(origin % ": message to redact"); + if (txnId.isEmpty()) + FAIL_TEST(); + + connectUntil(targetRoom, &Room::messageSent, this, + [this, thisTest, txnId](const QString& tId, const QString& evtId) { + if (tId != txnId) + return false; + + // The event may end up having been merged, and that's ok; + // but if it's not, it has to be in the ReachedServer state. + if (auto it = room()->findPendingEvent(tId); + it != room()->pendingEvents().cend() + && it->deliveryStatus() != EventStatus::ReachedServer) { + clog << "Incorrect sent event status (" + << it->deliveryStatus() << ')' << endl; + FAIL_TEST(); + } + + clog << "Redacting the message" << endl; + targetRoom->redactEvent(evtId, origin); + connectUntil(targetRoom, &Room::addedMessages, this, + [this, thisTest, evtId] { + return checkRedactionOutcome(thisTest, evtId); + }); + return false; + }); + return false; +} + +bool TestSuite::checkRedactionOutcome(const QByteArray& thisTest, + const QString& evtIdToRedact) +{ + // There are two possible (correct) outcomes: either the event comes already + // redacted at the next sync, or the nearest sync completes with + // the unredacted event but the next one brings redaction. + auto it = targetRoom->findInTimeline(evtIdToRedact); + if (it == targetRoom->historyEdge()) + return false; // Waiting for the next sync + + if ((*it)->isRedacted()) { + clog << "The sync brought already redacted message" << endl; + FINISH_TEST(true); + } + + clog << "Message came non-redacted with the sync, waiting for redaction" + << endl; + connectUntil(targetRoom, &Room::replacedEvent, this, + [this, thisTest, evtIdToRedact](const RoomEvent* newEvent, + const RoomEvent* oldEvent) { + if (oldEvent->id() != evtIdToRedact) + return false; + + FINISH_TEST(newEvent->isRedacted() + && newEvent->redactionReason() == origin); + }); + return true; +} + +TEST_IMPL(addAndRemoveTag) +{ + static const auto TestTag = QStringLiteral("im.quotient.test"); + // Pre-requisite + if (targetRoom->tags().contains(TestTag)) + targetRoom->removeTag(TestTag); + + // Unlike for most of Quotient, tags are applied and tagsChanged is emitted + // synchronously, with the server being notified async. The test checks + // that the signal is emitted, not only that tags have changed; but there's + // (currently) no way to check that the server has been correctly notified + // of the tag change. + QSignalSpy spy(targetRoom, &Room::tagsChanged); + targetRoom->addTag(TestTag); + if (spy.count() != 1 || !targetRoom->tags().contains(TestTag)) { + clog << "Tag adding failed" << endl; + FAIL_TEST(); + } + clog << "Test tag set, removing it now" << endl; + targetRoom->removeTag(TestTag); + FINISH_TEST(spy.count() == 2 && !targetRoom->tags().contains(TestTag)); +} + +bool TestSuite::checkDirectChat() const +{ + return targetRoom->directChatUsers().contains(connection()->user()); +} + +TEST_IMPL(markDirectChat) +{ + if (checkDirectChat()) + connection()->removeFromDirectChats(targetRoom->id(), + connection()->user()); + + int id = qRegisterMetaType<DirectChatsMap>(); // For QSignalSpy + Q_ASSERT(id != -1); + + // Same as with tags (and unusual for the rest of Quotient), direct chat + // operations are synchronous. + QSignalSpy spy(connection(), &Connection::directChatsListChanged); + clog << "Marking the room as a direct chat" << endl; + connection()->addToDirectChats(targetRoom, connection()->user()); + if (spy.count() != 1 || !checkDirectChat()) + FAIL_TEST(); + + // Check that the first argument (added DCs) actually contains the room + const auto& addedDCs = spy.back().front().value<DirectChatsMap>(); + if (addedDCs.size() != 1 + || !addedDCs.contains(connection()->user(), targetRoom->id())) { + clog << "The room is not in added direct chats" << endl; + FAIL_TEST(); + } + + clog << "Unmarking the direct chat" << endl; + connection()->removeFromDirectChats(targetRoom->id(), connection()->user()); + if (spy.count() != 2 && checkDirectChat()) + FAIL_TEST(); + + // Check that the second argument (removed DCs) actually contains the room + const auto& removedDCs = spy.back().back().value<DirectChatsMap>(); + FINISH_TEST(removedDCs.size() == 1 + && removedDCs.contains(connection()->user(), targetRoom->id())); +} + +TEST_IMPL(visitResources) +{ + // Same as the two tests above, ResourceResolver emits signals + // synchronously so we use signal spies to intercept them instead of + // connecting lambdas before calling openResource(). NB: this test + // assumes that ResourceResolver::openResource is implemented in terms + // of ResourceResolver::visitResource, so the latter doesn't need a + // separate test. + static UriDispatcher ud; + + // This lambda returns true in case of error, false if it's fine so far + auto testResourceResolver = [this, thisTest](const QStringList& uris, + auto signal, auto* target, + QVariantList otherArgs = {}) { + int r = qRegisterMetaType<decltype(target)>(); + Q_ASSERT(r != 0); + QSignalSpy spy(&ud, signal); + for (const auto& uriString: uris) { + Uri uri { uriString }; + clog << "Checking " << uriString.toStdString() + << " -> " << uri.toDisplayString().toStdString() << endl; + if (auto matrixToUrl = uri.toUrl(Uri::MatrixToUri).toDisplayString(); + !matrixToUrl.startsWith("https://matrix.to/#/")) { + clog << "Incorrect matrix.to representation:" + << matrixToUrl.toStdString() << endl; + } + const auto checkResult = checkResource(connection(), uriString); + if ((checkResult != UriResolved && uri.type() != Uri::NonMatrix) + || (uri.type() == Uri::NonMatrix + && checkResult != CouldNotResolve)) { + clog << "checkResource() returned incorrect result:" + << checkResult; + FAIL_TEST(); + } + ud.visitResource(connection(), uriString); + if (spy.count() != 1) { + clog << "Wrong number of signal emissions (" << spy.count() + << ')' << endl; + FAIL_TEST(); + } + const auto& emission = spy.front(); + Q_ASSERT(emission.count() >= 2); + if (emission.front().value<decltype(target)>() != target) { + clog << "Signal emitted with an incorrect target" << endl; + FAIL_TEST(); + } + if (!otherArgs.empty()) { + if (emission.size() < otherArgs.size() + 1) { + clog << "Emission doesn't include all arguments" << endl; + FAIL_TEST(); + } + for (auto i = 0; i < otherArgs.size(); ++i) + if (otherArgs[i] != emission[i + 1]) { + clog << "Mismatch in argument #" << i + 1 << endl; + FAIL_TEST(); + } + } + spy.clear(); + } + return false; + }; + + // Basic tests + for (const auto& u: { Uri {}, Uri { QUrl {} } }) + if (u.isValid() || !u.isEmpty()) { + clog << "Empty Matrix URI test failed" << endl; + FAIL_TEST(); + } + if (Uri { QStringLiteral("#") }.isValid()) { + clog << "Bare sigil URI test failed" << endl; + FAIL_TEST(); + } + QUrl invalidUrl { "https://" }; + invalidUrl.setAuthority("---:@@@"); + const Uri matrixUriFromInvalidUrl { invalidUrl }, + invalidMatrixUri { QStringLiteral("matrix:&invalid@") }; + if (matrixUriFromInvalidUrl.isEmpty() || matrixUriFromInvalidUrl.isValid()) { + clog << "Invalid Matrix URI test failed" << endl; + FAIL_TEST(); + } + if (invalidMatrixUri.isEmpty() || invalidMatrixUri.isValid()) { + clog << "Invalid sigil in a Matrix URI - test failed" << endl; + FAIL_TEST(); + } + + // Matrix identifiers used throughout all URI tests + const auto& roomId = room()->id(); + const auto& roomAlias = room()->canonicalAlias(); + const auto& userId = connection()->userId(); + const auto& eventId = room()->messageEvents().back()->id(); + Q_ASSERT(!roomId.isEmpty()); + Q_ASSERT(!roomAlias.isEmpty()); + Q_ASSERT(!userId.isEmpty()); + Q_ASSERT(!eventId.isEmpty()); + + const QStringList roomUris { + roomId, "matrix:roomid/" + roomId.mid(1), + "https://matrix.to/#/%21"/*`!`*/ + roomId.mid(1), + roomAlias, "matrix:room/" + roomAlias.mid(1), + "matrix:r/" + roomAlias.mid(1), + "https://matrix.to/#/" + roomAlias, + }; + const QStringList userUris { userId, "matrix:user/" + userId.mid(1), + "matrix:u/" + userId.mid(1), + "https://matrix.to/#/" + userId }; + const QStringList eventUris { + "matrix:room/" + roomAlias.mid(1) + "/event/" + eventId.mid(1), + "matrix:r/" + roomAlias.mid(1) + "/e/" + eventId.mid(1), + "https://matrix.to/#/" + roomId + '/' + eventId + }; + // Check that reserved characters are correctly processed. + static const auto& joinRoomAlias = + QStringLiteral("##/?.@\"unjoined:example.org"); + static const auto& encodedRoomAliasNoSigil = + QUrl::toPercentEncoding(joinRoomAlias.mid(1), ":"); + static const QString joinQuery { "?action=join" }; + // These URIs are not supposed to be actually joined (and even exist, + // as yet) - only to be syntactically correct + static const QStringList joinByAliasUris { + Uri(joinRoomAlias.toUtf8(), {}, joinQuery.mid(1)).toDisplayString(), + "matrix:room/" + encodedRoomAliasNoSigil + joinQuery, + "matrix:r/" + encodedRoomAliasNoSigil + joinQuery, + "https://matrix.to/#/%23"/*`#`*/ + encodedRoomAliasNoSigil + joinQuery, + "https://matrix.to/#/%23" + joinRoomAlias.mid(1) /* unencoded */ + joinQuery + }; + static const auto& joinRoomId = QStringLiteral("!anyid:example.org"); + static const QStringList viaServers { "matrix.org", "example.org" }; + static const auto viaQuery = + std::accumulate(viaServers.cbegin(), viaServers.cend(), joinQuery, + [](const QString& q, const QString& s) { + return q + "&via=" + s; + }); + static const QStringList joinByIdUris { + "matrix:roomid/" + joinRoomId.mid(1) + viaQuery, + "https://matrix.to/#/" + joinRoomId + viaQuery + }; + // If any test breaks, the breaking call will return true, and further + // execution will be cut by ||'s short-circuiting + if (testResourceResolver(roomUris, &UriDispatcher::roomAction, room()) + || testResourceResolver(userUris, &UriDispatcher::userAction, + connection()->user()) + || testResourceResolver(eventUris, &UriDispatcher::roomAction, + room(), { eventId }) + || testResourceResolver(joinByAliasUris, &UriDispatcher::joinAction, + connection(), { joinRoomAlias }) + || testResourceResolver(joinByIdUris, &UriDispatcher::joinAction, + connection(), { joinRoomId, viaServers })) + return true; + // TODO: negative cases + 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) + auto* room = testSuite->room(); + room->setTopic({}); + room->localUser()->rename({}); + + QString succeededRec { QString::number(succeeded.size()) % " of " + % QString::number(succeeded.size() + failed.size() + + running.size()) + % " tests succeeded" }; + QString plainReport = origin % ": Testing complete, " % succeededRec; + QString color = failed.empty() && running.empty() ? "00AA00" : "AA0000"; + QString htmlReport = origin % ": <strong><font data-mx-color='#" % color + % "' color='#" % color + % "'>Testing complete</font></strong>, " % succeededRec; + if (!failed.empty()) { + QByteArray failedList; + for (const auto& f : qAsConst(failed)) + failedList += ' ' + f; + plainReport += "\nFAILED:" + failedList; + htmlReport += "<br><strong>Failed:</strong>" + failedList; + } + if (!running.empty()) { + QByteArray dnfList; + for (const auto& r : qAsConst(running)) + dnfList += ' ' + r; + plainReport += "\nDID NOT FINISH:" + dnfList; + htmlReport += "<br><strong>Did not finish:</strong>" + dnfList; + } + + auto txnId = room->postHtmlText(plainReport, htmlReport); + // Now just wait until all the pending events reach the server + connectUntil(room, &Room::messageSent, this, + [this, txnId, room, plainReport] (const QString& sentTxnId) { + if (sentTxnId != txnId) + return false; + const auto& pendingEvents = room->pendingEvents(); + if (auto c = std::count_if(pendingEvents.cbegin(), + pendingEvents.cend(), + [](const PendingEventItem& pe) { + return pe.deliveryStatus() + < EventStatus::ReachedServer; + }); + c > 0) { + clog << "Events to reach the server: " << c + << ", not leaving yet" << endl; + return false; + } + + clog << "Leaving the room" << endl; + // TODO: Waiting for proper futures to come so that it could be: +// room->leaveRoom() +// .then(this, &TestManager::finalize); // Qt-style or +// .then([this] { finalize(); }); // STL-style + auto* job = room->leaveRoom(); + connect(job, &BaseJob::result, this, [this, job,plainReport] { + Q_ASSERT(job->status().good()); + finalize(); + // Still flying, as the exit() connection in finalize() is queued + clog << plainReport.toStdString() << endl; + }); + return true; + }); +} + +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(!testSuite ? -3 + : succeeded.empty() && failed.empty() + && running.empty() + ? -4 + : failed.size() + running.size()); + }, + Qt::QueuedConnection); +} + +int main(int argc, char* argv[]) +{ + // TODO: use QCommandLineParser + if (argc < 5) { + clog << "Usage: quotest <user> <passwd> <device_name> <room_alias> [origin]" + << endl; + return -1; + } + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + return TestManager(argc, argv).exec(); +} + +#include "quotest.moc" diff --git a/res.qrc b/res.qrc new file mode 100644 index 00000000..f6769103 --- /dev/null +++ b/res.qrc @@ -0,0 +1,5 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource prefix="/"> + <file>sas-emoji.json</file> +</qresource> +</RCC> diff --git a/sas-emoji.json b/sas-emoji.json new file mode 100644 index 00000000..06e1e4b3 --- /dev/null +++ b/sas-emoji.json @@ -0,0 +1,2178 @@ +[ + { + "number": 0, + "emoji": "🐶", + "description": "Dog", + "unicode": "U+1F436", + "translated_descriptions": { + "ar": "كَلب", + "bg": "Куче", + "ca": "Gos", + "cs": "Pes", + "de": "Hund", + "eo": "Hundo", + "es": "Perro", + "et": "Koer", + "fi": "Koira", + "fr": "Chien", + "hr": "pas", + "hu": "Kutya", + "it": "Cane", + "ja": "犬", + "nb_NO": "Hund", + "nl": "Hond", + "pt_BR": "Cachorro", + "ru": "Собака", + "si": "බල්ලා", + "sk": "Hlava psa", + "sr": "пас", + "sv": "Hund", + "szl": null, + "tzm": "Aydi", + "uk": "Пес", + "zh_Hans": "狗" + } + }, + { + "number": 1, + "emoji": "🐱", + "description": "Cat", + "unicode": "U+1F431", + "translated_descriptions": { + "ar": "هِرَّة", + "bg": "Котка", + "ca": "Gat", + "cs": "Kočka", + "de": "Katze", + "eo": "Kato", + "es": "Gato", + "et": "Kass", + "fi": "Kissa", + "fr": "Chat", + "hr": "mačka", + "hu": "Macska", + "it": "Gatto", + "ja": "猫", + "nb_NO": "Katt", + "nl": "Kat", + "pt_BR": "Gato", + "ru": "Кошка", + "si": "පූසා", + "sk": "Hlava mačky", + "sr": "мачка", + "sv": "Katt", + "szl": null, + "tzm": "Amuc", + "uk": "Кіт", + "zh_Hans": "猫" + } + }, + { + "number": 2, + "emoji": "🦁", + "description": "Lion", + "unicode": "U+1F981", + "translated_descriptions": { + "ar": "أَسَد", + "bg": "Лъв", + "ca": "Lleó", + "cs": "Lev", + "de": "Löwe", + "eo": "Leono", + "es": "León", + "et": "Lõvi", + "fi": "Leijona", + "fr": "Lion", + "hr": "lav", + "hu": "Oroszlán", + "it": "Leone", + "ja": "ライオン", + "nb_NO": "Løve", + "nl": "Leeuw", + "pt_BR": "Leão", + "ru": "Лев", + "si": "සිංහයා", + "sk": "Hlava leva", + "sr": "лав", + "sv": "Lejon", + "szl": null, + "tzm": "Izem", + "uk": "Лев", + "zh_Hans": "狮子" + } + }, + { + "number": 3, + "emoji": "🐎", + "description": "Horse", + "unicode": "U+1F40E", + "translated_descriptions": { + "ar": "حِصَان", + "bg": "Кон", + "ca": "Cavall", + "cs": "Kůň", + "de": "Pferd", + "eo": "Ĉevalo", + "es": "Caballo", + "et": "Hobune", + "fi": "Hevonen", + "fr": "Cheval", + "hr": "konj", + "hu": "Ló", + "it": "Cavallo", + "ja": "馬", + "nb_NO": "Hest", + "nl": "Paard", + "pt_BR": "Cavalo", + "ru": "Лошадь", + "si": "අශ්වයා", + "sk": "Kôň", + "sr": "коњ", + "sv": "Häst", + "szl": null, + "tzm": "Ayyis", + "uk": "Кінь", + "zh_Hans": "马" + } + }, + { + "number": 4, + "emoji": "🦄", + "description": "Unicorn", + "unicode": "U+1F984", + "translated_descriptions": { + "ar": "حِصَانٌ بِقَرن", + "bg": "Еднорог", + "ca": "Unicorn", + "cs": "Jednorožec", + "de": "Einhorn", + "eo": "Unukorno", + "es": "Unicornio", + "et": "Ükssarvik", + "fi": "Yksisarvinen", + "fr": "Licorne", + "hr": "jednorog", + "hu": "Egyszarvú", + "it": "Unicorno", + "ja": "ユニコーン", + "nb_NO": "Enhjørning", + "nl": "Eenhoorn", + "pt_BR": "Unicórnio", + "ru": "Единорог", + "si": null, + "sk": "Hlava jednorožca", + "sr": "једнорог", + "sv": "Enhörning", + "szl": null, + "tzm": null, + "uk": "Єдиноріг", + "zh_Hans": "独角兽" + } + }, + { + "number": 5, + "emoji": "🐷", + "description": "Pig", + "unicode": "U+1F437", + "translated_descriptions": { + "ar": "خِنزِير", + "bg": "Прасе", + "ca": "Porc", + "cs": "Prase", + "de": "Schwein", + "eo": "Porko", + "es": "Cerdo", + "et": "Siga", + "fi": "Sika", + "fr": "Cochon", + "hr": "svinja", + "hu": "Malac", + "it": "Maiale", + "ja": "ブタ", + "nb_NO": "Gris", + "nl": "Varken", + "pt_BR": "Porco", + "ru": "Свинья", + "si": null, + "sk": "Hlava prasaťa", + "sr": "прасе", + "sv": "Gris", + "szl": null, + "tzm": "Ilef", + "uk": "Свиня", + "zh_Hans": "猪" + } + }, + { + "number": 6, + "emoji": "🐘", + "description": "Elephant", + "unicode": "U+1F418", + "translated_descriptions": { + "ar": "فِيل", + "bg": "Слон", + "ca": "Elefant", + "cs": "Slon", + "de": "Elefant", + "eo": "Elefanto", + "es": "Elefante", + "et": "Elevant", + "fi": "Norsu", + "fr": "Éléphant", + "hr": "slon", + "hu": "Elefánt", + "it": "Elefante", + "ja": "ゾウ", + "nb_NO": "Elefant", + "nl": "Olifant", + "pt_BR": "Elefante", + "ru": "Слон", + "si": null, + "sk": "Slon", + "sr": "слон", + "sv": "Elefant", + "szl": null, + "tzm": "Ilu", + "uk": "Слон", + "zh_Hans": "大象" + } + }, + { + "number": 7, + "emoji": "🐰", + "description": "Rabbit", + "unicode": "U+1F430", + "translated_descriptions": { + "ar": "أَرنَب", + "bg": "Заек", + "ca": "Conill", + "cs": "Králík", + "de": "Hase", + "eo": "Kuniklo", + "es": "Conejo", + "et": "Jänes", + "fi": "Kani", + "fr": "Lapin", + "hr": "zec", + "hu": "Nyúl", + "it": "Coniglio", + "ja": "うさぎ", + "nb_NO": "Kanin", + "nl": "Konijn", + "pt_BR": "Coelho", + "ru": "Кролик", + "si": null, + "sk": "Hlava zajaca", + "sr": "зец", + "sv": "Kanin", + "szl": null, + "tzm": "Agnin", + "uk": "Кріль", + "zh_Hans": "兔子" + } + }, + { + "number": 8, + "emoji": "🐼", + "description": "Panda", + "unicode": "U+1F43C", + "translated_descriptions": { + "ar": "باندَا", + "bg": "Панда", + "ca": "Panda", + "cs": "Panda", + "de": "Panda", + "eo": "Pando", + "es": "Panda", + "et": "Panda", + "fi": "Panda", + "fr": "Panda", + "hr": "panda", + "hu": "Panda", + "it": "Panda", + "ja": "パンダ", + "nb_NO": "Panda", + "nl": "Panda", + "pt_BR": "Panda", + "ru": "Панда", + "si": null, + "sk": "Hlava pandy", + "sr": "панда", + "sv": "Panda", + "szl": null, + "tzm": null, + "uk": "Панда", + "zh_Hans": "熊猫" + } + }, + { + "number": 9, + "emoji": "🐓", + "description": "Rooster", + "unicode": "U+1F413", + "translated_descriptions": { + "ar": "دِيك", + "bg": "Петел", + "ca": "Gall", + "cs": "Kohout", + "de": "Hahn", + "eo": "Virkoko", + "es": "Gallo", + "et": "Kukk", + "fi": "Kukko", + "fr": "Coq", + "hr": "kokot", + "hu": "Kakas", + "it": "Gallo", + "ja": "ニワトリ", + "nb_NO": "Hane", + "nl": "Haan", + "pt_BR": "Galo", + "ru": "Петух", + "si": null, + "sk": "Kohút", + "sr": "петао", + "sv": "Tupp", + "szl": null, + "tzm": "Ayaẓiḍ", + "uk": "Когут", + "zh_Hans": "公鸡" + } + }, + { + "number": 10, + "emoji": "🐧", + "description": "Penguin", + "unicode": "U+1F427", + "translated_descriptions": { + "ar": "بِطريق", + "bg": "Пингвин", + "ca": "Pingüí", + "cs": "Tučňák", + "de": "Pinguin", + "eo": "Pingveno", + "es": "Pingüino", + "et": "Pingviin", + "fi": "Pingviini", + "fr": "Manchot", + "hr": "pingvin", + "hu": "Pingvin", + "it": "Pinguino", + "ja": "ペンギン", + "nb_NO": "Pingvin", + "nl": "Pinguïn", + "pt_BR": "Pinguim", + "ru": "Пингвин", + "si": null, + "sk": "Tučniak", + "sr": "пингвин", + "sv": "Pingvin", + "szl": null, + "tzm": null, + "uk": "Пінгвін", + "zh_Hans": "企鹅" + } + }, + { + "number": 11, + "emoji": "🐢", + "description": "Turtle", + "unicode": "U+1F422", + "translated_descriptions": { + "ar": "سُلحفاة", + "bg": "Костенурка", + "ca": "Tortuga", + "cs": "Želva", + "de": "Schildkröte", + "eo": "Testudo", + "es": "Tortuga", + "et": "Kilpkonn", + "fi": "Kilpikonna", + "fr": "Tortue", + "hr": "kornjača", + "hu": "Teknős", + "it": "Tartaruga", + "ja": "亀", + "nb_NO": "Skilpadde", + "nl": "Schildpad", + "pt_BR": "Tartaruga", + "ru": "Черепаха", + "si": null, + "sk": "Korytnačka", + "sr": "корњача", + "sv": "Sköldpadda", + "szl": null, + "tzm": "Ifker", + "uk": "Черепаха", + "zh_Hans": "乌龟" + } + }, + { + "number": 12, + "emoji": "🐟", + "description": "Fish", + "unicode": "U+1F41F", + "translated_descriptions": { + "ar": "سَمَكَة", + "bg": "Риба", + "ca": "Peix", + "cs": "Ryba", + "de": "Fisch", + "eo": "Fiŝo", + "es": "Pez", + "et": "Kala", + "fi": "Kala", + "fr": "Poisson", + "hr": "riba", + "hu": "Hal", + "it": "Pesce", + "ja": "魚", + "nb_NO": "Fisk", + "nl": "Vis", + "pt_BR": "Peixe", + "ru": "Рыба", + "si": null, + "sk": "Ryba", + "sr": "риба", + "sv": "Fisk", + "szl": null, + "tzm": "Aselm", + "uk": "Риба", + "zh_Hans": "鱼" + } + }, + { + "number": 13, + "emoji": "🐙", + "description": "Octopus", + "unicode": "U+1F419", + "translated_descriptions": { + "ar": "أُخطُبُوط", + "bg": "Октопод", + "ca": "Pop", + "cs": "Chobotnice", + "de": "Oktopus", + "eo": "Polpo", + "es": "Pulpo", + "et": "Kaheksajalg", + "fi": "Tursas", + "fr": "Poulpe", + "hr": "hobotnica", + "hu": "Polip", + "it": "Polpo", + "ja": "たこ", + "nb_NO": "Blekksprut", + "nl": "Octopus", + "pt_BR": "Polvo", + "ru": "Осьминог", + "si": null, + "sk": "Chobotnica", + "sr": "октопод", + "sv": "Bläckfisk", + "szl": null, + "tzm": null, + "uk": "Восьминіг", + "zh_Hans": "章鱼" + } + }, + { + "number": 14, + "emoji": "🦋", + "description": "Butterfly", + "unicode": "U+1F98B", + "translated_descriptions": { + "ar": "فَرَاشَة", + "bg": "Пеперуда", + "ca": "Papallona", + "cs": "Motýl", + "de": "Schmetterling", + "eo": "Papilio", + "es": "Mariposa", + "et": "Liblikas", + "fi": "Perhonen", + "fr": "Papillon", + "hr": "leptir", + "hu": "Pillangó", + "it": "Farfalla", + "ja": "ちょうちょ", + "nb_NO": "Sommerfugl", + "nl": "Vlinder", + "pt_BR": "Borboleta", + "ru": "Бабочка", + "si": null, + "sk": "Motýľ", + "sr": "лептир", + "sv": "Fjäril", + "szl": null, + "tzm": null, + "uk": "Метелик", + "zh_Hans": "蝴蝶" + } + }, + { + "number": 15, + "emoji": "🌷", + "description": "Flower", + "unicode": "U+1F337", + "translated_descriptions": { + "ar": "زَهرَة", + "bg": "Цвете", + "ca": "Flor", + "cs": "Květina", + "de": "Blume", + "eo": "Floro", + "es": "Flor", + "et": "Lill", + "fi": "Kukka", + "fr": "Fleur", + "hr": "svijet", + "hu": "Virág", + "it": "Fiore", + "ja": "花", + "nb_NO": "Blomst", + "nl": "Bloem", + "pt_BR": "Flor", + "ru": "Цветок", + "si": null, + "sk": "Tulipán", + "sr": "цвет", + "sv": "Blomma", + "szl": null, + "tzm": null, + "uk": "Квітка", + "zh_Hans": "花" + } + }, + { + "number": 16, + "emoji": "🌳", + "description": "Tree", + "unicode": "U+1F333", + "translated_descriptions": { + "ar": "شَجَرَة", + "bg": "Дърво", + "ca": "Arbre", + "cs": "Strom", + "de": "Baum", + "eo": "Arbo", + "es": "Árbol", + "et": "Puu", + "fi": "Puu", + "fr": "Arbre", + "hr": "drvo", + "hu": "Fa", + "it": "Albero", + "ja": "木", + "nb_NO": "Tre", + "nl": "Boom", + "pt_BR": "Árvore", + "ru": "Дерево", + "si": null, + "sk": "Listnatý strom", + "sr": "дрво", + "sv": "Träd", + "szl": null, + "tzm": "Aseklu", + "uk": "Дерево", + "zh_Hans": "树" + } + }, + { + "number": 17, + "emoji": "🌵", + "description": "Cactus", + "unicode": "U+1F335", + "translated_descriptions": { + "ar": "صبار", + "bg": "Кактус", + "ca": "Cactus", + "cs": "Kaktus", + "de": "Kaktus", + "eo": "Kakto", + "es": "Cactus", + "et": "Kaktus", + "fi": "Kaktus", + "fr": "Cactus", + "hr": "kaktus", + "hu": "Kaktusz", + "it": "Cactus", + "ja": "サボテン", + "nb_NO": "Kaktus", + "nl": "Cactus", + "pt_BR": "Cacto", + "ru": "Кактус", + "si": null, + "sk": "Kaktus", + "sr": "кактус", + "sv": "Kaktus", + "szl": null, + "tzm": null, + "uk": "Кактус", + "zh_Hans": "仙人掌" + } + }, + { + "number": 18, + "emoji": "🍄", + "description": "Mushroom", + "unicode": "U+1F344", + "translated_descriptions": { + "ar": "فُطر", + "bg": "Гъба", + "ca": "Bolet", + "cs": "Houba", + "de": "Pilz", + "eo": "Fungo", + "es": "Seta", + "et": "Seen", + "fi": "Sieni", + "fr": "Champignon", + "hr": "gljiva", + "hu": "Gomba", + "it": "Fungo", + "ja": "きのこ", + "nb_NO": "Sopp", + "nl": "Paddenstoel", + "pt_BR": "Cogumelo", + "ru": "Гриб", + "si": null, + "sk": "Huba", + "sr": "печурка", + "sv": "Svamp", + "szl": null, + "tzm": "Agursel", + "uk": "Гриб", + "zh_Hans": "蘑菇" + } + }, + { + "number": 19, + "emoji": "🌏", + "description": "Globe", + "unicode": "U+1F30F", + "translated_descriptions": { + "ar": "كُرَةٌ أرضِيَّة", + "bg": "Глобус", + "ca": "Globus terraqüi", + "cs": "Zeměkoule", + "de": "Globus", + "eo": "Globo", + "es": "Globo", + "et": "Maakera", + "fi": "Maapallo", + "fr": "Globe", + "hr": "Globus", + "hu": "Földgömb", + "it": "Globo", + "ja": "地球", + "nb_NO": "Globus", + "nl": "Wereldbol", + "pt_BR": "Globo", + "ru": "Глобус", + "si": null, + "sk": "Zemeguľa", + "sr": "глобус", + "sv": "Jordklot", + "szl": null, + "tzm": null, + "uk": "Глобус", + "zh_Hans": "地球" + } + }, + { + "number": 20, + "emoji": "🌙", + "description": "Moon", + "unicode": "U+1F319", + "translated_descriptions": { + "ar": "قَمَر", + "bg": "Луна", + "ca": "Lluna", + "cs": "Měsíc", + "de": "Mond", + "eo": "Luno", + "es": "Luna", + "et": "Kuu", + "fi": "Kuu", + "fr": "Lune", + "hr": "mjesec", + "hu": "Hold", + "it": "Luna", + "ja": "月", + "nb_NO": "Måne", + "nl": "Maan", + "pt_BR": "Lua", + "ru": "Луна", + "si": null, + "sk": "Polmesiac", + "sr": "месец", + "sv": "Måne", + "szl": null, + "tzm": "Ayyur", + "uk": "Місяць", + "zh_Hans": "月亮" + } + }, + { + "number": 21, + "emoji": "☁️", + "description": "Cloud", + "unicode": "U+2601U+FE0F", + "translated_descriptions": { + "ar": "سَحابَة", + "bg": "Облак", + "ca": "Núvol", + "cs": "Mrak", + "de": "Wolke", + "eo": "Nubo", + "es": "Nube", + "et": "Pilv", + "fi": "Pilvi", + "fr": "Nuage", + "hr": "oblak", + "hu": "Felhő", + "it": "Nuvola", + "ja": "雲", + "nb_NO": "Sky", + "nl": "Wolk", + "pt_BR": "Nuvem", + "ru": "Облако", + "si": null, + "sk": "Oblak", + "sr": "облак", + "sv": "Moln", + "szl": null, + "tzm": null, + "uk": "Хмара", + "zh_Hans": "云" + } + }, + { + "number": 22, + "emoji": "🔥", + "description": "Fire", + "unicode": "U+1F525", + "translated_descriptions": { + "ar": "نار", + "bg": "Огън", + "ca": "Foc", + "cs": "Oheň", + "de": "Feuer", + "eo": "Fajro", + "es": "Fuego", + "et": "Tuli", + "fi": "Tuli", + "fr": "Feu", + "hr": "vatra", + "hu": "Tűz", + "it": "Fuoco", + "ja": "炎", + "nb_NO": "Flamme", + "nl": "Vuur", + "pt_BR": "Fogo", + "ru": "Огонь", + "si": null, + "sk": "Oheň", + "sr": "ватра", + "sv": "Eld", + "szl": null, + "tzm": "Timessi", + "uk": "Вогонь", + "zh_Hans": "火" + } + }, + { + "number": 23, + "emoji": "🍌", + "description": "Banana", + "unicode": "U+1F34C", + "translated_descriptions": { + "ar": "مَوزَة", + "bg": "Банан", + "ca": "Plàtan", + "cs": "Banán", + "de": "Banane", + "eo": "Banano", + "es": "Plátano", + "et": "Banaan", + "fi": "Banaani", + "fr": "Banane", + "hr": "banana", + "hu": "Banán", + "it": "Banana", + "ja": "バナナ", + "nb_NO": "Banan", + "nl": "Banaan", + "pt_BR": "Banana", + "ru": "Банан", + "si": null, + "sk": "Banán", + "sr": "банана", + "sv": "Banan", + "szl": null, + "tzm": "Tabanant", + "uk": "Банан", + "zh_Hans": "香蕉" + } + }, + { + "number": 24, + "emoji": "🍎", + "description": "Apple", + "unicode": "U+1F34E", + "translated_descriptions": { + "ar": "تُفَّاحَة", + "bg": "Ябълка", + "ca": "Poma", + "cs": "Jablko", + "de": "Apfel", + "eo": "Pomo", + "es": "Manzana", + "et": "Õun", + "fi": "Omena", + "fr": "Pomme", + "hr": "jabuka", + "hu": "Alma", + "it": "Mela", + "ja": "リンゴ", + "nb_NO": "Eple", + "nl": "Appel", + "pt_BR": "Maçã", + "ru": "Яблоко", + "si": null, + "sk": "Červené jablko", + "sr": "јабука", + "sv": "Äpple", + "szl": null, + "tzm": "Tadeffuyt", + "uk": "Яблуко", + "zh_Hans": "苹果" + } + }, + { + "number": 25, + "emoji": "🍓", + "description": "Strawberry", + "unicode": "U+1F353", + "translated_descriptions": { + "ar": "فَراوِلَة", + "bg": "Ягода", + "ca": "Maduixa", + "cs": "Jahoda", + "de": "Erdbeere", + "eo": "Frago", + "es": "Fresa", + "et": "Maasikas", + "fi": "Mansikka", + "fr": "Fraise", + "hr": "jagoda", + "hu": "Eper", + "it": "Fragola", + "ja": "いちご", + "nb_NO": "Jordbær", + "nl": "Aardbei", + "pt_BR": "Morango", + "ru": "Клубника", + "si": null, + "sk": "Jahoda", + "sr": "јагода", + "sv": "Jordgubbe", + "szl": null, + "tzm": null, + "uk": "Полуниця", + "zh_Hans": "草莓" + } + }, + { + "number": 26, + "emoji": "🌽", + "description": "Corn", + "unicode": "U+1F33D", + "translated_descriptions": { + "ar": "ذُرَة", + "bg": "Царевица", + "ca": "Blat de moro", + "cs": "Kukuřice", + "de": "Mais", + "eo": "Maizo", + "es": "Maíz", + "et": "Mais", + "fi": "Maissi", + "fr": "Maïs", + "hr": "kukuruza", + "hu": "Kukorica", + "it": "Mais", + "ja": "とうもろこし", + "nb_NO": "Mais", + "nl": "Maïs", + "pt_BR": "Milho", + "ru": "Кукуруза", + "si": null, + "sk": "Kukuričný klas", + "sr": "кукуруз", + "sv": "Majs", + "szl": null, + "tzm": null, + "uk": "Кукурудза", + "zh_Hans": "玉米" + } + }, + { + "number": 27, + "emoji": "🍕", + "description": "Pizza", + "unicode": "U+1F355", + "translated_descriptions": { + "ar": "بِيتزا", + "bg": "Пица", + "ca": "Pizza", + "cs": "Pizza", + "de": "Pizza", + "eo": "Pico", + "es": "Pizza", + "et": "Pitsa", + "fi": "Pizza", + "fr": "Pizza", + "hr": "pizza", + "hu": "Pizza", + "it": "Pizza", + "ja": "ピザ", + "nb_NO": "Pizza", + "nl": "Pizza", + "pt_BR": "Pizza", + "ru": "Пицца", + "si": null, + "sk": "Pizza", + "sr": "пица", + "sv": "Pizza", + "szl": null, + "tzm": null, + "uk": "Піца", + "zh_Hans": "披萨" + } + }, + { + "number": 28, + "emoji": "🎂", + "description": "Cake", + "unicode": "U+1F382", + "translated_descriptions": { + "ar": "كَعكَة", + "bg": "Торта", + "ca": "Pastís", + "cs": "Dort", + "de": "Kuchen", + "eo": "Torto", + "es": "Tarta", + "et": "Kook", + "fi": "Kakku", + "fr": "Gâteau", + "hr": "torta", + "hu": "Süti", + "it": "Torta", + "ja": "ケーキ", + "nb_NO": "Kake", + "nl": "Taart", + "pt_BR": "Bolo", + "ru": "Торт", + "si": null, + "sk": "Narodeninová torta", + "sr": "торта", + "sv": "Tårta", + "szl": null, + "tzm": null, + "uk": "Пиріг", + "zh_Hans": "蛋糕" + } + }, + { + "number": 29, + "emoji": "❤️", + "description": "Heart", + "unicode": "U+2764U+FE0F", + "translated_descriptions": { + "ar": "قَلب", + "bg": "Сърце", + "ca": "Cor", + "cs": "Srdce", + "de": "Herz", + "eo": "Koro", + "es": "Corazón", + "et": "Süda", + "fi": "Sydän", + "fr": "Cœur", + "hr": "srca", + "hu": "Szív", + "it": "Cuore", + "ja": "ハート", + "nb_NO": "Hjerte", + "nl": "Hart", + "pt_BR": "Coração", + "ru": "Сердце", + "si": null, + "sk": "červené srdce", + "sr": "срце", + "sv": "Hjärta", + "szl": null, + "tzm": "Ul", + "uk": "Серце", + "zh_Hans": "心" + } + }, + { + "number": 30, + "emoji": "😀", + "description": "Smiley", + "unicode": "U+1F600", + "translated_descriptions": { + "ar": "اِبتِسَامَة", + "bg": "Усмивка", + "ca": "Somrient", + "cs": "Smajlík", + "de": "Lächeln", + "eo": "Rideto", + "es": "Emoticono", + "et": "Smaili", + "fi": "Hymynaama", + "fr": "Sourire", + "hr": "smajlića", + "hu": "Mosoly", + "it": "Faccina sorridente", + "ja": "スマイル", + "nb_NO": "Smilefjes", + "nl": "Smiley", + "pt_BR": "Sorriso", + "ru": "Улыбка", + "si": null, + "sk": "Škeriaca sa tvár", + "sr": "смајли", + "sv": "Smiley", + "szl": null, + "tzm": null, + "uk": "Посмішка", + "zh_Hans": "笑脸" + } + }, + { + "number": 31, + "emoji": "🤖", + "description": "Robot", + "unicode": "U+1F916", + "translated_descriptions": { + "ar": "رُوبُوت", + "bg": "Робот", + "ca": "Robot", + "cs": "Robot", + "de": "Roboter", + "eo": "Roboto", + "es": "Robot", + "et": "Robot", + "fi": "Robotti", + "fr": "Robot", + "hr": "robot", + "hu": "Robot", + "it": "Robot", + "ja": "ロボと", + "nb_NO": "Robot", + "nl": "Robot", + "pt_BR": "Robô", + "ru": "Робот", + "si": null, + "sk": "Robot", + "sr": "робот", + "sv": "Robot", + "szl": null, + "tzm": "Aṛubu", + "uk": "Робот", + "zh_Hans": "机器人" + } + }, + { + "number": 32, + "emoji": "🎩", + "description": "Hat", + "unicode": "U+1F3A9", + "translated_descriptions": { + "ar": "قُبَّعَة", + "bg": "Шапка", + "ca": "Barret", + "cs": "Klobouk", + "de": "Hut", + "eo": "Ĉapelo", + "es": "Sombrero", + "et": "Kübar", + "fi": "Hattu", + "fr": "Chapeau", + "hr": "kapa", + "hu": "Kalap", + "it": "Cappello", + "ja": "帽子", + "nb_NO": "Hatt", + "nl": "Hoed", + "pt_BR": "Chapéu", + "ru": "Шляпа", + "si": null, + "sk": "Cilinder", + "sr": "шешир", + "sv": "Hatt", + "szl": null, + "tzm": "Taraza", + "uk": "Капелюх", + "zh_Hans": "帽子" + } + }, + { + "number": 33, + "emoji": "👓", + "description": "Glasses", + "unicode": "U+1F453", + "translated_descriptions": { + "ar": "نَظَّارَة", + "bg": "Очила", + "ca": "Ulleres", + "cs": "Brýle", + "de": "Brille", + "eo": "Okulvitroj", + "es": "Gafas", + "et": "Prillid", + "fi": "Silmälasit", + "fr": "Lunettes", + "hr": "naočale", + "hu": "Szemüveg", + "it": "Occhiali", + "ja": "めがね", + "nb_NO": "Briller", + "nl": "Bril", + "pt_BR": "Óculos", + "ru": "Очки", + "si": null, + "sk": "Okuliare", + "sr": "наочаре", + "sv": "Glasögon", + "szl": null, + "tzm": null, + "uk": "Окуляри", + "zh_Hans": "眼镜" + } + }, + { + "number": 34, + "emoji": "🔧", + "description": "Spanner", + "unicode": "U+1F527", + "translated_descriptions": { + "ar": "مِفتَاحُ رَبط", + "bg": "Гаечен ключ", + "ca": "Clau anglesa", + "cs": "Klíč", + "de": "Schraubenschlüssel", + "eo": "Ŝraŭbŝlosilo", + "es": "Llave inglesa", + "et": "Mutrivõti", + "fi": "Kiintoavain", + "fr": "Clé à molette", + "hr": "ključ", + "hu": "Csavarkulcs", + "it": "Chiave inglese", + "ja": "スパナ", + "nb_NO": "Fastnøkkel", + "nl": "Moersleutel", + "pt_BR": "Chave inglesa", + "ru": "Ключ", + "si": null, + "sk": "Francúzsky kľúč", + "sr": "кључ", + "sv": "Skruvnyckel", + "szl": null, + "tzm": null, + "uk": "Гайковий ключ", + "zh_Hans": "扳手" + } + }, + { + "number": 35, + "emoji": "🎅", + "description": "Santa", + "unicode": "U+1F385", + "translated_descriptions": { + "ar": "سانتا", + "bg": "Дядо Коледа", + "ca": "Pare Noél", + "cs": "Mikuláš", + "de": "Weihnachtsmann", + "eo": "Kristnaska viro", + "es": "Papá Noel", + "et": "Jõuluvana", + "fi": "Joulupukki", + "fr": "Père Noël", + "hr": "deda Mraz", + "hu": "Télapó", + "it": "Babbo Natale", + "ja": "サンタ", + "nb_NO": "Julenisse", + "nl": "Kerstman", + "pt_BR": "Papai-noel", + "ru": "Санта", + "si": null, + "sk": "Santa Claus", + "sr": "деда Мраз", + "sv": "Tomte", + "szl": null, + "tzm": null, + "uk": "Санта Клаус", + "zh_Hans": "圣诞老人" + } + }, + { + "number": 36, + "emoji": "👍", + "description": "Thumbs Up", + "unicode": "U+1F44D", + "translated_descriptions": { + "ar": "رَفعُ إِبهَام", + "bg": "Палец нагоре", + "ca": "Polzes amunt", + "cs": "Palec nahoru", + "de": "Daumen Hoch", + "eo": "Dikfingro supren", + "es": "Pulgar arriba", + "et": "Pöidlad püsti", + "fi": "Peukalo ylös", + "fr": "Pouce en l’air", + "hr": "palac gore", + "hu": "Hüvelykujj fel", + "it": "Pollice alzato", + "ja": "いいね", + "nb_NO": "Tommel Opp", + "nl": "Duim omhoog", + "pt_BR": "Joinha", + "ru": "Большой палец вверх", + "si": null, + "sk": "Palec nahor", + "sr": "палчић горе", + "sv": "Tummen upp", + "szl": null, + "tzm": null, + "uk": "Великий палець вгору", + "zh_Hans": "赞" + } + }, + { + "number": 37, + "emoji": "☂️", + "description": "Umbrella", + "unicode": "U+2602U+FE0F", + "translated_descriptions": { + "ar": "مِظَلَّة", + "bg": "Чадър", + "ca": "Paraigües", + "cs": "Deštník", + "de": "Regenschirm", + "eo": "Ombrelo", + "es": "Paraguas", + "et": "Vihmavari", + "fi": "Sateenvarjo", + "fr": "Parapluie", + "hr": "kišobran", + "hu": "Esernyő", + "it": "Ombrello", + "ja": "傘", + "nb_NO": "Paraply", + "nl": "Paraplu", + "pt_BR": "Guarda-chuva", + "ru": "Зонт", + "si": null, + "sk": "Dáždnik", + "sr": "кишобран", + "sv": "Paraply", + "szl": null, + "tzm": null, + "uk": "Парасолька", + "zh_Hans": "伞" + } + }, + { + "number": 38, + "emoji": "⌛", + "description": "Hourglass", + "unicode": "U+231B", + "translated_descriptions": { + "ar": "سَاعَةٌ رَملِيَّة", + "bg": "Пясъчен часовник", + "ca": "Rellotge de sorra", + "cs": "Přesýpací hodiny", + "de": "Sanduhr", + "eo": "Sablohorloĝo", + "es": "Reloj de arena", + "et": "Liivakell", + "fi": "Tiimalasi", + "fr": "Sablier", + "hr": "pješčani sat", + "hu": "Homokóra", + "it": "Clessidra", + "ja": "砂時計", + "nb_NO": "Timeglass", + "nl": "Zandloper", + "pt_BR": "Ampulheta", + "ru": "Песочные часы", + "si": null, + "sk": "Presýpacie hodiny", + "sr": "пешчаник", + "sv": "Timglas", + "szl": null, + "tzm": null, + "uk": "Пісковий годинник", + "zh_Hans": "沙漏" + } + }, + { + "number": 39, + "emoji": "⏰", + "description": "Clock", + "unicode": "U+23F0", + "translated_descriptions": { + "ar": "سَاعَة", + "bg": "Часовник", + "ca": "Rellotge", + "cs": "Hodiny", + "de": "Uhr", + "eo": "Horloĝo", + "es": "Reloj", + "et": "Kell", + "fi": "Pöytäkello", + "fr": "Réveil", + "hr": "sat", + "hu": "Óra", + "it": "Orologio", + "ja": "時計", + "nb_NO": "Klokke", + "nl": "Wekker", + "pt_BR": "Relógio", + "ru": "Часы", + "si": null, + "sk": "Budík", + "sr": "сат", + "sv": "Klocka", + "szl": null, + "tzm": null, + "uk": "Годинник", + "zh_Hans": "时钟" + } + }, + { + "number": 40, + "emoji": "🎁", + "description": "Gift", + "unicode": "U+1F381", + "translated_descriptions": { + "ar": "هَدِيَّة", + "bg": "Подарък", + "ca": "Regal", + "cs": "Dárek", + "de": "Geschenk", + "eo": "Donaco", + "es": "Regalo", + "et": "Kingitus", + "fi": "Lahja", + "fr": "Cadeau", + "hr": "poklon", + "hu": "Ajándék", + "it": "Regalo", + "ja": "ギフト", + "nb_NO": "Gave", + "nl": "Geschenk", + "pt_BR": "Presente", + "ru": "Подарок", + "si": null, + "sk": "Zabalený darček", + "sr": "поклон", + "sv": "Present", + "szl": null, + "tzm": null, + "uk": "Подарунок", + "zh_Hans": "礼物" + } + }, + { + "number": 41, + "emoji": "💡", + "description": "Light Bulb", + "unicode": "U+1F4A1", + "translated_descriptions": { + "ar": "مِصبَاح", + "bg": "Лампа", + "ca": "Bombeta", + "cs": "Žárovka", + "de": "Glühbirne", + "eo": "Lampo", + "es": "Bombilla", + "et": "Lambipirn", + "fi": "Hehkulamppu", + "fr": "Ampoule", + "hr": "žarulja", + "hu": "Égő", + "it": "Lampadina", + "ja": "電球", + "nb_NO": "Lyspære", + "nl": "Gloeilamp", + "pt_BR": "Lâmpada", + "ru": "Лампочка", + "si": null, + "sk": "Žiarovka", + "sr": "сијалица", + "sv": "Lampa", + "szl": null, + "tzm": null, + "uk": "Лампочка", + "zh_Hans": "灯泡" + } + }, + { + "number": 42, + "emoji": "📕", + "description": "Book", + "unicode": "U+1F4D5", + "translated_descriptions": { + "ar": "كِتَاب", + "bg": "Книга", + "ca": "Llibre", + "cs": "Kniha", + "de": "Buch", + "eo": "Libro", + "es": "Libro", + "et": "Raamat", + "fi": "Kirja", + "fr": "Livre", + "hr": "knjiga", + "hu": "Könyv", + "it": "Libro", + "ja": "本", + "nb_NO": "Bok", + "nl": "Boek", + "pt_BR": "Livro", + "ru": "Книга", + "si": null, + "sk": "Zatvorená kniha", + "sr": "књига", + "sv": "Bok", + "szl": null, + "tzm": "Adlis", + "uk": "Книга", + "zh_Hans": "书" + } + }, + { + "number": 43, + "emoji": "✏️", + "description": "Pencil", + "unicode": "U+270FU+FE0F", + "translated_descriptions": { + "ar": "قَلَمُ رَصاص", + "bg": "Молив", + "ca": "Llapis", + "cs": "Tužka", + "de": "Bleistift", + "eo": "Krajono", + "es": "Lápiz", + "et": "Pliiats", + "fi": "Lyijykynä", + "fr": "Crayon", + "hr": "olovka", + "hu": "Ceruza", + "it": "Matita", + "ja": "鉛筆", + "nb_NO": "Blyant", + "nl": "Potlood", + "pt_BR": "Lápis", + "ru": "Карандаш", + "si": null, + "sk": "Ceruzka", + "sr": "оловка", + "sv": "Penna", + "szl": null, + "tzm": null, + "uk": "Олівець", + "zh_Hans": "铅笔" + } + }, + { + "number": 44, + "emoji": "📎", + "description": "Paperclip", + "unicode": "U+1F4CE", + "translated_descriptions": { + "ar": "مِشبَكُ وَرَق", + "bg": "Кламер", + "ca": "Clip", + "cs": "Sponka", + "de": "Büroklammer", + "eo": "Paperkuntenilo", + "es": "Clip", + "et": "Kirjaklamber", + "fi": "Paperiliitin", + "fr": "Trombone", + "hr": "spajalica", + "hu": "Gémkapocs", + "it": "Graffetta", + "ja": "クリップ", + "nb_NO": "BInders", + "nl": "Papierklemmetje", + "pt_BR": "Clipe de papel", + "ru": "Скрепка", + "si": null, + "sk": "Sponka na papier", + "sr": "спајалица", + "sv": "Gem", + "szl": null, + "tzm": null, + "uk": "Спиначка", + "zh_Hans": "回形针" + } + }, + { + "number": 45, + "emoji": "✂️", + "description": "Scissors", + "unicode": "U+2702U+FE0F", + "translated_descriptions": { + "ar": "مِقَصّ", + "bg": "Ножици", + "ca": "Tisores", + "cs": "Nůžky", + "de": "Schere", + "eo": "Tondilo", + "es": "Tijeras", + "et": "Käärid", + "fi": "Sakset", + "fr": "Ciseaux", + "hr": "škare", + "hu": "Olló", + "it": "Forbici", + "ja": "はさみ", + "nb_NO": "Saks", + "nl": "Schaar", + "pt_BR": "Tesoura", + "ru": "Ножницы", + "si": null, + "sk": "Nožnice", + "sr": "маказе", + "sv": "Sax", + "szl": null, + "tzm": null, + "uk": "Ножиці", + "zh_Hans": "剪刀" + } + }, + { + "number": 46, + "emoji": "🔒", + "description": "Lock", + "unicode": "U+1F512", + "translated_descriptions": { + "ar": "قُفل", + "bg": "Катинар", + "ca": "Cadenat", + "cs": "Zámek", + "de": "Schloss", + "eo": "Seruro", + "es": "Candado", + "et": "Lukk", + "fi": "Lukko", + "fr": "Cadenas", + "hr": "zaključati", + "hu": "Lakat", + "it": "Lucchetto", + "ja": "錠前", + "nb_NO": "Lås", + "nl": "Slot", + "pt_BR": "Cadeado", + "ru": "Замок", + "si": null, + "sk": "Zatvorená zámka", + "sr": "катанац", + "sv": "Lås", + "szl": null, + "tzm": null, + "uk": "Замок", + "zh_Hans": "锁" + } + }, + { + "number": 47, + "emoji": "🔑", + "description": "Key", + "unicode": "U+1F511", + "translated_descriptions": { + "ar": "مِفتَاح", + "bg": "Ключ", + "ca": "Clau", + "cs": "Klíč", + "de": "Schlüssel", + "eo": "Ŝlosilo", + "es": "Llave", + "et": "Võti", + "fi": "Avain", + "fr": "Clé", + "hr": "ključ", + "hu": "Kulcs", + "it": "Chiave", + "ja": "鍵", + "nb_NO": "Nøkkel", + "nl": "Sleutel", + "pt_BR": "Chave", + "ru": "Ключ", + "si": null, + "sk": "Kľúč", + "sr": "кључ", + "sv": "Nyckel", + "szl": null, + "tzm": "Tasarut", + "uk": "Ключ", + "zh_Hans": "钥匙" + } + }, + { + "number": 48, + "emoji": "🔨", + "description": "Hammer", + "unicode": "U+1F528", + "translated_descriptions": { + "ar": "مِطرَقَة", + "bg": "Чук", + "ca": "Martell", + "cs": "Kladivo", + "de": "Hammer", + "eo": "Martelo", + "es": "Martillo", + "et": "Haamer", + "fi": "Vasara", + "fr": "Marteau", + "hr": "čekić", + "hu": "Kalapács", + "it": "Martello", + "ja": "金槌", + "nb_NO": "Hammer", + "nl": "Hamer", + "pt_BR": "Martelo", + "ru": "Молоток", + "si": null, + "sk": "Kladivo", + "sr": "чекић", + "sv": "Hammare", + "szl": null, + "tzm": null, + "uk": "Молоток", + "zh_Hans": "锤子" + } + }, + { + "number": 49, + "emoji": "☎️", + "description": "Telephone", + "unicode": "U+260EU+FE0F", + "translated_descriptions": { + "ar": "تِلِفُون", + "bg": "Телефон", + "ca": "Telèfon", + "cs": "Telefon", + "de": "Telefon", + "eo": "Telefono", + "es": "Telefono", + "et": "Telefon", + "fi": "Puhelin", + "fr": "Téléphone", + "hr": "telefon", + "hu": "Telefon", + "it": "Telefono", + "ja": "電話機", + "nb_NO": "Telefon", + "nl": "Telefoon", + "pt_BR": "Telefone", + "ru": "Телефон", + "si": null, + "sk": "Telefón", + "sr": "телефон", + "sv": "Telefon", + "szl": null, + "tzm": "Atilifun", + "uk": "Телефон", + "zh_Hans": "电话" + } + }, + { + "number": 50, + "emoji": "🏁", + "description": "Flag", + "unicode": "U+1F3C1", + "translated_descriptions": { + "ar": "عَلَم", + "bg": "Флаг", + "ca": "Bandera", + "cs": "Vlajka", + "de": "Flagge", + "eo": "Flago", + "es": "Bandera", + "et": "Lipp", + "fi": "Lippu", + "fr": "Drapeau", + "hr": "zastava", + "hu": "Zászló", + "it": "Bandiera", + "ja": "旗", + "nb_NO": "Flagg", + "nl": "Vlag", + "pt_BR": "Bandeira", + "ru": "Флаг", + "si": null, + "sk": "Kockovaná zástava", + "sr": "застава", + "sv": "Flagga", + "szl": null, + "tzm": "Acenyal", + "uk": "Прапор", + "zh_Hans": "旗帜" + } + }, + { + "number": 51, + "emoji": "🚂", + "description": "Train", + "unicode": "U+1F682", + "translated_descriptions": { + "ar": "قِطَار", + "bg": "Влак", + "ca": "Tren", + "cs": "Vlak", + "de": "Zug", + "eo": "Vagonaro", + "es": "Tren", + "et": "Rong", + "fi": "Juna", + "fr": "Train", + "hr": "vlak", + "hu": "Vonat", + "it": "Treno", + "ja": "電車", + "nb_NO": "Tog", + "nl": "Trein", + "pt_BR": "Trem", + "ru": "Поезд", + "si": null, + "sk": "Rušeň", + "sr": "воз", + "sv": "Tåg", + "szl": null, + "tzm": null, + "uk": "Потяг", + "zh_Hans": "火车" + } + }, + { + "number": 52, + "emoji": "🚲", + "description": "Bicycle", + "unicode": "U+1F6B2", + "translated_descriptions": { + "ar": "دَرّاجَة", + "bg": "Колело", + "ca": "Bicicleta", + "cs": "Kolo", + "de": "Fahrrad", + "eo": "Biciklo", + "es": "Bicicleta", + "et": "Jalgratas", + "fi": "Polkupyörä", + "fr": "Vélo", + "hr": "bicikl", + "hu": "Kerékpár", + "it": "Bicicletta", + "ja": "自転車", + "nb_NO": "Sykkel", + "nl": "Fiets", + "pt_BR": "Bicicleta", + "ru": "Велосипед", + "si": null, + "sk": "Bicykel", + "sr": "бицикл", + "sv": "Cykel", + "szl": null, + "tzm": null, + "uk": "Велосипед", + "zh_Hans": "自行车" + } + }, + { + "number": 53, + "emoji": "✈️", + "description": "Aeroplane", + "unicode": "U+2708U+FE0F", + "translated_descriptions": { + "ar": "طَائِرة", + "bg": "Самолет", + "ca": "Avió", + "cs": "Letadlo", + "de": "Flugzeug", + "eo": "Aviadilo", + "es": "Avión", + "et": "Lennuk", + "fi": "Lentokone", + "fr": "Avion", + "hr": "avion", + "hu": "Repülő", + "it": "Aeroplano", + "ja": "飛行機", + "nb_NO": "Fly", + "nl": "Vliegtuig", + "pt_BR": "Avião", + "ru": "Самолет", + "si": null, + "sk": "Lietadlo", + "sr": "авион", + "sv": "Flygplan", + "szl": null, + "tzm": null, + "uk": "Літак", + "zh_Hans": "飞机" + } + }, + { + "number": 54, + "emoji": "🚀", + "description": "Rocket", + "unicode": "U+1F680", + "translated_descriptions": { + "ar": "صَارُوخ", + "bg": "Ракета", + "ca": "Coet", + "cs": "Raketa", + "de": "Rakete", + "eo": "Raketo", + "es": "Cohete", + "et": "Rakett", + "fi": "Raketti", + "fr": "Fusée", + "hr": "raketa", + "hu": "Rakáta", + "it": "Razzo", + "ja": "ロケット", + "nb_NO": "Rakett", + "nl": "Raket", + "pt_BR": "Foguete", + "ru": "Ракета", + "si": null, + "sk": "Raketa", + "sr": "ракета", + "sv": "Raket", + "szl": null, + "tzm": null, + "uk": "Ракета", + "zh_Hans": "火箭" + } + }, + { + "number": 55, + "emoji": "🏆", + "description": "Trophy", + "unicode": "U+1F3C6", + "translated_descriptions": { + "ar": "كَأسُ النَّصر", + "bg": "Трофей", + "ca": "Trofeu", + "cs": "Pohár", + "de": "Pokal", + "eo": "Trofeo", + "es": "Trofeo", + "et": "Auhind", + "fi": "Palkinto", + "fr": "Trophée", + "hr": "trofej", + "hu": "Trófea", + "it": "Trofeo", + "ja": "トロフィー", + "nb_NO": "Pokal", + "nl": "Trofee", + "pt_BR": "Troféu", + "ru": "Кубок", + "si": null, + "sk": "Trofej", + "sr": "пехар", + "sv": "Trofé", + "szl": null, + "tzm": null, + "uk": "Приз", + "zh_Hans": "奖杯" + } + }, + { + "number": 56, + "emoji": "⚽", + "description": "Ball", + "unicode": "U+26BD", + "translated_descriptions": { + "ar": "كُرَة", + "bg": "Топка", + "ca": "Pilota", + "cs": "Míč", + "de": "Ball", + "eo": "Pilko", + "es": "Bola", + "et": "Pall", + "fi": "Pallo", + "fr": "Ballon", + "hr": "lopta", + "hu": "Labda", + "it": "Palla", + "ja": "ボール", + "nb_NO": "Ball", + "nl": "Bal", + "pt_BR": "Bola", + "ru": "Мяч", + "si": null, + "sk": "Futbal", + "sr": "лопта", + "sv": "Boll", + "szl": null, + "tzm": "Tcama", + "uk": "М'яч", + "zh_Hans": "球" + } + }, + { + "number": 57, + "emoji": "🎸", + "description": "Guitar", + "unicode": "U+1F3B8", + "translated_descriptions": { + "ar": "غيتار", + "bg": "Китара", + "ca": "Guitarra", + "cs": "Kytara", + "de": "Gitarre", + "eo": "Gitaro", + "es": "Guitarra", + "et": "Kitarr", + "fi": "Kitara", + "fr": "Guitare", + "hr": "gitara", + "hu": "Gitár", + "it": "Chitarra", + "ja": "ギター", + "nb_NO": "Gitar", + "nl": "Gitaar", + "pt_BR": "Guitarra", + "ru": "Гитара", + "si": null, + "sk": "Gitara", + "sr": "гитара", + "sv": "Gitarr", + "szl": null, + "tzm": "Agiṭaṛ", + "uk": "Гітара", + "zh_Hans": "吉他" + } + }, + { + "number": 58, + "emoji": "🎺", + "description": "Trumpet", + "unicode": "U+1F3BA", + "translated_descriptions": { + "ar": "بُوق", + "bg": "Тромпет", + "ca": "Trompeta", + "cs": "Trumpeta", + "de": "Trompete", + "eo": "Trumpeto", + "es": "Trompeta", + "et": "Trompet", + "fi": "Trumpetti", + "fr": "Trompette", + "hr": "truba", + "hu": "Trombita", + "it": "Trombetta", + "ja": "トランペット", + "nb_NO": "Trompet", + "nl": "Trompet", + "pt_BR": "Trombeta", + "ru": "Труба", + "si": null, + "sk": "Trúbka", + "sr": "труба", + "sv": "Trumpet", + "szl": null, + "tzm": null, + "uk": "Труба", + "zh_Hans": "喇叭" + } + }, + { + "number": 59, + "emoji": "🔔", + "description": "Bell", + "unicode": "U+1F514", + "translated_descriptions": { + "ar": "جَرَس", + "bg": "Звънец", + "ca": "Campana", + "cs": "Zvonek", + "de": "Glocke", + "eo": "Sonorilo", + "es": "Campana", + "et": "Kelluke", + "fi": "Soittokello", + "fr": "Cloche", + "hr": "zvono", + "hu": "Harang", + "it": "Campana", + "ja": "ベル", + "nb_NO": "Bjelle", + "nl": "Bel", + "pt_BR": "Sino", + "ru": "Колокол", + "si": null, + "sk": "Zvon", + "sr": "звоно", + "sv": "Bjällra", + "szl": null, + "tzm": null, + "uk": "Дзвін", + "zh_Hans": "铃铛" + } + }, + { + "number": 60, + "emoji": "⚓", + "description": "Anchor", + "unicode": "U+2693", + "translated_descriptions": { + "ar": "مِرسَاة", + "bg": "Котва", + "ca": "Àncora", + "cs": "Kotva", + "de": "Anker", + "eo": "Ankro", + "es": "Ancla", + "et": "Ankur", + "fi": "Ankkuri", + "fr": "Ancre", + "hr": "sidro", + "hu": "Horgony", + "it": "Ancora", + "ja": "いかり", + "nb_NO": "Anker", + "nl": "Anker", + "pt_BR": "Âncora", + "ru": "Якорь", + "si": null, + "sk": "Kotva", + "sr": "сидро", + "sv": "Ankare", + "szl": null, + "tzm": null, + "uk": "Якір", + "zh_Hans": "锚" + } + }, + { + "number": 61, + "emoji": "🎧", + "description": "Headphones", + "unicode": "U+1F3A7", + "translated_descriptions": { + "ar": "سَمّاعَة رَأس", + "bg": "Слушалки", + "ca": "Auriculars", + "cs": "Sluchátka", + "de": "Kopfhörer", + "eo": "Kapaŭdilo", + "es": "Cascos", + "et": "Kõrvaklapid", + "fi": "Kuulokkeet", + "fr": "Casque audio", + "hr": "slušalice", + "hu": "Fejhallgató", + "it": "Cuffie", + "ja": "ヘッドホン", + "nb_NO": "Hodetelefoner", + "nl": "Koptelefoon", + "pt_BR": "Fones de ouvido", + "ru": "Наушники", + "si": null, + "sk": "Slúchadlá", + "sr": "слушалице", + "sv": "Hörlurar", + "szl": null, + "tzm": null, + "uk": "Навушники", + "zh_Hans": "耳机" + } + }, + { + "number": 62, + "emoji": "📁", + "description": "Folder", + "unicode": "U+1F4C1", + "translated_descriptions": { + "ar": "مُجَلَّد", + "bg": "Папка", + "ca": "Carpeta", + "cs": "Složka", + "de": "Ordner", + "eo": "Dosierujo", + "es": "Carpeta", + "et": "Kaust", + "fi": "Kansio", + "fr": "Dossier", + "hr": "mapu", + "hu": "Mappa", + "it": "Cartella", + "ja": "フォルダ", + "nb_NO": "Mappe", + "nl": "Map", + "pt_BR": "Pasta", + "ru": "Папка", + "si": null, + "sk": "Fascikel", + "sr": "фасцикла", + "sv": "Mapp", + "szl": null, + "tzm": "Asdaw", + "uk": "Тека", + "zh_Hans": "文件夹" + } + }, + { + "number": 63, + "emoji": "📌", + "description": "Pin", + "unicode": "U+1F4CC", + "translated_descriptions": { + "ar": "دَبُّوس", + "bg": "Кабърче", + "ca": "Xinxeta", + "cs": "Špendlík", + "de": "Stecknadel", + "eo": "Pinglo", + "es": "Alfiler", + "et": "Nööpnõel", + "fi": "Nuppineula", + "fr": "Punaise", + "hr": "pribadača", + "hu": "Rajszeg", + "it": "Puntina", + "ja": "ピン", + "nb_NO": "Tegnestift", + "nl": "Duimspijker", + "pt_BR": "Alfinete", + "ru": "Булавка", + "si": null, + "sk": "Špendlík", + "sr": "чиода", + "sv": "Häftstift", + "szl": null, + "tzm": null, + "uk": "Кнопка", + "zh_Hans": "图钉" + } + } +] 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 |