diff options
34 files changed, 864 insertions, 210 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 ca597469..19fbdcbe 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}" ABSOLUTE) endif () @@ -64,12 +82,27 @@ 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 (MATRIX_DOC_PATH AND GTAD_PATH) message( STATUS "Generating API stubs enabled" ) message( STATUS " Using GTAD at ${ABS_GTAD_PATH}" ) message( STATUS " Using API files at ${ABS_API_DEF_PATH}" ) endif () +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 ) @@ -87,6 +120,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 @@ -103,6 +137,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 @@ -158,7 +193,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 37dfa77e..57edb8d1 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; @@ -152,15 +152,15 @@ The generating sequence only works with CMake atm; patches to enable it with qmake are (you guessed it) very welcome. #### Why generate the code at all? -Because before both original authors of libQMatrixClient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep, coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). +Because before both original authors of 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). -3. Get the Matrix CS API definitions that are included in the matrix-doc repo: `git clone https://github.com/QMatrixClient/matrix-doc.git` (QMatrixClient/matrix-doc is a fork that's known to produce working code; you may want to use your own fork if you wish to alter something in the API). +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 libQuotient itself). +3. Get the Matrix CS API definitions that are included in the matrix-doc repo: `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). #### 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, @@ -175,14 +175,14 @@ Because before both original authors of libQMatrixClient had to do monkey busine is recommended if the list of generated files has changed. #### Changing generated code -See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQMatrixClient are described here. +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. @@ -191,7 +191,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 @@ -205,12 +205,12 @@ 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. ### Qt-flavoured C++ This is our primary language. A particular code style is not enforced _yet_ but -[the PR imposing the common code style](https://github.com/QMatrixClient/libqmatrixclient/pull/295) +[the PR imposing the common code style](https://github.com/quotient-im/libQuotient/pull/295) is planned to arrive in version 0.6. * 4-space indents, no tabs, no trailing spaces, no last empty lines. If you spot the code abusing these - we'll thank you for fixing it. @@ -231,7 +231,7 @@ is planned to arrive in version 0.6. 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 @@ -276,14 +276,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. @@ -337,7 +337,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. @@ -350,16 +350,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 511a0411..79411dce 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -23,6 +23,7 @@ #include "events/eventloader.h" #include "room.h" #include "settings.h" +#include "encryptionmanager.h" #include "csapi/login.h" #include "csapi/capabilities.h" #include "csapi/logout.h" @@ -104,6 +105,8 @@ class Connection::Private GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; + QScopedPointer<EncryptionManager> encryptionManager; + SyncJob* syncJob = nullptr; bool cacheState = true; @@ -161,33 +164,11 @@ Connection::~Connection() stopSync(); } -static const auto ServerPartRegEx = QStringLiteral( - "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address - "(?::(\\d{1,5}))?" // Optional port -); - -QString serverPart(const QString& mxId) -{ - static auto re = "^[@!#$+].+?:(" // Localpart and colon - % ServerPartRegEx % ")$"; - static QRegularExpression parser(re, - QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits - return parser.match(mxId).captured(1); -} - -void Connection::resolveServer(const QString& mxidOrDomain) +void Connection::resolveServer(const QString& mxid) { - // mxIdOrDomain may be something as complex as - // @username:[IPv6:address]:port, or as simple as a plain serverpart. - static QRegularExpression parser( - "^(@.+?:)?" // Optional username (allow everything for compatibility) - % ServerPartRegEx % '$', - QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits - auto match = parser.match(mxidOrDomain); - - auto 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") @@ -258,6 +239,16 @@ void Connection::doConnectToServer(const QString& user, const QString& password, [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] { diff --git a/lib/connection.h b/lib/connection.h index eca3c5be..11499a6e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -122,7 +122,7 @@ namespace QMatrixClient 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 @@ -408,8 +408,8 @@ namespace QMatrixClient /** 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..50db9889 --- /dev/null +++ b/lib/encryptionmanager.cpp @@ -0,0 +1,223 @@ +#include "encryptionmanager.h" + +#include <functional> +#include <memory> +#include <QtCore/QStringBuilder> +#include <QtCore/QHash> +#include <account.h> // QtOlm + +#include "csapi/keys.h" +#include "connection.h" + +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..40fe7383 --- /dev/null +++ b/lib/encryptionmanager.h @@ -0,0 +1,32 @@ +#pragma once + +#include <functional> +#include <memory> +#include <QtCore/QObject> + +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..b8e2b575 --- /dev/null +++ b/lib/events/encryptionevent.cpp @@ -0,0 +1,53 @@ +// +// 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; + } + }; +} + +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..b9e108f0 --- /dev/null +++ b/lib/events/encryptionevent.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * 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 "stateevent.h" +#include "eventcontent.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 b7bbd83e..b3a58806 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -60,14 +60,16 @@ namespace QMatrixClient 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) @@ -259,7 +261,7 @@ namespace QMatrixClient } 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 ab31a75d..254eb9a9 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -53,6 +53,9 @@ namespace QMatrixClient QJsonObject originalJson; protected: + Base(const Base&) = default; + Base(Base&&) = default; + virtual void fillJson(QJsonObject* o) const = 0; }; @@ -167,11 +170,14 @@ namespace QMatrixClient 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 da663392..d0fa60a2 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -32,7 +32,8 @@ namespace QMatrixClient { } } - /** 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. @@ -44,7 +45,8 @@ namespace QMatrixClient { 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. @@ -57,6 +59,20 @@ namespace QMatrixClient { 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>> { static auto load(const QJsonValue& jv) diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 3d03509f..f1e563ff 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -78,7 +78,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 ce96174e..e26a7135 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -60,6 +60,9 @@ namespace QMatrixClient 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 b8224033..39aa280c 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -56,8 +56,14 @@ namespace QMatrixClient explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) { } + [[deprecated("Use RoomMemberEvent(userId, contentArgs) instead")]] RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), 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 @@ -76,7 +82,7 @@ namespace QMatrixClient MembershipType membership() const { return content().membership; } QString userId() const - { return fullJson()["state_key"_ls].toString(); } + { 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 2c23d9ca..ef56c7b2 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -20,8 +20,6 @@ #include "stateevent.h" -#include "converters.h" - namespace QMatrixClient { namespace EventContent @@ -63,7 +61,7 @@ namespace QMatrixClient explicit _Name() : _Name(value_type()) { } \ template <typename T> \ explicit _Name(T&& value) \ - : StateEvent(typeId(), matrixTypeId(), \ + : StateEvent(typeId(), matrixTypeId(), QString(), \ QStringLiteral(#_ContentKey), \ std::forward<T>(value)) \ { } \ @@ -78,15 +76,27 @@ namespace QMatrixClient 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 a84f302b..6a6e7782 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -27,7 +27,7 @@ using namespace QMatrixClient; 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)) @@ -36,6 +36,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 3f54f7bf..3b56a265 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -21,12 +21,28 @@ #include "roomevent.h" 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; } @@ -83,8 +99,9 @@ namespace QMatrixClient { } 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/joinstate.h b/lib/joinstate.h index 379183f6..4ae67de8 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -41,7 +41,7 @@ namespace QMatrixClient inline const char* toCString(JoinState js) { size_t state = size_t(js), index = 0; - while (state >>= 1) ++index; + while (state >>= 1u) ++index; return JoinStateStrings[index]; } } // namespace QMatrixClient diff --git a/lib/logging.cpp b/lib/logging.cpp index 7476781f..a0c7f7bb 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -25,9 +25,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 06f3490c..cb368d9e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -31,6 +31,7 @@ #include "csapi/tags.h" #include "csapi/room_upgrades.h" #include "events/simplestateevents.h" +#include "events/encryptionevent.h" #include "events/roomcreateevent.h" #include "events/roomtombstoneevent.h" #include "events/roomavatarevent.h" @@ -93,6 +94,8 @@ class Room::Private /// 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; @@ -193,9 +196,22 @@ class Room::Private 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); @@ -272,28 +288,26 @@ class Room::Private 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)...)); } - /** + /** * @brief Apply redaction to the timeline * * Tries to find an event in the timeline and redact it; deletes the @@ -317,6 +331,8 @@ class Room::Private } }; +decltype(Room::Private::baseState) Room::Private::stubbedState { }; + Room::Room(Connection* connection, QString id, JoinState initialJoinState) : QObject(connection), d(new Private(connection, id, initialJoinState)) { @@ -1400,6 +1416,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)); @@ -1409,6 +1429,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))); @@ -1614,38 +1639,38 @@ 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::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) @@ -1694,33 +1719,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) @@ -1755,9 +1780,10 @@ LeaveRoomJob* Room::leaveRoom() return connection()->leaveRoom(this); } -SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const +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) @@ -1919,7 +1945,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, auto originalJson = target.originalJsonObject(); static const QStringList keepKeys { EventIdKey, TypeKey, QStringLiteral("room_id"), - QStringLiteral("sender"), QStringLiteral("state_key"), + QStringLiteral("sender"), StateKeyKey, QStringLiteral("prev_content"), ContentKey, QStringLiteral("hashes"), QStringLiteral("signatures"), QStringLiteral("depth"), QStringLiteral("prev_events"), @@ -18,6 +18,7 @@ #pragma once +#include "connection.h" #include "csapi/message_pagination.h" #include "events/roommessageevent.h" #include "events/accountdataevents.h" @@ -36,7 +37,6 @@ namespace QMatrixClient class Avatar; class SyncRoomData; class RoomMemberEvent; - class Connection; class User; class MemberSorter; class LeaveRoomJob; @@ -406,17 +406,14 @@ namespace QMatrixClient 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(); @@ -440,6 +437,9 @@ namespace QMatrixClient 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); /// Set room aliases on the user's current server @@ -453,6 +453,7 @@ namespace QMatrixClient 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 = {}); @@ -477,6 +478,15 @@ namespace QMatrixClient /// 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 /** diff --git a/lib/settings.cpp b/lib/settings.cpp index 124d7042..5f10299c 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -90,6 +90,7 @@ 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 { @@ -114,7 +115,7 @@ 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); } @@ -124,3 +125,22 @@ void AccountSettings::clearAccessToken() legacySettings.remove(QStringLiteral("device_id")); // Force the server to 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 759bda35..61e5232a 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -131,6 +131,7 @@ void classname::setter(type newValue) \ 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) @@ -148,5 +149,9 @@ void classname::setter(type newValue) \ * 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/user.cpp b/lib/user.cpp index 7b695618..7ca97b1a 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -292,7 +292,7 @@ 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))), + connect(r->setState<RoomMemberEvent>(id(), move(evtC)), &BaseJob::success, this, [=] { updateName(actualNewName, r); }); } diff --git a/lib/util.cpp b/lib/util.cpp index 88cba959..b9639843 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -112,6 +112,20 @@ 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 @@ -300,27 +300,33 @@ namespace QMatrixClient 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); -} // namespace QMatrixClient + /** 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 \ |