diff options
37 files changed, 1106 insertions, 332 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index 410ad12e..4e2d4b5d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -7,16 +7,9 @@ environment: 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" @@ -26,11 +19,15 @@ init: 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%" +- cd 3rdparty/libQtOlm +- git clone https://gitlab.matrix.org/matrix-org/olm.git +- cd ../.. +- cmake -G "NMake Makefiles JOM" -H. -Bbuild -DCMAKE_CXX_FLAGS="/EHsc /W3" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="%DEPLOY_DIR%" build_script: -- if %MAKETOOL% == cmake cmake --build build -- if %MAKETOOL% == qmake qmake && jom +- cmake --build build +# qmake uses olm just built by CMake - it can't build olm on its own. +- qmake "INCLUDEPATH += 3rdparty/libQtOlm/olm/include" "LIBS += -Lbuild" && jom #after_build: #- cmake --build build --target install diff --git a/.gitmodules b/.gitmodules index e69de29b..eb4c1815 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "3rdparty/libQtOlm"] + path = 3rdparty/libQtOlm + url = https://gitlab.com/b0/libqtolm.git diff --git a/.travis.yml b/.travis.yml index e0b10ce8..6880844b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,15 @@ language: cpp +git: + depth: false + +before_cache: +- brew cleanup + +cache: + directories: + - $HOME/Library/Caches/Homebrew + addons: apt: sources: @@ -33,6 +43,9 @@ before_install: - if [ "$TRAVIS_OS_NAME" = "linux" ]; then USE_NINJA="-GNinja"; VALGRIND="valgrind $VALGRIND_OPTIONS"; . /opt/qt57/bin/qt57-env.sh; fi install: +- pushd 3rdparty/libQtOlm +- git clone https://gitlab.matrix.org/matrix-org/olm.git +- popd - git clone https://github.com/QMatrixClient/matrix-doc.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - pushd gtad @@ -54,11 +67,11 @@ script: - 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" +# Build with qmake +- qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" "INCLUDEPATH += 3rdparty/libQtOlm/olm/include" "LIBS += -Lbuild/lib" - 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 +- if [ "$QMC_TEST_USER" != "" ]; then LD_LIBRARY_PATH="build/lib" $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: diff --git a/3rdparty/libQtOlm b/3rdparty/libQtOlm new file mode 160000 +Subproject f610197ba38ef87bbab8bcff1053bda684a5994 diff --git a/CMakeLists.txt b/CMakeLists.txt index eafa95f1..13f6fcfb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,24 @@ endforeach () find_package(Qt5 5.4.1 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) +if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) + AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h) + add_subdirectory(3rdparty/libQtOlm EXCLUDE_FROM_ALL) + include_directories(3rdparty/libQtOlm) + if (NOT DEFINED USE_INTREE_LIBQOLM) + set (USE_INTREE_LIBQOLM 1) + endif () +endif () +if (NOT USE_INTREE_LIBQOLM) + find_package(QtOlm 0.1.0 REQUIRED) + if (NOT QtOlm_FOUND) + message( WARNING "libQtOlm not found; configuration will most likely fail.") + message( WARNING "Make sure you have installed libQtOlm development files") + message( WARNING "as a package or checked out the library sources in lib/.") + message( WARNING "See also BUILDING.md") + endif () +endif () + if (GTAD_PATH) get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) endif () @@ -70,6 +88,7 @@ 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 "Install Prefix: ${CMAKE_INSTALL_PREFIX}" ) message( STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}" ) if (ABS_API_DEF_PATH AND ABS_GTAD_PATH) message( STATUS "Generating API stubs enabled (use --target update-api)" ) @@ -81,6 +100,20 @@ if (ABS_API_DEF_PATH AND ABS_GTAD_PATH) message( STATUS "${CLANG_FORMAT} is NOT FOUND; --target update-format-api disabled") endif () endif () +find_package(Git) +if (USE_INTREE_LIBQOLM) + message( STATUS "Using in-tree libQtOlm") + if (GIT_FOUND) + execute_process(COMMAND + "${GIT_EXECUTABLE}" rev-parse -q HEAD + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm + OUTPUT_VARIABLE QTOLM_GIT_SHA1 + OUTPUT_STRIP_TRAILING_WHITESPACE) + message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}") + endif (GIT_FOUND) +else () + message( STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}") +endif () message( STATUS "=============================================================================" ) message( STATUS ) @@ -98,6 +131,7 @@ set(libqmatrixclient_SRCS lib/networksettings.cpp lib/converters.cpp lib/util.cpp + lib/encryptionmanager.cpp lib/eventitem.cpp lib/events/event.cpp lib/events/roomevent.cpp @@ -114,6 +148,7 @@ set(libqmatrixclient_SRCS lib/events/callhangupevent.cpp lib/events/callinviteevent.cpp lib/events/directchatevent.cpp + lib/events/encryptionevent.cpp lib/jobs/requestdata.cpp lib/jobs/basejob.cpp lib/jobs/syncjob.cpp @@ -187,7 +222,7 @@ target_include_directories(QMatrixClient PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib> $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> ) -target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) +target_link_libraries(QMatrixClient QtOlm Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) add_executable(qmc-example ${example_SRCS}) target_link_libraries(qmc-example Qt5::Core QMatrixClient) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5480b22c..d7fa19bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,14 +17,14 @@ 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 easy](https://github.com/QMatrixClient/libQMatrixClient/labels/easy) +[issues tagged as easy](https://github.com/quotient-im/libQuotient/labels/easy) are smaller tasks that may typically take 1-3 days. You are welcome aboard! @@ -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 @@ -88,7 +88,7 @@ a commit without a DCO is an accident and the DCO still applies. Unless a contributor explicitly specifies otherwise, we assume contributors to agree that all contributed code is released either under *LGPL v2.1 or later*. -This is more than just [LGPL v2.1 libQMatrixClient now uses](./COPYING) +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 @@ -108,7 +108,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; @@ -157,17 +157,17 @@ there are handy build targets for CMake; patches with the same targets for qmake are (you guessed it) very welcome. #### Why generate the code at all? -Because before both original authors of libQMatrixClient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep, coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). +Because before both original authors of libQuotient 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). + similar to libQuotient 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; + `git clone https://github.com/quotient-im/matrix-doc.git` + (quotient-im/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). 4. If you plan to submit a PR or just would like the generated code to be formatted, you should either ensure you have clang-format in your PATH or @@ -175,7 +175,7 @@ Because before both original authors of libQMatrixClient had to do monkey busine to the CMake invocation below. #### Generating CS API contents -1. Pass additional configuration to CMake when configuring libQMatrixClient: +1. Pass additional configuration to CMake when configuring libQuotient: `-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, @@ -189,14 +189,14 @@ Because before both original authors of libQMatrixClient had to do monkey busine 3. Re-run CMake so that the build system knows about new files, if there are any. #### Changing generated code -See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQMatrixClient are described here. +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. 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 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 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. @@ -205,7 +205,7 @@ The types map in `gtad.yaml` is the central switchboard when it comes to matchin * `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable). * `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`. -Instead of relying on the event structure definition in the OpenAPI files, `gtad.yaml` uses pointers to libQMatrixClient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. +Instead of relying on the event structure definition in the OpenAPI files, `gtad.yaml` uses pointers to 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. ### Library API and doc-comments @@ -219,7 +219,7 @@ implementation details as much as possible. `_impl` namespace is reserved for definitions that should not be used by clients and are not covered by API guarantees. -Note: As of now, all header files of libQMatrixClient are considered public; this may change eventually. +Note: As of now, all header files of libQuotient are considered public; this may change eventually. ### Code formatting @@ -244,7 +244,7 @@ Additional considerations: 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: + 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 @@ -292,14 +292,14 @@ Exercise the [principle of least privilege](https://en.wikipedia.org/wiki/Princi 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. -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). +We want the software to have decent performance for typical users. 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 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). 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. 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 +(aka quotient.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. @@ -360,7 +360,7 @@ 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. +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. Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for futures and automated testing, so PRs onboarding those will be considered with much gratitude. @@ -373,16 +373,15 @@ Some cases need additional explanation: from KDE Frameworks. We don't rule this out and there's no prejudice against KDE; it just so happened that KDE Frameworks is one of most obvious reuse candidates but so far none of these components survived - as libQMatrixClient deps. So we are cautious. Extra notice to KDE folks: - I'll be happy if an addon library on top of libQMatrixClient is made using + as libQuotient deps. So we are cautious. Extra notice to KDE folks: + I'll be happy if an addon library on top of libQuotient is made using KDE facilities, and I'm willing to take part in its evolution; but please also respect LXDE people who normally don't have KDE frameworks installed. -* Never forget that libQMatrixClient is aimed to be a non-visual library; +* 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 libQMatrixClient-enabled _applications_, this is likely to end up - in a separate (libQMatrixClient-enabled) library, rather than - libQMatrixClient itself. + between Quotient-enabled _applications_, this is likely to end up + in a separate (Quotient-enabled) library, rather than libQuotient itself. ## Attribution diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 64a80350..1e3f172b 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -13,7 +13,8 @@ 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. +Describe here the problem that you are experiencing, or the feature +you are requesting. ### Steps to reproduce @@ -23,17 +24,25 @@ Describe here the problem that you are experiencing, or the feature you are requ 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): +libQuotient-based clients either have a log file or dump log +to the standard output. If you can identify any log snippets relevant +to your issue, please include those here (please be careful to remove +any personal or private data): ### Version information -<!-- IMPORTANT: please answer the following questions, to help us narrow down the problem --> - -- **The client application**: <!-- the problem might be not with the library but with the client --> -- **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 --> +<!-- IMPORTANT: please answer the following questions, + to help us narrow down the problem --> + +- **The client application**: +<!-- the problem might be not with the library but with the client --> +- **libQuotient version if you know it**: +<!-- try to find it basing on the client version --> +- **Qt version**: +<!-- for Linux systems, it's usually installed system-wide; for other OSes, +as well as Flatpak/AppImage/etc. containerised environments, +it's a version used in the container. --> +- **Install method**: +<!-- package manager/Flatpak/archive downloaded (from which site?) --> +- **Platform**: +<!-- Operating system and anything about your platform you think can be relevant --> @@ -1,54 +1,57 @@ -# libQMatrixClient +# libQuotient (former libQMatrixClient) <a href='https://matrix.org'><img src='https://matrix.org/docs/projects/images/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a> -[![license](https://img.shields.io/github/license/QMatrixClient/libqmatrixclient.svg)](https://github.com/QMatrixClient/libqmatrixclient/blob/master/COPYING) +[![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) +[![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/QMatrixClient/libQMatrixClient.svg) +![](https://img.shields.io/github/commit-activity/y/quotient-im/libQuotient.svg) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Spectral](https://matrix.org/docs/projects/client/spectral.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), +[Spectral](https://matrix.org/docs/projects/client/spectral.html) 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 find Quotient developers in the Matrix room: +[#quotient:matrix.org](https://matrix.to/#/#quotient:matrix.org). -You can also file issues at -[the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues). +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. -## 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). After installing the library the CMake package becomes -available for `find_package(QMatrixClient)` to setup the client code with -the installed library files. PRs to enable the same for qmake are most welcome. - -[The source code is hosted at GitHub](https://github.com/QMatrixClient/libqmatrixclient) - -checking out a certain commit or tag (rather than downloading the archive) is -the recommended way for one-off building. If you want to hack on the library -as a part of another project (e.g. you are working on Quaternion but need -to do some changes to the library code), you're advised to make a recursive -check out of that project (in this case, Quaternion) and update -the library submodule to its master branch. - -Tags consisting of digits and periods represent released versions; tags ending with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases, it is advised to replace a dash with a tilde. +## Getting and using libQuotient +Depending on your platform, the library can come as a separate package. +Recent releases of Debian and OpenSuSE, e.g., already have the package +(under the old name). If your Linux repo doesn't provide binary package +(either libqmatrixclient - older - or libquotient - newer), or you're +on Windows or macOS, your best bet is to build the library from the source +and bundle it with your application. In ### Pre-requisites -- a Linux, macOS or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) - - For Ubuntu flavours - zesty or later (or a derivative) is good enough out of the box; older ones will need PPAs at least for a newer Qt; in particular, if you have xenial you're advised to add Kubuntu Backports PPA for it -- a Git client to check out this repo -- Qt 5 (either Open Source or Commercial), version 5.6 or higher - (5.9 or higher is strongly recommended) -- a build configuration tool: - - CMake (from your package management system or [the official website](https://cmake.org/download/)) +- A Linux, macOS or Windows system (desktop versions tried; Ubuntu Touch + is known to work; mobile Windows and iOS might work too but never tried) + - For Ubuntu flavours - zesty or later is good enough out of the box; + older ones will need PPAs at least for a newer Qt. In particular, + if you (still) have xenial and cannot upgrade to a newer release + you'll have to add Kubuntu Backports PPA for it. +- Qt 5 (either Open Source or Commercial), 5.9 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, macOS), Clang 5 (Linux), Apple Clang 8.1 (macOS) and Visual C++ 2015 (Windows) are the oldest officially supported; Clang 3.8 and GCC 4.9.2 are known to still work, maintenance patches for them are accepted - - any build system that works with CMake and/or qmake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. +- A C++ toolchain with C++14 support + - GCC 5 (Windows, Linux, macOS), Clang 5 (Linux), Apple Clang 8.1 (macOS) + and Visual Studio 2017 (Windows) are the oldest officially supported; + Clang 3.8, GCC 4.9.2, VS 2015 may work but not actively maintained. +- Any build system that works with CMake and/or qmake should be fine: + GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. #### Linux Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to run cmake/qmake and look at error messages. The library is entirely offscreen (QtCore and QtNetwork are essential) but it also depends on QtGui in order to handle avatar thumbnails. @@ -60,35 +63,73 @@ Just install things from the list above using your preferred package manager. If 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 macOS (see above), also helps. -There are no official MinGW-based 64-bit packages for Qt. If you're determined to build a 64-bit library, either use a Visual Studio toolchain or build Qt5 yourself as described in Qt documentation. - -### Building -#### CMake-based +### Using the library +If you use CMake, `find_package(Quotient)` sets up the client code to use +libQuotient, assuming the library development files are installed. There's no +documented procedure to use a preinstalled library with qmake; consider +introducing a submodule in your source tree and build it along with the rest +of the application for now. Patches to provide .prl files for qmake +are welcome. + +Building with dynamic linkage are only tested on Linux at the moment and are +a recommended way of linking your application with libQuotient on this platform. +Feel free +Static linkage is the default on Windows/macOS; feel free to experiment +with dynamic linking and submit PRs if you get reusable results. + +The example/test application that comes with libQuotient, +[qmc-example](https://github.com/quotient-im/libQuotient/tree/master/examples) +includes most common use cases such as sending messages, uploading files, +setting room state etc.; for more extensive usage check out the source code +of [Quaternion](https://github.com/quotient-im/Quaternion) +(the reference client of Quotient) or [Spectral](https://gitlab.com/b0/spectral). + +To ease the first step, `examples/CMakeLists.txt` is a good starting point +for your own CMake-based project using libQuotient. + +## 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) to its master branch. + +Tags consisting of digits and periods represent released versions; tags ending +with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases, +it is advised to replace a dash with a tilde. + +### CMake-based 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 --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/macOS are welcome. Taking a look at [qmc-example](https://github.com/QMatrixClient/libqmatrixclient/tree/master/examples) (used to test the library) should give you a basic idea of using libQMatrixClient; for more extensive usage check out the source code of [Quaternion](https://github.com/QMatrixClient/Quaternion) (the reference client built on QMatrixClient). +This will get you the compiled library in `build_dir` inside your project +sources. Static builds are tested on all supported platforms. 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. -Installation of `qmc-example` application can be skipped by setting `QMATRIXCLIENT_INSTALL_EXAMPLE` to `OFF`. +This will also install cmake package config files; once this is done, you +should be able to use `examples/CMakeLists.txt` to compile qmc-example +with the _installed_ library. Installation of the `qmc-example` binary +along with the rest of the library can be skipped +by setting `QMATRIXCLIENT_INSTALL_EXAMPLE` to `OFF`. -#### qmake-based +### 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: -``` +```shell script 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. +Installing the standalone library with qmake is not implemented yet. ## Troubleshooting @@ -105,19 +146,33 @@ CMake Warning at CMakeLists.txt:11 (find_package): #### 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`) +- `<category>` is one of: `main`, `jobs`, `jobs.sync`, `events`, `events.ephemeral`, and `profiler` (you can always find the full list in the file `lib/logging.cpp`) - `<level>` is one of `debug` 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 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). diff --git a/cmake/QMatrixClientConfig.cmake b/cmake/QMatrixClientConfig.cmake index 900038a5..64180cca 100644 --- a/cmake/QMatrixClientConfig.cmake +++ b/cmake/QMatrixClientConfig.cmake @@ -1 +1,4 @@ +include(CMakeFindDependencyMacro) + +find_dependency(QtOlm) include("${CMAKE_CURRENT_LIST_DIR}/QMatrixClientTargets.cmake") diff --git a/lib/connection.cpp b/lib/connection.cpp index 43681d12..52a693f7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -19,6 +19,7 @@ #include "connection.h" #include "connectiondata.h" +#include "encryptionmanager.h" #include "room.h" #include "settings.h" #include "user.h" @@ -83,8 +84,9 @@ public: // separately; specifically, we should keep objects for Invite and // Leave state of the same room if the two happen to co-exist. QHash<QPair<QString, bool>, Room*> roomMap; - // Mapping from aliases to room ids, as per the last sync - QHash<QString, QString> roomAliasMap; + /// Mapping from serverparts to alias/room id mappings, + /// as of the last sync + QHash<QString, QHash<QString, QString>> roomAliasMap; QVector<QString> roomIdsToForget; QVector<Room*> firstTimeRooms; QVector<QString> pendingStateRoomIds; @@ -103,6 +105,8 @@ public: GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; + QScopedPointer<EncryptionManager> encryptionManager; + SyncJob* syncJob = nullptr; bool cacheState = true; @@ -113,6 +117,7 @@ public: void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); + void removeRoom(const QString& roomId); template <typename EventT> EventT* unpackAccountData() const @@ -160,22 +165,11 @@ Connection::~Connection() stopSync(); } -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. - - // 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()) { + if (maybeBaseUrl.isEmpty() || !maybeBaseUrl.isValid()) { emit resolveError(tr("%1 is not a valid homeserver address") .arg(maybeBaseUrl.toString())); return; @@ -234,6 +228,17 @@ void Connection::doConnectToServer(const QString& user, const QString& password, connect(loginJob, &BaseJob::success, this, [this, loginJob] { d->connectWithToken(loginJob->userId(), loginJob->accessToken(), loginJob->deviceId()); + + AccountSettings accountSettings(loginJob->userId()); + d->encryptionManager.reset( + new EncryptionManager(accountSettings.encryptionAccountPickle())); + if (accountSettings.encryptionAccountPickle().isEmpty()) { + accountSettings.setEncryptionAccountPickle( + d->encryptionManager->olmAccountPickle()); + } + + d->encryptionManager->uploadIdentityKeys(this); + d->encryptionManager->uploadOneTimeKeys(this); }); connect(loginJob, &BaseJob::failure, this, [this, loginJob] { emit loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -763,25 +768,33 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) 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()); - }); + connect(leaveJob, &BaseJob::result, this, + [this, leaveJob, forgetJob, room] { + if (leaveJob->error() == BaseJob::Success + || leaveJob->error() == BaseJob::NotFoundError) { + 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()); + } 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(); - } + connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] { + // Leave room in case of success, or room not known by server + if (forgetJob->error() == BaseJob::Success + || forgetJob->error() == BaseJob::NotFoundError) + d->removeRoom(id); // Delete the room from roomMap + else + qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " + << forgetJob->errorString(); }); return forgetJob; } @@ -841,32 +854,34 @@ Room* Connection::room(const QString& roomId, JoinStates states) const Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const { - const auto id = d->roomAliasMap.value(roomAlias); + const auto id = d->roomAliasMap.value(serverPart(roomAlias)).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 QString& aliasServer, const QStringList& previousRoomAliases, const QStringList& roomAliases) { + auto& aliasMap = d->roomAliasMap[aliasServer]; // Allocate if necessary for (const auto& a : previousRoomAliases) - if (d->roomAliasMap.remove(a) == 0) + if (aliasMap.remove(a) == 0) qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; for (const auto& a : roomAliases) { - auto& mappedId = d->roomAliasMap[a]; + auto& mappedId = aliasMap[a]; if (!mappedId.isEmpty()) { if (mappedId == roomId) qCDebug(MAIN) - << "Alias" << a << "is already mapped to room" << roomId; + << "Alias" << a << "is already mapped to" << roomId; else - qCWarning(MAIN) - << "Alias" << a << "will be force-remapped from room" - << mappedId << "to" << roomId; + qCWarning(MAIN) << "Alias" << a << "will be force-remapped from" + << mappedId << "to" << roomId; } mappedId = roomId; } @@ -904,8 +919,6 @@ QString Connection::userId() const { return d->userId; } QString Connection::deviceId() const { return d->data->deviceId(); } -QString Connection::token() const { return accessToken(); } - QByteArray Connection::accessToken() const { return d->data->accessToken(); } SyncJob* Connection::syncJob() const { return d->syncJob; } @@ -998,6 +1011,18 @@ Connection::DirectChatsMap Connection::directChats() const return d->directChats; } +// Removes room with given id from roomMap +void Connection::Private::removeRoom(const QString& roomId) +{ + for (auto f : { false, true }) + if (auto r = roomMap.take({ roomId, f })) { + qCDebug(MAIN) << "Room" << r->objectName() << "in state" + << toCString(r->joinState()) << "will be deleted"; + emit r->beforeDestruction(r); + r->deleteLater(); + } +} + void Connection::addToDirectChats(const Room* room, User* user) { Q_ASSERT(room != nullptr && user != nullptr); diff --git a/lib/connection.h b/lib/connection.h index a13def10..ef6cc156 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -132,7 +132,7 @@ public: explicit Connection(QObject* parent = nullptr); explicit Connection(const QUrl& server, QObject* parent = nullptr); - virtual ~Connection(); + ~Connection() override; /** Get all Invited and Joined rooms * \return a hashmap from a composite key - room name and whether @@ -261,9 +261,10 @@ public: JoinStates states = JoinState::Invite | JoinState::Join) const; /** Update the internal map of room aliases to IDs */ - /// This is used for internal bookkeeping of rooms. Do NOT use - /// it to try change aliases, use Room::setAliases instead - void updateRoomAliases(const QString& roomId, + /// 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 QString& aliasServer, const QStringList& previousRoomAliases, const QStringList& roomAliases); Q_INVOKABLE Room* invitation(const QString& roomId) const; @@ -276,7 +277,6 @@ public: Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; - [[deprecated("Use accessToken() instead")]] Q_INVOKABLE QString token() const; Q_INVOKABLE void getTurnServers(); struct SupportedRoomVersion @@ -422,8 +422,8 @@ 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); + /** Determine and set the homeserver from MXID */ + void resolveServer(const QString& mxid); void connectToServer(const QString& user, const QString& password, const QString& initialDeviceName, diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp new file mode 100644 index 00000000..61fcf9b4 --- /dev/null +++ b/lib/encryptionmanager.cpp @@ -0,0 +1,228 @@ +#include "encryptionmanager.h" + +#include "connection.h" + +#include "csapi/keys.h" + +#include <QtCore/QHash> +#include <QtCore/QStringBuilder> + +#include <account.h> // QtOlm +#include <functional> +#include <memory> + +using namespace QMatrixClient; +using namespace QtOlm; +using std::move; + +static const auto ed25519Name = QStringLiteral("ed25519"); +static const auto Curve25519Name = QStringLiteral("curve25519"); +static const auto SignedCurve25519Name = QStringLiteral("signed_curve25519"); +static const auto OlmV1Curve25519AesSha2AlgoName = + QStringLiteral("m.olm.v1.curve25519-aes-sha2"); +static const auto MegolmV1AesSha2AlgoName = + QStringLiteral("m.megolm.v1.aes-sha2"); +static const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoName, + MegolmV1AesSha2AlgoName }; + +class EncryptionManager::Private +{ +public: + explicit Private(const QByteArray& encryptionAccountPickle, + float signedKeysProportion, float oneTimeKeyThreshold) + : signedKeysProportion(move(signedKeysProportion)) + , oneTimeKeyThreshold(move(oneTimeKeyThreshold)) + { + Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); + Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); + if (encryptionAccountPickle.isEmpty()) { + olmAccount.reset(new Account()); + } else { + olmAccount.reset( + new Account(encryptionAccountPickle)); // TODO: passphrase even + // with qtkeychain? + } + /* + * Note about targetKeysNumber: + * + * From: https://github.com/Zil0/matrix-python-sdk/ + * File: matrix_client/crypto/olm_device.py + * + * Try to maintain half the number of one-time keys libolm can hold + * uploaded on the HS. This is because some keys will be claimed by + * peers but not used instantly, and we want them to stay in libolm, + * until the limit is reached and it starts discarding keys, starting by + * the oldest. + */ + targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below + targetOneTimeKeyCounts = { + { SignedCurve25519Name, + qRound(signedKeysProportion * targetKeysNumber) }, + { Curve25519Name, + qRound((1 - signedKeysProportion) * targetKeysNumber) } + }; + } + ~Private() = default; + + UploadKeysJob* uploadIdentityKeysJob = nullptr; + UploadKeysJob* uploadOneTimeKeysJob = nullptr; + + QScopedPointer<Account> olmAccount; + + float signedKeysProportion; + float oneTimeKeyThreshold; + int targetKeysNumber; + + void updateKeysToUpload(); + bool oneTimeKeyShouldUpload(); + + QHash<QString, int> oneTimeKeyCounts; + void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue) + { + oneTimeKeyCounts = oneTimeKeyCountsNewValue; + updateKeysToUpload(); + } + QHash<QString, int> oneTimeKeysToUploadCounts; + QHash<QString, int> targetOneTimeKeyCounts; +}; + +EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, + float signedKeysProportion, + float oneTimeKeyThreshold, QObject* parent) + : QObject(parent) + , d(std::make_unique<Private>(std::move(encryptionAccountPickle), + std::move(signedKeysProportion), + std::move(oneTimeKeyThreshold))) +{} + +EncryptionManager::~EncryptionManager() = default; + +void EncryptionManager::uploadIdentityKeys(Connection* connection) +{ + // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload + DeviceKeys deviceKeys { + /* + * The ID of the user the device belongs to. Must match the user ID used + * when logging in. The ID of the device these keys belong to. Must + * match the device ID used when logging in. The encryption algorithms + * supported by this device. + */ + connection->userId(), + connection->deviceId(), + SupportedAlgorithms, + /* + * 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. + */ + { { Curve25519Name + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->curve25519IdentityKey() }, + { ed25519Name + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->ed25519IdentityKey() } }, + /* signatures should be provided after the unsigned deviceKeys + generation */ + {} + }; + + QJsonObject deviceKeysJsonObject = toJson(deviceKeys); + /* additionally removing signatures key, + * since we could not initialize deviceKeys + * without an empty signatures value: + */ + deviceKeysJsonObject.remove(QStringLiteral("signatures")); + /* + * 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 called Signing + * JSON. + */ + deviceKeys.signatures = { + { connection->userId(), + { { ed25519Name + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->sign(deviceKeysJsonObject) } } } + }; + + connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); + qDebug() << QString("Uploaded identity keys."); + }); + d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys); +} + +void EncryptionManager::uploadOneTimeKeys(Connection* connection, + bool forceUpdate) +{ + if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { + auto job = connection->callApi<UploadKeysJob>(); + connect(job, &BaseJob::success, this, [job, this] { + d->setOneTimeKeyCounts(job->oneTimeKeyCounts()); + }); + } + + int signedKeysToUploadCount = + d->oneTimeKeysToUploadCounts.value(SignedCurve25519Name, 0); + int unsignedKeysToUploadCount = + d->oneTimeKeysToUploadCounts.value(Curve25519Name, 0); + + d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount + + unsignedKeysToUploadCount); + + QHash<QString, QVariant> oneTimeKeys = {}; + const auto& olmAccountCurve25519OneTimeKeys = + d->olmAccount->curve25519OneTimeKeys(); + + int oneTimeKeysCounter = 0; + for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); + it != olmAccountCurve25519OneTimeKeys.cend(); ++it) { + QString keyId = it.key(); + QString keyType; + QVariant key; + if (oneTimeKeysCounter < signedKeysToUploadCount) { + QJsonObject message { { QStringLiteral("key"), + it.value().toString() } }; + key = d->olmAccount->sign(message); + keyType = SignedCurve25519Name; + + } else { + key = it.value(); + keyType = Curve25519Name; + } + ++oneTimeKeysCounter; + oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); + } + + d->uploadOneTimeKeysJob = connection->callApi<UploadKeysJob>(none, + oneTimeKeys); + d->olmAccount->markKeysAsPublished(); + qDebug() << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") + .arg(signedKeysToUploadCount) + .arg(unsignedKeysToUploadCount); +} + +QByteArray EncryptionManager::olmAccountPickle() +{ + return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? +} + +void EncryptionManager::Private::updateKeysToUpload() +{ + for (auto it = targetOneTimeKeyCounts.cbegin(); + it != targetOneTimeKeyCounts.cend(); ++it) { + int numKeys = oneTimeKeyCounts.value(it.key(), 0); + int numToCreate = qMax(it.value() - numKeys, 0); + oneTimeKeysToUploadCounts.insert(it.key(), numToCreate); + } +} + +bool EncryptionManager::Private::oneTimeKeyShouldUpload() +{ + if (oneTimeKeyCounts.empty()) + return true; + for (auto it = targetOneTimeKeyCounts.cbegin(); + it != targetOneTimeKeyCounts.cend(); ++it) { + if (oneTimeKeyCounts.value(it.key(), 0) + < it.value() * oneTimeKeyThreshold) + return true; + } + return false; +} diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h new file mode 100644 index 00000000..a41d88e1 --- /dev/null +++ b/lib/encryptionmanager.h @@ -0,0 +1,34 @@ +#pragma once + +#include <QtCore/QObject> + +#include <functional> +#include <memory> + +namespace QMatrixClient +{ +class Connection; + +class EncryptionManager : public QObject +{ + Q_OBJECT + +public: + // TODO: store constats separately? + // TODO: 0.5 oneTimeKeyThreshold instead of 0.1? + explicit EncryptionManager( + const QByteArray& encryptionAccountPickle = QByteArray(), + float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1), + QObject* parent = nullptr); + ~EncryptionManager(); + + void uploadIdentityKeys(Connection* connection); + void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); + QByteArray olmAccountPickle(); + +private: + class Private; + std::unique_ptr<Private> d; +}; + +} // namespace QMatrixClient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp new file mode 100644 index 00000000..6aa7063b --- /dev/null +++ b/lib/events/encryptionevent.cpp @@ -0,0 +1,54 @@ +// +// Created by rusakov on 26/09/2017. +// Contributed by andreev on 27/06/2019. +// + +#include "encryptionevent.h" + +#include "converters.h" +#include "logging.h" + +#include <array> + +static const std::array<QString, 1> encryptionStrings = { { QStringLiteral( + "m.megolm.v1.aes-sha2") } }; + +namespace QMatrixClient +{ +template <> +struct JsonConverter<EncryptionType> +{ + static EncryptionType load(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()); + + qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; + return EncryptionType::Undefined; + } +}; +} // namespace QMatrixClient + +using namespace QMatrixClient; + +EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) + : encryption(fromJson<EncryptionType>(json["algorithm"_ls])) + , algorithm(sanitized(json["algorithm"_ls].toString())) + , rotationPeriodMs(json["rotation_period_ms"_ls].toInt(604800000)) + , rotationPeriodMsgs(json["rotation_period_msgs"_ls].toInt(100)) +{} + +void EncryptionEventContent::fillJson(QJsonObject* o) const +{ + Q_ASSERT(o); + Q_ASSERT_X( + encryption != EncryptionType::Undefined, __FUNCTION__, + "The key 'algorithm' must be explicit in EncryptionEventContent"); + if (encryption != EncryptionType::Undefined) + o->insert(QStringLiteral("algorithm"), algorithm); + o->insert(QStringLiteral("rotation_period_ms"), rotationPeriodMs); + o->insert(QStringLiteral("rotation_period_msgs"), rotationPeriodMsgs); +} diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h new file mode 100644 index 00000000..97119c8d --- /dev/null +++ b/lib/events/encryptionevent.h @@ -0,0 +1,81 @@ +/****************************************************************************** + * 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 "eventcontent.h" +#include "stateevent.h" + +namespace QMatrixClient +{ +class EncryptionEventContent : public EventContent::Base +{ +public: + enum EncryptionType : size_t + { + MegolmV1AesSha2 = 0, + Undefined + }; + + explicit EncryptionEventContent(EncryptionType et = Undefined) + : encryption(et) + {} + explicit EncryptionEventContent(const QJsonObject& json); + + EncryptionType encryption; + QString algorithm; + int rotationPeriodMs; + int rotationPeriodMsgs; + +protected: + void fillJson(QJsonObject* o) const override; +}; + +using EncryptionType = EncryptionEventContent::EncryptionType; + +class EncryptionEvent : public StateEvent<EncryptionEventContent> +{ + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) + + using EncryptionType = EncryptionEventContent::EncryptionType; + + explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate + // default value + : StateEvent(typeId(), obj) + {} + template <typename... ArgTs> + EncryptionEvent(ArgTs&&... contentArgs) + : StateEvent(typeId(), matrixTypeId(), QString(), + std::forward<ArgTs>(contentArgs)...) + {} + + EncryptionType encryption() const { return content().encryption; } + + QString algorithm() const { return content().algorithm; } + int rotationPeriodMs() const { return content().rotationPeriodMs; } + int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } + +private: + REGISTER_ENUM(EncryptionType) +}; + +REGISTER_EVENT_TYPE(EncryptionEvent) +DEFINE_EVENTTYPE_ALIAS(Encryption, EncryptionEvent) +} // namespace QMatrixClient diff --git a/lib/events/event.h b/lib/events/event.h index 9dcec1ae..8056ccbe 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -61,14 +61,16 @@ 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 StateKeyKey = QStringLiteral("state_key"); 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; +static const auto StateKeyKeyL = "state_key"_ls; -// Minimal correct Matrix event JSON +/// Make a minimal correct Matrix event JSON template <typename StrT> inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content) { @@ -257,7 +259,7 @@ public: } template <typename T> - T content(const QLatin1String& key) const + T content(QLatin1String key) const { return fromJson<T>(contentJson()[key]); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index d2b5e477..7a3db1fc 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -55,6 +55,9 @@ namespace EventContent QJsonObject originalJson; protected: + Base(const Base&) = default; + Base(Base&&) = default; + virtual void fillJson(QJsonObject* o) const = 0; }; @@ -172,13 +175,16 @@ namespace EventContent class TypedBase : public Base { public: - explicit TypedBase(const QJsonObject& o = {}) - : Base(o) + explicit TypedBase(QJsonObject o = {}) + : Base(std::move(o)) {} virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } + + protected: + using Base::Base; }; /** diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 9c797701..a203eaa3 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -34,7 +34,8 @@ namespace _impl } } // namespace _impl -/** Create an event with proper type from a JSON object +/*! 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. @@ -45,7 +46,8 @@ 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 +/*! 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. @@ -58,6 +60,20 @@ inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, matrixType); } +/*! Create a state event from a type string, content JSON and state key + * + * Use this factory to resolve the C++ type from the Matrix type string + * in \p matrixType and create a state event of that type with content part + * set to \p content and state key set to \p stateKey (empty by default). + */ +inline StateEventPtr loadStateEvent(const QString& matrixType, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return _impl::loadEvent<StateEventBase>( + basicStateEventJson(matrixType, content, stateKey), matrixType); +} + template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>> { diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index c28de559..513a99d0 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -74,7 +74,17 @@ QString RoomEvent::transactionId() const QString RoomEvent::stateKey() const { - return fullJson()["state_key"_ls].toString(); + return fullJson()[StateKeyKeyL].toString(); +} + +void RoomEvent::setRoomId(const QString& roomId) +{ + editJson().insert(QStringLiteral("room_id"), roomId); +} + +void RoomEvent::setSender(const QString& senderId) +{ + editJson().insert(QStringLiteral("sender"), senderId); } void RoomEvent::setTransactionId(const QString& txnId) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 42cd8fe4..dd0d25eb 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -60,6 +60,9 @@ public: QString transactionId() const; QString stateKey() const; + void setRoomId(const QString& roomId); + void setSender(const QString& senderId); + /** * Sets the transaction id for locally created events. This should be * done before the event is exposed to any code using the respective diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index a837b026..c1015df2 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -63,8 +63,14 @@ public: explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c) + [[deprecated("Use RoomMemberEvent(userId, contentArgs) " + "instead")]] RoomMemberEvent(MemberEventContent&& c) + : StateEvent(typeId(), matrixTypeId(), QString(), c) + {} + template <typename... ArgTs> + RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) + : StateEvent(typeId(), matrixTypeId(), userId, + std::forward<ArgTs>(contentArgs)...) {} /// A special constructor to create unknown RoomMemberEvents @@ -82,7 +88,7 @@ public: {} MembershipType membership() const { return content().membership; } - QString userId() const { return fullJson()["state_key"_ls].toString(); } + QString userId() const { return fullJson()[StateKeyKeyL].toString(); } bool isDirect() const { return content().isDirect; } QString displayName() const { return content().displayName; } QUrl avatarUrl() const { return content().avatarUrl; } diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 7ad2efa6..0078c44d 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -18,7 +18,6 @@ #pragma once -#include "converters.h" #include "stateevent.h" namespace QMatrixClient @@ -65,7 +64,7 @@ namespace EventContent {} \ template <typename T> \ explicit _Name(T&& value) \ - : StateEvent(typeId(), matrixTypeId(), \ + : StateEvent(typeId(), matrixTypeId(), QString(), \ QStringLiteral(#_ContentKey), std::forward<T>(value)) \ {} \ explicit _Name(QJsonObject obj) \ @@ -79,15 +78,27 @@ namespace EventContent 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) + +class RoomAliasesEvent + : public StateEvent<EventContent::SimpleContent<QStringList>> +{ +public: + DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) + explicit RoomAliasesEvent(const QJsonObject& obj) + : StateEvent(typeId(), obj, QStringLiteral("aliases")) + {} + RoomAliasesEvent(const QString& server, const QStringList& aliases) + : StateEvent(typeId(), matrixTypeId(), server, + QStringLiteral("aliases"), aliases) + {} + QString server() const { return stateKey(); } + QStringList aliases() const { return content().value; } +}; +REGISTER_EVENT_TYPE(RoomAliasesEvent) } // namespace QMatrixClient diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 7fea59a1..bd228abd 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -26,7 +26,7 @@ using namespace QMatrixClient; [[gnu::unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains("state_key"_ls)) + if (!json.contains(StateKeyKeyL)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) @@ -35,6 +35,12 @@ using namespace QMatrixClient; return makeEvent<StateEventBase>(unknownEventTypeId(), json); }); +StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, + const QString& stateKey, + const QJsonObject& contentJson) + : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey)) +{} + bool StateEventBase::repeatsState() const { const auto prevContentJson = unsignedJson().value(PrevContentKeyL); diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 8a89c86c..d1b742ba 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -22,12 +22,29 @@ namespace QMatrixClient { + +/// Make a minimal correct Matrix state event JSON +template <typename StrT> +inline QJsonObject basicStateEventJson(StrT matrixType, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return { { TypeKey, std::forward<StrT>(matrixType) }, + { StateKeyKey, stateKey }, + { ContentKey, content } }; +} + class StateEventBase : public RoomEvent { public: using factory_t = EventFactory<StateEventBase>; - using RoomEvent::RoomEvent; + StateEventBase(Type type, const QJsonObject& json) + : RoomEvent(type, json) + {} + StateEventBase(Type type, event_mtype_t matrixType, + const QString& stateKey = {}, + const QJsonObject& contentJson = {}); ~StateEventBase() override = default; bool isStateEvent() const override { return true; } @@ -87,8 +104,9 @@ public: } template <typename... ContentParamTs> explicit StateEvent(Type type, event_mtype_t matrixType, + const QString& stateKey, ContentParamTs&&... contentParams) - : StateEventBase(type, matrixType) + : StateEventBase(type, matrixType, stateKey) , _content(std::forward<ContentParamTs>(contentParams)...) { editJson().insert(ContentKey, _content.toJson()); diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 9c0b431c..f46d2d61 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -275,51 +275,23 @@ void BaseJob::gotReply() 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 (errCode == "M_UNSUPPORTED_ROOM_VERSION" - || errCode == "M_INCOMPATIBLE_ROOM_VERSION") { - d->status.code = UnsupportedRoomVersionError; - if (json.contains("room_version")) - d->status.message = - tr("Requested room version: %1") - .arg(json.value("room_version").toString()); - } else if (!json.isEmpty()) // Not localisable on the client side - setStatus(d->status.code, json.value("error"_ls).toString()); - } + if (jsonBody) + setStatus( + parseError(d->reply.data(), + QJsonDocument::fromJson(d->rawResponse).object())); } - finishJob(); + if (error() != TooManyRequestsError) + finishJob(); + else { + stop(); + emit retryScheduled(d->retriesTaken, d->retryTimer.interval()); + } } bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) @@ -348,34 +320,6 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) return false; } -BaseJob::Status BaseJob::Status::fromHttpCode(int httpCode, QString msg) -{ - // clang-format off - return { [httpCode]() -> StatusCode { - if (httpCode / 10 == 41) // 41x errors - 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; - } - }(), msg }; - // clang-format on -} - BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const { // QNetworkReply error codes seem to be flawed when it comes to HTTP; @@ -409,7 +353,38 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const qCWarning(d->logCat).noquote().nospace() << this << urlString; qCWarning(d->logCat).noquote() << " " << httpCode << reason << replyState; - return Status::fromHttpCode(httpCode, reply->errorString()); + 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() }; } BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) @@ -425,8 +400,46 @@ BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) BaseJob::Status BaseJob::parseJson(const QJsonDocument&) { return Success; } +BaseJob::Status BaseJob::parseError(QNetworkReply* reply, + const QJsonObject& errorJson) +{ + const auto errCode = errorJson.value("errcode"_ls).toString(); + if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") { + QString msg = tr("Too many requests"); + auto retryInterval = errorJson.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(); + + qCWarning(d->logCat) << this << "will retry in" << retryInterval << "ms"; + d->retryTimer.start(retryInterval); + + return { TooManyRequestsError, msg }; + } + if (errCode == "M_CONSENT_NOT_GIVEN") { + d->errorUrl = errorJson.value("consent_uri"_ls).toString(); + return { UserConsentRequiredError }; + } + if (errCode == "M_UNSUPPORTED_ROOM_VERSION" + || errCode == "M_INCOMPATIBLE_ROOM_VERSION") + return { UnsupportedRoomVersionError, + errorJson.contains("room_version"_ls) + ? tr("Requested room version: %1") + .arg(errorJson.value("room_version"_ls).toString()) + : errorJson.value("error"_ls).toString() }; + + // Not localisable on the client side + if (errorJson.contains("error"_ls)) + d->status.message = errorJson.value("error"_ls).toString(); + + return d->status; +} + void BaseJob::stop() { + // This method is 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) { d->reply->disconnect(this); // Ignore whatever comes from the reply diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 4d379f26..d94ab31d 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -116,8 +116,9 @@ public: * along the lines of StatusCode, with additional values * starting at UserDefinedError */ - struct Status + class Status { + public: Status(StatusCode c) : code(c) {} @@ -125,7 +126,6 @@ public: : code(c) , message(std::move(m)) {} - static Status fromHttpCode(int httpCode, QString msg = {}); bool good() const { return code < ErrorLevel; } friend QDebug operator<<(QDebug dbg, const Status& s) @@ -326,8 +326,7 @@ protected: * 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) + * @param reply raw contents of a HTTP reply from the server * * @see gotReply, parseJson */ @@ -335,7 +334,7 @@ protected: /** * Processes the JSON document received from the Matrix server. - * By default returns succesful status without analysing the JSON. + * By default returns successful status without analysing the JSON. * * @param json valid JSON document received from the server * @@ -343,6 +342,15 @@ protected: */ virtual Status parseJson(const QJsonDocument&); + /** + * Processes the reply in case of unsuccessful HTTP code. + * The body is already loaded from the reply object to errorJson. + * @param reply the HTTP reply from the server + * @param errorJson the JSON payload describing the error + */ + virtual Status parseError(QNetworkReply* reply, + const QJsonObject& errorJson); + void setStatus(Status s); void setStatus(int code, QString message); diff --git a/lib/joinstate.h b/lib/joinstate.h index f7c0cb2b..e4dc679a 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -41,7 +41,7 @@ static const std::array<const char*, 3> JoinStateStrings { { "join", "invite", inline const char* toCString(JoinState js) { size_t state = size_t(js), index = 0; - while (state >>= 1) + while (state >>= 1u) ++index; return JoinStateStrings[index]; } diff --git a/lib/logging.cpp b/lib/logging.cpp index 73cc59a1..a7676c97 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -26,9 +26,9 @@ #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(PROFILER, "quotient.profiler") +LOGGING_CATEGORY(EVENTS, "quotient.events") +LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") +LOGGING_CATEGORY(JOBS, "quotient.jobs") +LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") diff --git a/lib/room.cpp b/lib/room.cpp index ec2a34ef..045cc7bc 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -41,6 +41,7 @@ #include "events/callcandidatesevent.h" #include "events/callhangupevent.h" #include "events/callinviteevent.h" +#include "events/encryptionevent.h" #include "events/receiptevent.h" #include "events/redactionevent.h" #include "events/roomavatarevent.h" @@ -101,12 +102,19 @@ public: /// The state of the room at timeline position before-0 /// \sa timelineBase std::unordered_map<StateEventKey, StateEventPtr> baseState; + /// State event stubs - events without content, just type and state key + static decltype(baseState) stubbedState; /// The state of the room at timeline position after-maxTimelineIndex() /// \sa Room::syncEdge QHash<StateEventKey, const StateEventBase*> 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; + QString displayname; Avatar avatar; int highlightCount = 0; @@ -198,9 +206,22 @@ public: template <typename EventT> const EventT* getCurrentState(const QString& stateKey = {}) const { - static const EventT empty; - const auto* evt = - currentState.value({ EventT::matrixTypeId(), stateKey }, &empty); + const StateEventKey evtKey { EventT::matrixTypeId(), stateKey }; + 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, + loadStateEvent(EventT::matrixTypeId(), {}, + stateKey)); + qCDebug(MAIN) << "A new stub event created for key {" + << evtKey.first << evtKey.second << "}"; + } + evt = stubbedState[evtKey].get(); + Q_ASSERT(evt); + } Q_ASSERT(evt->type() == EventT::typeId() && evt->matrixType() == EventT::matrixTypeId()); return static_cast<const EventT*>(evt); @@ -275,24 +296,22 @@ public: QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); - template <typename EvT> - SetRoomStateWithKeyJob* requestSetState(const QString& stateKey, - const EvT& event) + SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event) { - if (q->successorId().isEmpty()) { - // TODO: Queue up state events sending (see #133). - return connection->callApi<SetRoomStateWithKeyJob>( - id, EvT::matrixTypeId(), stateKey, event.contentJson()); - } - qCWarning(MAIN) << q << "has been upgraded, state won't be set"; - return nullptr; + // 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, event.matrixType(), event.stateKey(), event.contentJson()); } - template <typename EvT> - auto requestSetState(const EvT& event) + template <typename EvT, typename... ArgTs> + auto requestSetState(ArgTs&&... args) { - return connection->callApi<SetRoomStateJob>(id, EvT::matrixTypeId(), - event.contentJson()); + return requestSetState(EvT(std::forward<ArgTs>(args)...)); } /** @@ -316,6 +335,8 @@ private: bool isLocalUser(const User* u) const { return u == q->localUser(); } }; +decltype(Room::Private::baseState) Room::Private::stubbedState {}; + Room::Room(Connection* connection, QString id, JoinState initialJoinState) : QObject(connection) , d(new Private(connection, id, initialJoinState)) @@ -376,9 +397,20 @@ QString Room::name() const return d->getCurrentState<RoomNameEvent>()->name(); } -QStringList Room::aliases() const +QStringList Room::localAliases() const +{ + return d + ->getCurrentState<RoomAliasesEvent>( + connection()->homeserver().authority()) + ->aliases(); +} + +QStringList Room::remoteAliases() const { - return d->getCurrentState<RoomAliasesEvent>()->aliases(); + QStringList result; + for (const auto& s : d->aliasServers) + result += d->getCurrentState<RoomAliasesEvent>(s)->aliases(); + return result; } QString Room::canonicalAlias() const @@ -1279,6 +1311,10 @@ 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(pEvent); unsyncedEvents.emplace_back(move(event)); @@ -1288,6 +1324,11 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { + if (q->usesEncryption()) { + qCCritical(MAIN) << "Room" << q->objectName() + << "enforces encryption; sending encrypted messages " + "is not supported yet"; + } if (q->successorId().isEmpty()) return doSendEvent(addAsPending(std::move(event))); @@ -1484,39 +1525,39 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, 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 StateEventBase& evt) const +{ + return d->requestSetState(evt); } void Room::setName(const QString& newName) { - d->requestSetState(RoomNameEvent(newName)); + d->requestSetState<RoomNameEvent>(newName); } void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState(RoomCanonicalAliasEvent(newAlias)); + d->requestSetState<RoomCanonicalAliasEvent>(newAlias); } -void Room::setAliases(const QStringList& aliases) +void Room::setLocalAliases(const QStringList& aliases) { - d->requestSetState(RoomAliasesEvent(aliases)); + d->requestSetState<RoomAliasesEvent>(connection()->homeserver().authority(), + aliases); } void Room::setTopic(const QString& newTopic) { - d->requestSetState(RoomTopicEvent(newTopic)); + d->requestSetState<RoomTopicEvent>(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) @@ -1561,33 +1602,33 @@ 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, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallAnswerEvent(callId, lifetime, sdp)); + d->sendEvent<CallAnswerEvent>(callId, lifetime, 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) { d->getPreviousContent(limit); } @@ -1622,7 +1663,7 @@ LeaveRoomJob* Room::leaveRoom() SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const { - return d->requestSetState(memberId, event); + return d->requestSetState<RoomMemberEvent>(memberId, event.content()); } void Room::kickMember(const QString& memberId, const QString& reason) @@ -1780,7 +1821,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, TypeKey, QStringLiteral("room_id"), QStringLiteral("sender"), - QStringLiteral("state_key"), + StateKeyKey, QStringLiteral("prev_content"), ContentKey, QStringLiteral("hashes"), @@ -2063,27 +2104,47 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!is<RoomMemberEvent>(e)) // Room member events are too numerous qCDebug(EVENTS) << "Room state event:" << e; - return visit( - e, [](const RoomNameEvent&) { return NameChange; }, - [this, oldStateEvent](const RoomAliasesEvent& ae) { + // clang-format off + return visit(e + , [] (const RoomNameEvent&) { + return NameChange; + } + , [this,oldStateEvent] (const RoomAliasesEvent& ae) { + // clang-format on + if (ae.aliases().isEmpty()) { + qDebug(MAIN).noquote() + << ae.stateKey() << "no more has aliases for room" + << objectName(); + d->aliasServers.remove(ae.stateKey()); + } else { + d->aliasServers.insert(ae.stateKey()); + qDebug(MAIN).nospace().noquote() + << "New server with aliases for room " << objectName() + << ": " << ae.stateKey(); + } const auto previousAliases = oldStateEvent ? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases() : QStringList(); - connection()->updateRoomAliases(id(), previousAliases, ae.aliases()); + connection()->updateRoomAliases(id(), ae.stateKey(), + previousAliases, ae.aliases()); return OtherChange; - }, - [this](const RoomCanonicalAliasEvent& evt) { + // clang-format off + } + , [this] (const RoomCanonicalAliasEvent& evt) { setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); return CanonicalAliasChange; - }, - [](const RoomTopicEvent&) { return TopicChange; }, - [this](const RoomAvatarEvent& evt) { + } + , [] (const RoomTopicEvent&) { + return TopicChange; + } + , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) emit avatarChanged(); return AvatarChange; - }, - [this, oldStateEvent](const RoomMemberEvent& evt) { + } + , [this,oldStateEvent] (const RoomMemberEvent& evt) { + // clang-format on auto* u = user(evt.userId()); const auto* oldMemberEvent = static_cast<const RoomMemberEvent*>(oldStateEvent); @@ -2150,27 +2211,30 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) d->membersLeft.append(u); } return MembersChange; - }, - [this](const EncryptionEvent&) { + // clang-format off + } + , [this] (const EncryptionEvent&) { emit encryption(); // It can only be done once, so emit it here. return OtherChange; - }, - [this](const RoomTombstoneEvent& evt) { + } + , [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; - }); + [this,successorId,serverMsg=evt.serverMessage()] + (Room* newRoom) { + if (newRoom->id() != successorId) + return false; + emit upgraded(serverMsg, newRoom); + return true; + }); return OtherChange; - }); + } + ); + // clang-format on } Room::Changes Room::processEphemeralEvent(EventPtr&& event) @@ -18,6 +18,7 @@ #pragma once +#include "connection.h" #include "eventitem.h" #include "joinstate.h" @@ -38,7 +39,6 @@ class Event; class Avatar; class SyncRoomData; class RoomMemberEvent; -class Connection; class User; class MemberSorter; class LeaveRoomJob; @@ -95,7 +95,8 @@ class Room : public QObject 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 localAliases READ localAliases NOTIFY namesChanged) + Q_PROPERTY(QStringList remoteAliases READ remoteAliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) @@ -176,7 +177,12 @@ public: QString predecessorId() const; QString successorId() const; QString name() const; - QStringList aliases() const; + /// Room aliases defined on the current user's server + /// \sa remoteAliases, setLocalAliases + QStringList localAliases() const; + /// Room aliases defined on other servers + /// \sa localAliases + QStringList remoteAliases() const; QString canonicalAlias() const; QString displayName() const; QString topic() const; @@ -420,16 +426,14 @@ public: 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; + template <typename EvT, typename... ArgTs> + auto setState(ArgTs&&... args) const + { + return setState(EvT(std::forward<ArgTs>(args)...)); + } + public slots: /** Check whether the room should be upgraded */ void checkVersion(); @@ -451,9 +455,13 @@ public slots: 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 with the given event + SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); - void setAliases(const QStringList& aliases); + /// 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 @@ -463,6 +471,7 @@ public slots: void inviteToRoom(const QString& memberId); LeaveRoomJob* leaveRoom(); + /// \deprecated - use setState() instead") SetRoomStateWithKeyJob* setMemberState(const QString& memberId, const RoomMemberEvent& event) const; void kickMember(const QString& memberId, const QString& reason = {}); @@ -485,6 +494,14 @@ public slots: /// 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); + void answerCall(const QString& callId, const int lifetime, + const QString& sdp); + void answerCall(const QString& callId, const QString& sdp); + void hangupCall(const QString& callId); + signals: /// Initial set of state events has been loaded /** @@ -604,7 +621,6 @@ signals: void beforeDestruction(Room*); protected: - /// Returns true if any of room names/aliases has changed virtual Changes processStateEvent(const RoomEvent& e); virtual Changes processEphemeralEvent(EventPtr&& event); virtual Changes processAccountDataEvent(EventPtr&& event); diff --git a/lib/settings.cpp b/lib/settings.cpp index f8f1eae5..1278fe33 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -91,6 +91,8 @@ QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, static const auto HomeserverKey = QStringLiteral("homeserver"); static const auto AccessTokenKey = QStringLiteral("access_token"); +static const auto EncryptionAccountPickleKey = + QStringLiteral("encryption_account_pickle"); QUrl AccountSettings::homeserver() const { @@ -112,7 +114,8 @@ QString AccountSettings::accessToken() const void AccountSettings::setAccessToken(const QString& accessToken) { qCWarning(MAIN) << "Saving access_token to QSettings is insecure." - " Developers, please save access_token separately."; + " Developers, do it manually or contribute to share " + "QtKeychain logic to libQuotient."; setValue(AccessTokenKey, accessToken); } @@ -123,3 +126,25 @@ void AccountSettings::clearAccessToken() // re-issue it remove(AccessTokenKey); } + +QByteArray AccountSettings::encryptionAccountPickle() +{ + QString passphrase = ""; // FIXME: add QtKeychain + return value("encryption_account_pickle", "").toByteArray(); +} + +void AccountSettings::setEncryptionAccountPickle( + const QByteArray& encryptionAccountPickle) +{ + qCWarning(MAIN) + << "Saving encryption_account_pickle to QSettings is insecure." + " Developers, do it manually or contribute to share QtKeychain " + "logic to libQuotient."; + QString passphrase = ""; // FIXME: add QtKeychain + setValue("encryption_account_pickle", encryptionAccountPickle); +} + +void AccountSettings::clearEncryptionAccountPickle() +{ + remove(EncryptionAccountPickleKey); // TODO: Force to re-issue it? +} diff --git a/lib/settings.h b/lib/settings.h index cb09c479..e1ca0866 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -130,6 +130,8 @@ class AccountSettings : public SettingsGroup QMC_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) /** \deprecated \sa setAccessToken */ Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) + Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle + WRITE setEncryptionAccountPickle) public: template <typename... ArgTs> explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs) @@ -147,5 +149,9 @@ public: * see QMatrixClient/Quaternion#181 */ void setAccessToken(const QString& accessToken); Q_INVOKABLE void clearAccessToken(); + + QByteArray encryptionAccountPickle(); + void setEncryptionAccountPickle(const QByteArray& encryptionAccountPickle); + Q_INVOKABLE void clearEncryptionAccountPickle(); }; } // namespace QMatrixClient diff --git a/lib/syncdata.h b/lib/syncdata.h index 49df8db6..6932878d 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -37,7 +37,7 @@ struct RoomSummary Omittable<int> joinedMemberCount; Omittable<int> invitedMemberCount; Omittable<QStringList> heroes; //< mxids of users to take part in the room - //name + // name bool isEmpty() const; /// Merge the contents of another RoomSummary object into this one diff --git a/lib/user.cpp b/lib/user.cpp index c463b42e..f0216454 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -269,8 +269,8 @@ void User::rename(const QString& newName, const Room* r) const auto actualNewName = sanitized(newName); MemberEventContent evtC; evtC.displayName = actualNewName; - connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))), - &BaseJob::success, this, [=] { updateName(actualNewName, r); }); + connect(r->setState<RoomMemberEvent>(id(), move(evtC)), &BaseJob::success, + this, [=] { updateName(actualNewName, r); }); } bool User::setAvatar(const QString& fileName) diff --git a/lib/util.cpp b/lib/util.cpp index 4a7e0f61..9e0807c6 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -45,7 +45,7 @@ void QMatrixClient::linkifyUrls(QString& htmlEscapedText) // comma or dot static const QRegularExpression FullUrlRegExp( QStringLiteral( - R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] @@ -113,6 +113,21 @@ qreal QMatrixClient::stringToHueF(const QString& string) return hueF; } +static const auto ServerPartRegEx = QStringLiteral( + "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address + "(?::(\\d{1,5}))?" // Optional port +); + +QString QMatrixClient::serverPart(const QString& mxId) +{ + static QString re = "^[@!#$+].+?:(" // Localpart and colon + % ServerPartRegEx % ")$"; + static QRegularExpression parser( + re, + QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits + return parser.match(mxId).captured(1); +} + // Tests for function_traits<> #ifdef Q_CC_CLANG @@ -327,26 +327,33 @@ inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, void linkifyUrls(QString& htmlEscapedText); /** Sanitize the text before showing in HTML + * * This does toHtmlEscaped() and removes Unicode BiDi marks. */ QString sanitized(const QString& plainText); /** Pretty-print plain text into HTML + * * This includes HTML escaping of <,>,",& and calling linkifyUrls() */ QString prettyPrint(const QString& plainText); /** Return a path to cache directory after making sure that it exists + * * The returned path has a trailing slash, clients don't need to append it. * \param dir path to cache directory relative to the standard cache path */ QString cacheLocation(const QString& dirName); /** 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 */ qreal stringToHueF(const QString& string); + +/** Extract the serverpart from MXID */ +QString serverPart(const QString& mxId); } // namespace QMatrixClient diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index be568bd2..c561a415 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -7,12 +7,15 @@ win32-msvc* { QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter } +include(3rdparty/libQtOlm/libQtOlm.pri) + SRCPATH = $$PWD/lib INCLUDEPATH += $$SRCPATH HEADERS += \ $$SRCPATH/connectiondata.h \ $$SRCPATH/connection.h \ + $$SRCPATH/encryptionmanager.h \ $$SRCPATH/eventitem.h \ $$SRCPATH/room.h \ $$SRCPATH/user.h \ @@ -38,6 +41,7 @@ HEADERS += \ $$SRCPATH/events/callinviteevent.h \ $$SRCPATH/events/accountdataevents.h \ $$SRCPATH/events/directchatevent.h \ + $$SRCPATH/events/encryptionevent.h \ $$SRCPATH/events/redactionevent.h \ $$SRCPATH/events/eventloader.h \ $$SRCPATH/jobs/requestdata.h \ @@ -60,6 +64,7 @@ HEADERS += \ SOURCES += \ $$SRCPATH/connectiondata.cpp \ $$SRCPATH/connection.cpp \ + $$SRCPATH/encryptionmanager.cpp \ $$SRCPATH/eventitem.cpp \ $$SRCPATH/room.cpp \ $$SRCPATH/user.cpp \ @@ -81,6 +86,7 @@ SOURCES += \ $$SRCPATH/events/callinviteevent.cpp \ $$SRCPATH/events/receiptevent.cpp \ $$SRCPATH/events/directchatevent.cpp \ + $$SRCPATH/events/encryptionevent.cpp \ $$SRCPATH/jobs/requestdata.cpp \ $$SRCPATH/jobs/basejob.cpp \ $$SRCPATH/jobs/syncjob.cpp \ |