From 0a9d95dfc9ef2c7e822257cc7b256cdd24d4a0d3 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Mon, 4 Jun 2018 13:54:51 +0300 Subject: [PATCH 01/18] Include mtxclient in the build --- .gitignore | 1 + CMakeLists.txt | 31 ++++++------- Makefile | 16 ++++++- appveyor.yml | 25 ++++++++++- cmake/MatrixStructs.cmake | 33 -------------- deps/CMakeLists.txt | 82 ++++++++++++++++++++++++++++++++++ deps/cmake/Boost.cmake | 23 ++++++++++ deps/cmake/MatrixClient.cmake | 30 +++++++++++++ deps/cmake/MatrixStructs.cmake | 25 +++++++++++ deps/cmake/Olm.cmake | 24 ++++++++++ deps/cmake/SpdLog.cmake | 15 +++++++ src/MatrixClient.cc | 3 ++ 12 files changed, 257 insertions(+), 51 deletions(-) delete mode 100644 cmake/MatrixStructs.cmake create mode 100644 deps/CMakeLists.txt create mode 100644 deps/cmake/Boost.cmake create mode 100644 deps/cmake/MatrixClient.cmake create mode 100644 deps/cmake/MatrixStructs.cmake create mode 100644 deps/cmake/Olm.cmake create mode 100644 deps/cmake/SpdLog.cmake diff --git a/.gitignore b/.gitignore index cb860801..88f85bbb 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ package.dir # Dependencies .third-party +.deps diff --git a/CMakeLists.txt b/CMakeLists.txt index fffe0f7d..0fcd9f2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,16 @@ set(IDENTIFIER "com.github.mujx.nheko") add_project_meta(META_FILES_TO_INCLUDE) +set(DEPS_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/.deps/usr" + CACHE PATH "Path prefix for finding dependencies") +list(INSERT CMAKE_PREFIX_PATH 0 ${DEPS_PREFIX}) + +include_directories(SYSTEM ${DEPS_PREFIX}/include) + +if(APPLE) + set(OPENSSL_ROOT_DIR /usr/local/opt/openssl) +endif() + # # LMDB # @@ -177,20 +187,10 @@ set(SRC_FILES # ExternalProject dependencies set(EXTERNAL_PROJECT_DEPS "") -# -# matrix-structs -# -find_library(MATRIX_STRUCTS_LIBRARY - NAMES matrix_structs - PATHS ${MATRIX_STRUCTS_ROOT} - ${MATRIX_STRUCTS_ROOT}/lib - ${MATRIX_STRUCTS_ROOT}/lib/static) - -if(NOT MATRIX_STRUCTS_LIBRARY) - include(MatrixStructs) - set(EXTERNAL_PROJECT_DEPS ${EXTERNAL_PROJECT_DEPS} MatrixStructs) -endif() -include_directories(SYSTEM ${MATRIX_STRUCTS_INCLUDE_DIR}) +find_package(MatrixStructs REQUIRED) +find_package(MatrixClient REQUIRED) +find_package(OpenSSL REQUIRED) +find_package(ZLIB REQUIRED) # # tweeny @@ -293,7 +293,8 @@ include(Translations) set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC}) set(COMMON_LIBS - ${MATRIX_STRUCTS_LIBRARY} + MatrixStructs::MatrixStructs + MatrixClient::MatrixClient Qt5::Widgets Qt5::Network Qt5::Svg diff --git a/Makefile b/Makefile index 4d46e935..833d81a0 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,23 @@ +DEPS_BUILD_DIR=.deps +DEPS_SOURCE_DIR=deps debug: @cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 @cmake --build build +third_party: + @mkdir -p ${DEPS_BUILD_DIR}/usr/{lib,include}/ + @cmake -GNinja -H${DEPS_SOURCE_DIR} -B${DEPS_BUILD_DIR} \ + -DCMAKE_BUILD_TYPE=Release \ + -DUSE_BUNDLED_BOOST=OFF + @cmake --build ${DEPS_BUILD_DIR} + ci: - @cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo - @cmake --build build + mkdir -p ${DEPS_BUILD_DIR}/usr/{lib,include}/ + cmake -H${DEPS_SOURCE_DIR} -B${DEPS_BUILD_DIR} -DCMAKE_BUILD_TYPE=Release + cmake --build ${DEPS_BUILD_DIR} + cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build release: @cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo diff --git a/appveyor.yml b/appveyor.yml index adbad603..0cd819ab 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,7 +15,19 @@ install: - set QT_DIR=C:\Qt\5.10.1\msvc2017_64 - set PATH=%PATH%;%QT_DIR%\bin;C:\MinGW\bin - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" - - vcpkg install lmdb:%PLATFORM%-windows + - vcpkg install + boost-asio:%PLATFORM%-windows + boost-beast:%PLATFORM%-windows + boost-iostreams:%PLATFORM%-windows + boost-random:%PLATFORM%-windows + boost-signals2:%PLATFORM%-windows + boost-system:%PLATFORM%-windows + boost-thread:%PLATFORM%-windows + libsodium:%PLATFORM%-windows + lmdb:%PLATFORM%-windows + openssl:%PLATFORM%-windows + spdlog:%PLATFORM%-windows + zlib:%PLATFORM%-windows build_script: # VERSION format: branch-master/branch-1.2 @@ -35,6 +47,17 @@ build_script: - echo %VERSION% - echo %INSTVERSION% - echo %DATE% + + # Build & install the dependencies + - cmake -G "Visual Studio 15 2017 Win64" -Hdeps -B.deps + -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake + -DCMAKE_BUILD_TYPE=Release + -DUSE_BUNDLED_BOOST=OFF + -DUSE_BUNDLED_SPDLOG=OFF + -DUSE_BUNDLED_GTEST=OFF + - cmake --build .deps --config Release + + # Build nheko - cmake -G "Visual Studio 15 2017 Win64" -H. -Bbuild -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release diff --git a/cmake/MatrixStructs.cmake b/cmake/MatrixStructs.cmake deleted file mode 100644 index af694c0f..00000000 --- a/cmake/MatrixStructs.cmake +++ /dev/null @@ -1,33 +0,0 @@ -include(ExternalProject) - -# -# Build matrix-structs. -# - -set(THIRD_PARTY_ROOT ${CMAKE_SOURCE_DIR}/.third-party) -set(MATRIX_STRUCTS_ROOT ${THIRD_PARTY_ROOT}/matrix_structs) -set(MATRIX_STRUCTS_INCLUDE_DIR ${MATRIX_STRUCTS_ROOT}/include) -set(MATRIX_STRUCTS_LIBRARY matrix_structs) - -link_directories(${MATRIX_STRUCTS_ROOT}) - -set(WINDOWS_FLAGS "") - -if(MSVC) - set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") -endif() - -ExternalProject_Add( - MatrixStructs - - GIT_REPOSITORY https://github.com/mujx/matrix-structs - GIT_TAG 5e57c2385a79b6629d1998fec4a7c0baee23555e - - BUILD_IN_SOURCE 1 - SOURCE_DIR ${MATRIX_STRUCTS_ROOT} - CONFIGURE_COMMAND ${CMAKE_COMMAND} - -DCMAKE_BUILD_TYPE=Release ${MATRIX_STRUCTS_ROOT} - ${WINDOWS_FLAGS} - BUILD_COMMAND ${CMAKE_COMMAND} --build ${MATRIX_STRUCTS_ROOT} --config Release - INSTALL_COMMAND "" -) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt new file mode 100644 index 00000000..234e904f --- /dev/null +++ b/deps/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.1) +project(NHEKO_DEPS) + +# Point CMake at any custom modules we may ship +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +set(DEPS_INSTALL_DIR "${CMAKE_BINARY_DIR}/usr" + CACHE PATH "Dependencies install directory.") +set(DEPS_BIN_DIR "${DEPS_INSTALL_DIR}/bin" + CACHE PATH "Dependencies binary install directory.") +set(DEPS_LIB_DIR "${DEPS_INSTALL_DIR}/lib" + CACHE PATH "Dependencies library install directory.") +set(DEPS_BUILD_DIR "${CMAKE_BINARY_DIR}/build" + CACHE PATH "Dependencies build directory.") +set(DEPS_DOWNLOAD_DIR "${DEPS_BUILD_DIR}/downloads" + CACHE PATH "Dependencies download directory.") + +option(USE_BUNDLED "Use bundled dependencies." ON) + +option(USE_BUNDLED_BOOST "Use the bundled version of Boost." ${USE_BUNDLED}) +option(USE_BUNDLED_SPDLOG "Use the bundled version of spdlog." ${USE_BUNDLED}) +option(USE_BUNDLED_OLM "Use the bundled version of libolm." ${USE_BUNDLED}) +option(USE_BUNDLED_MATRIX_STRUCTS "Use the bundled version of matrix-structs." + ${USE_BUNDLED}) +option(USE_BUNDLED_MATRIX_CLIENT "Use the bundled version of mtxclient." + ${USE_BUNDLED}) + +include(ExternalProject) + +set(BOOST_URL + https://dl.bintray.com/boostorg/release/1.66.0/source/boost_1_66_0.tar.bz2) +set(BOOST_SHA256 + 5721818253e6a0989583192f96782c4a98eb6204965316df9f5ad75819225ca9) + +set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs) +set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de) + +set(MTXCLIENT_URL https://github.com/mujx/mtxclient) +set(MTXCLIENT_TAG 219d2a8887376122e76ba0f64c0cc9935f62f308) + +set(OLM_URL https://git.matrix.org/git/olm.git) +set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) + +set(SPDLOG_URL https://github.com/gabime/spdlog) +set(SPDLOG_TAG 560df2878ad308b27873b3cc5e810635d69cfad6) + +if(USE_BUNDLED_BOOST) + include(Boost) +endif() + +if(USE_BUNDLED_SPDLOG) + include(SpdLog) +endif() + +if(USE_BUNDLED_OLM) + include(Olm) +endif() + +if(USE_BUNDLED_MATRIX_STRUCTS) + include(MatrixStructs) +endif() + +if(WIN32) + if("${TARGET_ARCH}" STREQUAL "X86_64") + set(TARGET_ARCH x64) + elseif(TARGET_ARCH STREQUAL "X86") + set(TARGET_ARCH ia32) + endif() +endif() + +add_custom_target(third-party ALL + COMMAND ${CMAKE_COMMAND} -E touch .third-party + DEPENDS ${THIRD_PARTY_DEPS}) + +if(USE_BUNDLED_MATRIX_CLIENT) + include(MatrixClient) + add_dependencies(MatrixClient third-party) +endif() diff --git a/deps/cmake/Boost.cmake b/deps/cmake/Boost.cmake new file mode 100644 index 00000000..572d1d07 --- /dev/null +++ b/deps/cmake/Boost.cmake @@ -0,0 +1,23 @@ +if(WIN32) + message(STATUS "Building Boost in Windows is not supported (skipping)") + return() +endif() + +ExternalProject_Add( + Boost + + URL ${BOOST_URL} + URL_HASH SHA256=${BOOST_SHA256} + DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/boost + DOWNLOAD_NO_PROGRESS 0 + + BUILD_IN_SOURCE 1 + SOURCE_DIR ${DEPS_BUILD_DIR}/boost + CONFIGURE_COMMAND ${DEPS_BUILD_DIR}/boost/bootstrap.sh + --with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex + --prefix=${DEPS_INSTALL_DIR} + BUILD_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 variant=release link=static threading=multi --layout=system + INSTALL_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 install +) + +list(APPEND THIRD_PARTY_DEPS Boost) diff --git a/deps/cmake/MatrixClient.cmake b/deps/cmake/MatrixClient.cmake new file mode 100644 index 00000000..7377f710 --- /dev/null +++ b/deps/cmake/MatrixClient.cmake @@ -0,0 +1,30 @@ +set(PLATFORM_FLAGS "") + +if(MSVC) + set(PLATFORM_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") +endif() + +if(APPLE) + set(PLATFORM_FLAGS "-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl") +endif() + +ExternalProject_Add( + MatrixClient + + DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/mtxclient + GIT_REPOSITORY ${MTXCLIENT_URL} + GIT_TAG ${MTXCLIENT_TAG} + + BUILD_IN_SOURCE 1 + SOURCE_DIR ${DEPS_BUILD_DIR}/mtxclient + CONFIGURE_COMMAND ${CMAKE_COMMAND} + -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} + -DCMAKE_BUILD_TYPE=Release + -DBUILD_LIB_TESTS=OFF + -DBUILD_LIB_EXAMPLES=OFF + ${PLATFORM_FLAGS} + ${DEPS_BUILD_DIR}/mtxclient + BUILD_COMMAND + ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/mtxclient --config Release) + +list(APPEND THIRD_PARTY_DEPS MatrixClient) diff --git a/deps/cmake/MatrixStructs.cmake b/deps/cmake/MatrixStructs.cmake new file mode 100644 index 00000000..fd12ad39 --- /dev/null +++ b/deps/cmake/MatrixStructs.cmake @@ -0,0 +1,25 @@ +set(WINDOWS_FLAGS "") + +if(MSVC) + set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") +endif() + +ExternalProject_Add( + MatrixStructs + + DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/matrix_structs + GIT_REPOSITORY ${MATRIX_STRUCTS_URL} + GIT_TAG ${MATRIX_STRUCTS_TAG} + + BUILD_IN_SOURCE 1 + SOURCE_DIR ${DEPS_BUILD_DIR}/matrix_structs + CONFIGURE_COMMAND ${CMAKE_COMMAND} + -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} + -DCMAKE_BUILD_TYPE=Release + ${DEPS_BUILD_DIR}/matrix_structs + ${WINDOWS_FLAGS} + BUILD_COMMAND ${CMAKE_COMMAND} + --build ${DEPS_BUILD_DIR}/matrix_structs + --config Release) + +list(APPEND THIRD_PARTY_DEPS MatrixStructs) diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake new file mode 100644 index 00000000..b0df2833 --- /dev/null +++ b/deps/cmake/Olm.cmake @@ -0,0 +1,24 @@ +if(MSVC) + set(MAKE_CMD "mingw32-make.exe") +else() + set(MAKE_CMD "make") +endif() + +set(OLM_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}olm${CMAKE_STATIC_LIBRARY_SUFFIX}") + +ExternalProject_Add( + Olm + + GIT_REPOSITORY ${OLM_URL} + GIT_TAG ${OLM_TAG} + + BUILD_IN_SOURCE 1 + SOURCE_DIR ${DEPS_BUILD_DIR}/olm + CONFIGURE_COMMAND "" + BUILD_COMMAND ${MAKE_CMD} static + INSTALL_COMMAND + cp -R ${DEPS_BUILD_DIR}/olm/include ${DEPS_INSTALL_DIR} && + cp ${DEPS_BUILD_DIR}/olm/build/libolm.a ${DEPS_INSTALL_DIR}/lib +) + +list(APPEND THIRD_PARTY_DEPS Olm) diff --git a/deps/cmake/SpdLog.cmake b/deps/cmake/SpdLog.cmake new file mode 100644 index 00000000..e49c947f --- /dev/null +++ b/deps/cmake/SpdLog.cmake @@ -0,0 +1,15 @@ +ExternalProject_Add( + SpdLog + + GIT_REPOSITORY ${SPDLOG_URL} + GIT_TAG ${SPDLOG_TAG} + + BUILD_IN_SOURCE 1 + SOURCE_DIR ${DEPS_BUILD_DIR}/spdlog + CONFIGURE_COMMAND ${CMAKE_COMMAND} + -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} + -DCMAKE_BUILD_TYPE=Release + ${DEPS_BUILD_DIR}/spdlog +) + +list(APPEND THIRD_PARTY_DEPS SpdLog) diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index a9720d10..c4eaf347 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -32,6 +32,7 @@ #include #include "MatrixClient.h" +#include namespace { std::unique_ptr instance_ = nullptr; @@ -39,6 +40,8 @@ std::unique_ptr instance_ = nullptr; namespace http { +std::shared_ptr client_ = nullptr; + void init() { From 1366b01790631ba1134a4dfd03d939b4b40360b8 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Mon, 4 Jun 2018 16:07:47 +0300 Subject: [PATCH 02/18] Install missing dependencies in travis-ci/appveyor --- .ci/install.sh | 7 +++++-- .ci/script.sh | 11 ++++++++++- .travis.yml | 8 ++++++++ CMakeLists.txt | 24 +++++++++++++++++++++--- appveyor.yml | 2 ++ cmake/CompilerFlags.cmake | 26 -------------------------- deps/cmake/Olm.cmake | 2 -- 7 files changed, 46 insertions(+), 34 deletions(-) delete mode 100644 cmake/CompilerFlags.cmake diff --git a/.ci/install.sh b/.ci/install.sh index f81f9265..ee172581 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -4,7 +4,8 @@ set -ex if [ $TRAVIS_OS_NAME == osx ]; then brew update - brew install qt5 lmdb clang-format ninja + brew install qt5 lmdb clang-format ninja libsodium spdlog + brew upgrade boost curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py sudo python get-pip.py @@ -23,6 +24,7 @@ if [ $TRAVIS_OS_NAME == linux ]; then QT_PKG="59" fi + sudo add-apt-repository -y ppa:chris-lea/libsodium sudo add-apt-repository -y ppa:beineri/opt-qt${QT_VERSION}-trusty sudo add-apt-repository -y ppa:george-edison55/cmake-3.x sudo apt-get update -qq @@ -32,5 +34,6 @@ if [ $TRAVIS_OS_NAME == linux ]; then qt${QT_PKG}svg \ qt${QT_PKG}multimedia \ cmake \ - liblmdb-dev + liblmdb-dev \ + libsodium-dev fi diff --git a/.ci/script.sh b/.ci/script.sh index a954eba6..f5966349 100755 --- a/.ci/script.sh +++ b/.ci/script.sh @@ -10,7 +10,16 @@ if [ $TRAVIS_OS_NAME == osx ]; then export CMAKE_PREFIX_PATH=/usr/local/opt/qt5 fi -make ci +# Build & install dependencies +mkdir -p .deps/usr/{lib,include}/ +cmake -Hdeps -B.deps \ + -DUSE_BUNDLED_BOOST=${USE_BUNDLED_BOOST} \ + -DUSE_BUNDLED_SPDLOG=${USE_BUNDLED_SPDLOG} +cmake --build .deps + +# Build nheko +cmake -GNinja -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake --build build if [ $TRAVIS_OS_NAME == osx ]; then make lint; diff --git a/.travis.yml b/.travis.yml index 2047a2db..3e631f30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,8 @@ matrix: compiler: clang env: - DEPLOYMENT=1 + - USE_BUNDLED_BOOST=0 + - USE_BUNDLED_SPDLOG=0 - os: linux compiler: gcc env: @@ -22,6 +24,8 @@ matrix: - QT_VERSION="-5.10.1" - QT_PKG=510 - DEPLOYMENT=1 + - USE_BUNDLED_BOOST=1 + - USE_BUNDLED_SPDLOG=1 addons: apt: sources: ["ubuntu-toolchain-r-test"] @@ -33,6 +37,8 @@ matrix: - C_COMPILER=gcc-7 - QT_VERSION=571 - QT_PKG=57 + - USE_BUNDLED_BOOST=1 + - USE_BUNDLED_SPDLOG=1 addons: apt: sources: ["ubuntu-toolchain-r-test"] @@ -44,6 +50,8 @@ matrix: - C_COMPILER=clang-5.0 - QT_VERSION=592 - QT_PKG=59 + - USE_BUNDLED_BOOST=1 + - USE_BUNDLED_SPDLOG=1 addons: apt: sources: ["ubuntu-toolchain-r-test", "llvm-toolchain-trusty-5.0"] diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fcd9f2c..63c718e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,11 @@ if(APPLE) set(OPENSSL_ROOT_DIR /usr/local/opt/openssl) endif() +if(NOT MSVC AND NOT APPLE) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) +endif() + # # LMDB # @@ -72,7 +77,20 @@ set(CMAKE_C_COMPILER gcc) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) -include(CompilerFlags) +if(NOT MSVC) + set( + CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} \ + -Wall \ + -Wextra \ + -Werror \ + -pipe \ + -pedantic \ + -fsized-deallocation \ + -fdiagnostics-color=always \ + -Wunreachable-code" + ) +endif() if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING @@ -187,10 +205,10 @@ set(SRC_FILES # ExternalProject dependencies set(EXTERNAL_PROJECT_DEPS "") +find_package(ZLIB REQUIRED) +find_package(OpenSSL REQUIRED) find_package(MatrixStructs REQUIRED) find_package(MatrixClient REQUIRED) -find_package(OpenSSL REQUIRED) -find_package(ZLIB REQUIRED) # # tweeny diff --git a/appveyor.yml b/appveyor.yml index 0cd819ab..c9f3023e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,6 +14,8 @@ build: install: - set QT_DIR=C:\Qt\5.10.1\msvc2017_64 - set PATH=%PATH%;%QT_DIR%\bin;C:\MinGW\bin + - set PATH=%PATH%;C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\bin + - mingw32-make.exe --version - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" - vcpkg install boost-asio:%PLATFORM%-windows diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake deleted file mode 100644 index f5860a99..00000000 --- a/cmake/CompilerFlags.cmake +++ /dev/null @@ -1,26 +0,0 @@ -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ - -Wall \ - -Wextra \ - -Werror \ - -pipe \ - -Wno-unused-function \ - -pedantic \ - -Wunreachable-code") - - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") - execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) - - if (GCC_VERSION VERSION_GREATER 4.9) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always" ) - endif() - endif() - - if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always" ) - endif() -endif() - -if(NOT APPLE AND NOT MSVC) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") -endif() diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake index b0df2833..0eb44d37 100644 --- a/deps/cmake/Olm.cmake +++ b/deps/cmake/Olm.cmake @@ -4,8 +4,6 @@ else() set(MAKE_CMD "make") endif() -set(OLM_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}olm${CMAKE_STATIC_LIBRARY_SUFFIX}") - ExternalProject_Add( Olm From b89257a34b2a98b737f4ae544f7e436b9000b240 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sat, 9 Jun 2018 16:03:14 +0300 Subject: [PATCH 03/18] Migrate to mtxclient for the http calls --- CMakeLists.txt | 4 +- deps/CMakeLists.txt | 2 +- deps/cmake/Olm.cmake | 1 + deps/cmake/SpdLog.cmake | 2 + include/AvatarProvider.h | 18 +- include/Cache.h | 3 +- include/ChatPage.h | 65 +- include/CommunitiesList.h | 2 + include/Logging.hpp | 18 + include/LoginPage.h | 33 +- include/MainWindow.h | 3 +- include/MatrixClient.h | 290 +---- include/RegisterPage.h | 5 + include/RoomList.h | 2 + include/TextInputWidget.h | 10 + include/dialogs/ReCaptcha.hpp | 2 +- include/dialogs/RoomSettings.hpp | 3 + include/timeline/TimelineItem.h | 14 +- include/timeline/TimelineView.h | 65 +- include/timeline/TimelineViewManager.h | 6 +- include/timeline/widgets/AudioItem.h | 7 +- include/timeline/widgets/FileItem.h | 7 +- include/timeline/widgets/ImageItem.h | 10 +- src/AvatarProvider.cc | 50 +- src/Cache.cc | 112 +- src/ChatPage.cc | 890 ++++++++++----- src/CommunitiesList.cc | 61 +- src/Logging.cpp | 50 + src/LoginPage.cc | 69 +- src/MainWindow.cc | 54 +- src/MatrixClient.cc | 1375 +----------------------- src/RegisterPage.cc | 126 ++- src/RoomList.cc | 54 +- src/TextInputWidget.cc | 2 - src/dialogs/ReCaptcha.cpp | 15 +- src/dialogs/RoomSettings.cpp | 76 +- src/main.cc | 46 +- src/timeline/TimelineItem.cc | 20 +- src/timeline/TimelineView.cc | 217 +++- src/timeline/TimelineViewManager.cc | 35 +- src/timeline/widgets/AudioItem.cc | 32 +- src/timeline/widgets/FileItem.cc | 44 +- src/timeline/widgets/ImageItem.cc | 138 +-- src/timeline/widgets/VideoItem.cc | 16 +- 44 files changed, 1624 insertions(+), 2430 deletions(-) create mode 100644 include/Logging.hpp create mode 100644 src/Logging.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 63c718e8..eedf9a69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,6 @@ include(LMDB) # Discover Qt dependencies. # find_package(Qt5Widgets REQUIRED) -find_package(Qt5Network REQUIRED) find_package(Qt5LinguistTools REQUIRED) find_package(Qt5Concurrent REQUIRED) find_package(Qt5Svg REQUIRED) @@ -181,6 +180,7 @@ set(SRC_FILES src/Community.cc src/InviteeItem.cc src/LoginPage.cc + src/Logging.cpp src/MainWindow.cc src/MatrixClient.cc src/QuickSwitcher.cc @@ -287,7 +287,6 @@ qt5_wrap_cpp(MOC_HEADERS include/LoginPage.h include/MainWindow.h include/InviteeItem.h - include/MatrixClient.h include/QuickSwitcher.h include/RegisterPage.h include/RoomInfoListItem.h @@ -314,7 +313,6 @@ set(COMMON_LIBS MatrixStructs::MatrixStructs MatrixClient::MatrixClient Qt5::Widgets - Qt5::Network Qt5::Svg Qt5::Concurrent) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 234e904f..d6bab7e5 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs) set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de) set(MTXCLIENT_URL https://github.com/mujx/mtxclient) -set(MTXCLIENT_TAG 219d2a8887376122e76ba0f64c0cc9935f62f308) +set(MTXCLIENT_TAG 57f56d1fe73989dbe041a7ac0a28bf2e3286bf98) set(OLM_URL https://git.matrix.org/git/olm.git) set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake index 0eb44d37..dde18db9 100644 --- a/deps/cmake/Olm.cmake +++ b/deps/cmake/Olm.cmake @@ -15,6 +15,7 @@ ExternalProject_Add( CONFIGURE_COMMAND "" BUILD_COMMAND ${MAKE_CMD} static INSTALL_COMMAND + mkdir -p ${DEPS_INSTALL_DIR}/lib && cp -R ${DEPS_BUILD_DIR}/olm/include ${DEPS_INSTALL_DIR} && cp ${DEPS_BUILD_DIR}/olm/build/libolm.a ${DEPS_INSTALL_DIR}/lib ) diff --git a/deps/cmake/SpdLog.cmake b/deps/cmake/SpdLog.cmake index e49c947f..1335725e 100644 --- a/deps/cmake/SpdLog.cmake +++ b/deps/cmake/SpdLog.cmake @@ -8,6 +8,8 @@ ExternalProject_Add( SOURCE_DIR ${DEPS_BUILD_DIR}/spdlog CONFIGURE_COMMAND ${CMAKE_COMMAND} -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} + -DSPDLOG_BUILD_EXAMPLES=0 + -DSPDLOG_BUILD_TESTING=0 -DCMAKE_BUILD_TYPE=Release ${DEPS_BUILD_DIR}/spdlog ) diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h index ce82f2aa..4b4e15e9 100644 --- a/include/AvatarProvider.h +++ b/include/AvatarProvider.h @@ -20,15 +20,17 @@ #include #include -class AvatarProvider : public QObject +class AvatarProxy : public QObject { Q_OBJECT -public: - //! The callback is called with the downloaded avatar for the given user - //! or the avatar is downloaded first and then saved for re-use. - static void resolve(const QString &room_id, - const QString &userId, - QObject *receiver, - std::function callback); +signals: + void avatarDownloaded(const QByteArray &data); }; + +using AvatarCallback = std::function; + +namespace AvatarProvider { +void +resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb); +} diff --git a/include/Cache.h b/include/Cache.h index d2574b76..afc7a148 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -192,7 +192,7 @@ public: void saveState(const mtx::responses::Sync &res); bool isInitialized() const; - QString nextBatchToken() const; + std::string nextBatchToken() const; void deleteData(); @@ -237,6 +237,7 @@ public: { return image(QString::fromStdString(url)); } + void saveImage(const std::string &url, const std::string &data); void saveImage(const QString &url, const QByteArray &data); RoomInfo singleRoomInfo(const std::string &room_id); diff --git a/include/ChatPage.h b/include/ChatPage.h index b6c431e4..e99e94ba 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -17,6 +17,8 @@ #pragma once +#include + #include #include #include @@ -50,9 +52,6 @@ constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int SHOW_CONTENT_TIMEOUT = 3000; constexpr int TYPING_REFRESH_TIMEOUT = 10000; -Q_DECLARE_METATYPE(mtx::responses::Rooms) -Q_DECLARE_METATYPE(std::vector) - class ChatPage : public QWidget { Q_OBJECT @@ -71,7 +70,37 @@ public: QSharedPointer userSettings() { return userSettings_; } void deleteConfigs(); +public slots: + void leaveRoom(const QString &room_id); + signals: + void connectionLost(); + void connectionRestored(); + + void notificationsRetrieved(const mtx::responses::Notifications &); + + void uploadFailed(const QString &msg); + void imageUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void fileUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void audioUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void videoUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void contentLoaded(); void closing(); void changeWindowTitle(const QString &msg); @@ -82,30 +111,44 @@ signals: void showOverlayProgressBar(); void startConsesusTimer(); + void removeTimelineEvent(const QString &room_id, const QString &event_id); + + void ownProfileOk(); + void setUserDisplayName(const QString &name); + void setUserAvatar(const QImage &avatar); + void loggedOut(); + + void trySyncCb(); + void tryInitialSyncCb(); + void leftRoom(const QString &room_id); + void initializeRoomList(QMap); void initializeViews(const mtx::responses::Rooms &rooms); void initializeEmptyViews(const std::vector &rooms); void syncUI(const mtx::responses::Rooms &rooms); - void continueSync(const QString &next_batch); void syncRoomlist(const std::map &updates); void syncTopBar(const std::map &updates); + void dropToLoginPageCb(const QString &msg); private slots: void showUnreadMessageNotification(int count); void updateTopBarAvatar(const QString &roomid, const QPixmap &img); - void updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name); void updateOwnCommunitiesInfo(const QList &own_communities); - void initialSyncCompleted(const mtx::responses::Sync &response); - void syncCompleted(const mtx::responses::Sync &response); void changeTopRoomInfo(const QString &room_id); void logout(); void removeRoom(const QString &room_id); - //! Handles initial sync failures. - void retryInitialSync(int status_code = -1); + void dropToLoginPage(const QString &msg); + + void joinRoom(const QString &room); + void createRoom(const mtx::requests::CreateRoom &req); + void sendTypingNotifications(); private: static ChatPage *instance_; + void tryInitialSync(); + void trySync(); + //! Check if the given room is currently open. bool isRoomActive(const QString &room_id) { @@ -161,8 +204,8 @@ private: // Safety net if consensus is not possible or too slow. QTimer *showContentTimer_; QTimer *consensusTimer_; - QTimer *syncTimeoutTimer_; - QTimer *initialSyncTimer_; + QTimer connectivityTimer_; + std::atomic_bool isConnected_; QString current_room_; QString current_community_; diff --git a/include/CommunitiesList.h b/include/CommunitiesList.h index 3299e7c4..78b9602e 100644 --- a/include/CommunitiesList.h +++ b/include/CommunitiesList.h @@ -23,12 +23,14 @@ public: signals: void communityChanged(const QString &id); + void avatarRetrieved(const QString &id, const QPixmap &img); public slots: void updateCommunityAvatar(const QString &id, const QPixmap &img); void highlightSelectedCommunity(const QString &id); private: + void fetchCommunityAvatar(const QString &id, const QString &avatarUrl); void addGlobalItem() { addCommunity(QSharedPointer(new Community), "world"); } //! Check whether or not a community id is currently managed. diff --git a/include/Logging.hpp b/include/Logging.hpp new file mode 100644 index 00000000..c301d80d --- /dev/null +++ b/include/Logging.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace log { +void +init(const std::string &file); + +std::shared_ptr +main(); + +std::shared_ptr +net(); + +std::shared_ptr +db(); +} diff --git a/include/LoginPage.h b/include/LoginPage.h index 34a08df9..c52ccaa4 100644 --- a/include/LoginPage.h +++ b/include/LoginPage.h @@ -28,6 +28,12 @@ class OverlayModal; class RaisedButton; class TextField; +namespace mtx { +namespace responses { +struct Login; +} +} + class LoginPage : public QWidget { Q_OBJECT @@ -42,12 +48,19 @@ signals: void loggingIn(); void errorOccurred(); + //! Used to trigger the corresponding slot outside of the main thread. + void versionErrorCb(const QString &err); + void loginErrorCb(const QString &err); + void versionOkCb(); + + void loginOk(const mtx::responses::Login &res); + protected: void paintEvent(QPaintEvent *event) override; public slots: // Displays errors produced during the login. - void loginError(QString msg) { error_label_->setText(msg); } + void loginError(const QString &msg) { error_label_->setText(msg); } private slots: // Callback for the back button. @@ -63,13 +76,25 @@ private slots: void onServerAddressEntered(); // Callback for errors produced during server probing - void versionError(QString error_message); - + void versionError(const QString &error_message); // Callback for successful server probing - void versionSuccess(); + void versionOk(); private: bool isMatrixIdValid(); + void checkHomeserverVersion(); + std::string initialDeviceName() + { +#if defined(Q_OS_MAC) + return "nheko on macOS"; +#elif defined(Q_OS_LINUX) + return "nheko on Linux"; +#elif defined(Q_OS_WIN) + return "nheko on Windows"; +#else + return "nheko"; +#endif + } QVBoxLayout *top_layout_; diff --git a/include/MainWindow.h b/include/MainWindow.h index 0fbc7567..f0fa9a08 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -59,6 +59,7 @@ class MainWindow : public QMainWindow public: explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); static MainWindow *instance() { return instance_; }; void saveCurrentWindowSize(); @@ -96,7 +97,7 @@ private slots: void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); } //! Show the chat page and start communicating with the given access token. - void showChatPage(QString user_id, QString home_server, QString token); + void showChatPage(); void showOverlayProgressBar(); void removeOverlayProgressBar(); diff --git a/include/MatrixClient.h b/include/MatrixClient.h index eae57281..832d6cad 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -1,287 +1,25 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include -class DownloadMediaProxy : public QObject -{ - Q_OBJECT - -signals: - void imageDownloaded(const QPixmap &data); - void fileDownloaded(const QByteArray &data); - void avatarDownloaded(const QImage &img); -}; - -class StateEventProxy : public QObject -{ - Q_OBJECT - -signals: - void stateEventSent(); - void stateEventError(const QString &msg); -}; +#include +#include +Q_DECLARE_METATYPE(mtx::responses::Login) +Q_DECLARE_METATYPE(mtx::responses::Messages) +Q_DECLARE_METATYPE(mtx::responses::Notifications) +Q_DECLARE_METATYPE(mtx::responses::Rooms) Q_DECLARE_METATYPE(mtx::responses::Sync) - -/* - * MatrixClient provides the high level API to communicate with - * a Matrix homeserver. All the responses are returned through signals. - */ -class MatrixClient : public QNetworkAccessManager -{ - Q_OBJECT -public: - MatrixClient(QObject *parent = 0); - - // Client API. - void initialSync() noexcept; - void sync() noexcept; - template - std::shared_ptr sendStateEvent(const EventBody &body, - const QString &roomId, - const QString &stateKey = ""); - void sendRoomMessage(mtx::events::MessageType ty, - int txnId, - const QString &roomid, - const QString &msg, - const QString &mime, - uint64_t media_size, - const QString &url = "") noexcept; - void login(const QString &username, const QString &password) noexcept; - void registerUser(const QString &username, - const QString &password, - const QString &server, - const QString &session = "") noexcept; - void versions() noexcept; - void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url); - //! Download user's avatar. - QSharedPointer fetchUserAvatar(const QUrl &avatarUrl); - void fetchCommunityAvatar(const QString &communityId, const QUrl &avatarUrl); - void fetchCommunityProfile(const QString &communityId); - void fetchCommunityRooms(const QString &communityId); - QSharedPointer downloadImage(const QUrl &url); - QSharedPointer downloadFile(const QUrl &url); - void messages(const QString &room_id, const QString &from_token, int limit = 30) noexcept; - void uploadImage(const QString &roomid, - const QString &filename, - const QSharedPointer data); - void uploadFile(const QString &roomid, - const QString &filename, - const QSharedPointer data); - void uploadAudio(const QString &roomid, - const QString &filename, - const QSharedPointer data); - void uploadVideo(const QString &roomid, - const QString &filename, - const QSharedPointer data); - void uploadFilter(const QString &filter) noexcept; - void joinRoom(const QString &roomIdOrAlias); - void leaveRoom(const QString &roomId); - void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000); - void removeTypingNotification(const QString &roomid); - void readEvent(const QString &room_id, const QString &event_id); - void redactEvent(const QString &room_id, const QString &event_id); - void inviteUser(const QString &room_id, const QString &user); - void createRoom(const mtx::requests::CreateRoom &request); - void getNotifications() noexcept; - - QUrl getHomeServer() { return server_; }; - int transactionId() { return txn_id_; }; - int incrementTransactionId() { return ++txn_id_; }; - - void reset() noexcept; - -public slots: - void getOwnProfile() noexcept; - void getOwnCommunities() noexcept; - void logout() noexcept; - - void setServer(const QString &server) - { - server_ = QUrl(QString("%1://%2").arg(serverProtocol_).arg(server)); - }; - void setAccessToken(const QString &token) { token_ = token; }; - void setNextBatchToken(const QString &next_batch) { next_batch_ = next_batch; }; - -signals: - void loginError(const QString &error); - void registerError(const QString &error); - void registrationFlow(const QString &user, - const QString &pass, - const QString &server, - const QString &session); - void versionError(const QString &error); - - void loggedOut(); - void invitedUser(const QString &room_id, const QString &user); - void roomCreated(const QString &room_id); - - void loginSuccess(const QString &userid, const QString &homeserver, const QString &token); - void registerSuccess(const QString &userid, - const QString &homeserver, - const QString &token); - void versionSuccess(); - void uploadFailed(int statusCode, const QString &msg); - void imageUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void fileUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void audioUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void videoUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size); - void roomAvatarRetrieved(const QString &roomid, - const QPixmap &img, - const QString &url, - const QByteArray &data); - void userAvatarRetrieved(const QString &userId, const QImage &img); - void communityAvatarRetrieved(const QString &communityId, const QPixmap &img); - void communityProfileRetrieved(const QString &communityId, const QJsonObject &profile); - void communityRoomsRetrieved(const QString &communityId, const QJsonObject &rooms); - - // Returned profile data for the user's account. - void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name); - void getOwnCommunitiesResponse(const QList &own_communities); - void initialSyncCompleted(const mtx::responses::Sync &response); - void initialSyncFailed(int status_code = -1); - void syncCompleted(const mtx::responses::Sync &response); - void syncFailed(const QString &msg); - void joinFailed(const QString &msg); - void messageSent(const QString &event_id, const QString &roomid, int txn_id); - void messageSendFailed(const QString &roomid, int txn_id); - void emoteSent(const QString &event_id, const QString &roomid, int txn_id); - void messagesRetrieved(const QString &room_id, const mtx::responses::Messages &msgs); - void joinedRoom(const QString &room_id); - void leftRoom(const QString &room_id); - void roomCreationFailed(const QString &msg); - - void redactionFailed(const QString &error); - void redactionCompleted(const QString &room_id, const QString &event_id); - void invalidToken(); - void syncError(const QString &error); - void notificationsRetrieved(const mtx::responses::Notifications ¬ifications); - -private: - QNetworkReply *makeUploadRequest(QSharedPointer iodev); - QJsonObject getUploadReply(QNetworkReply *reply); - void setupAuth(QNetworkRequest &req) - { - req.setRawHeader("Authorization", QString("Bearer %1").arg(token_).toLocal8Bit()); - } - - // Client API prefix. - QString clientApiUrl_; - - // Media API prefix. - QString mediaApiUrl_; - - // The Matrix server used for communication. - QUrl server_; - - // The access token used for authentication. - QString token_; - - // Increasing transaction ID. - int txn_id_; - - //! Token to be used for the next sync. - QString next_batch_; - //! http or https (default). - QString serverProtocol_; - //! Filter to be send as filter-param for (initial) /sync requests. - QString filter_; -}; +Q_DECLARE_METATYPE(std::string) +Q_DECLARE_METATYPE(std::vector); namespace http { -//! Initialize the http module -void -init(); - -//! Retrieve the client instance. -MatrixClient * +namespace v2 { +mtx::http::Client * client(); } -template -std::shared_ptr -MatrixClient::sendStateEvent(const EventBody &body, const QString &roomId, const QString &stateKey) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/state/%2/%3") - .arg(roomId) - .arg(QString::fromStdString(to_string(EventT))) - .arg(stateKey)); - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - setupAuth(request); - - auto proxy = std::shared_ptr(new StateEventProxy, - [](StateEventProxy *p) { p->deleteLater(); }); - - auto serializedBody = nlohmann::json(body).dump(); - auto reply = put(request, QByteArray(serializedBody.data(), serializedBody.size())); - connect(reply, &QNetworkReply::finished, this, [reply, proxy]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - auto data = reply->readAll(); - - if (status == 0 || status >= 400) { - try { - mtx::errors::Error res = nlohmann::json::parse(data); - emit proxy->stateEventError(QString::fromStdString(res.error)); - } catch (const std::exception &e) { - emit proxy->stateEventError(QString::fromStdString(e.what())); - } - - return; - } - - try { - mtx::responses::EventId res = nlohmann::json::parse(data); - emit proxy->stateEventSent(); - } catch (const std::exception &e) { - emit proxy->stateEventError(QString::fromStdString(e.what())); - } - }); - - return proxy; +//! Initialize the http module +void +init(); } diff --git a/include/RegisterPage.h b/include/RegisterPage.h index f4d97816..d02de7c4 100644 --- a/include/RegisterPage.h +++ b/include/RegisterPage.h @@ -44,6 +44,11 @@ signals: void backButtonClicked(); void errorOccurred(); void registering(); + void registerOk(); + void registerErrorCb(const QString &msg); + void registrationFlow(const std::string &user, + const std::string &pass, + const std::string &session); private slots: void onBackButtonClicked(); diff --git a/include/RoomList.h b/include/RoomList.h index 98d9443e..59b0e865 100644 --- a/include/RoomList.h +++ b/include/RoomList.h @@ -60,6 +60,8 @@ signals: void acceptInvite(const QString &room_id); void declineInvite(const QString &room_id); void roomAvatarChanged(const QString &room_id, const QPixmap &img); + void joinRoom(const QString &room_id); + void updateRoomAvatarCb(const QString &room_id, const QPixmap &img); public slots: void updateRoomAvatar(const QString &roomid, const QPixmap &img); diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h index c679b9b2..af58c2c3 100644 --- a/include/TextInputWidget.h +++ b/include/TextInputWidget.h @@ -129,6 +129,16 @@ public: QColor borderColor() const { return borderColor_; } void setBorderColor(QColor &color) { borderColor_ = color; } + void disableInput() + { + input_->setEnabled(false); + input_->setPlaceholderText(tr("Connection lost. Nheko is trying to re-connect...")); + } + void enableInput() + { + input_->setEnabled(true); + input_->setPlaceholderText(tr("Write a message...")); + } public slots: void openFileSelection(); diff --git a/include/dialogs/ReCaptcha.hpp b/include/dialogs/ReCaptcha.hpp index 1eda40c7..5f47b0eb 100644 --- a/include/dialogs/ReCaptcha.hpp +++ b/include/dialogs/ReCaptcha.hpp @@ -12,7 +12,7 @@ class ReCaptcha : public QWidget Q_OBJECT public: - ReCaptcha(const QString &server, const QString &session, QWidget *parent = nullptr); + ReCaptcha(const QString &session, QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; diff --git a/include/dialogs/RoomSettings.hpp b/include/dialogs/RoomSettings.hpp index 375a531e..9a01d5c9 100644 --- a/include/dialogs/RoomSettings.hpp +++ b/include/dialogs/RoomSettings.hpp @@ -30,6 +30,9 @@ public: signals: void nameChanged(const QString &roomName); + void nameEventSentCb(const QString &newName); + void topicEventSentCb(); + void stateEventErrorCb(const QString &msg); private: QString roomId_; diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h index 9997ec1d..4dcca1a5 100644 --- a/include/timeline/TimelineItem.h +++ b/include/timeline/TimelineItem.h @@ -197,12 +197,24 @@ public: void sendReadReceipt() const { if (!event_id_.isEmpty()) - http::client()->readEvent(room_id_, event_id_); + http::v2::client()->read_event( + room_id_.toStdString(), + event_id_.toStdString(), + [this](mtx::http::RequestErr err) { + if (err) { + qWarning() << QString("failed to read_event (%1, %2)") + .arg(room_id_, event_id_); + } + }); } //! Add a user avatar for this event. void addAvatar(); +signals: + void eventRedacted(const QString &event_id); + void redactionFailed(const QString &msg); + protected: void paintEvent(QPaintEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override; diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index e6e35ccb..30af97fb 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -18,7 +18,6 @@ #pragma once #include -#include #include #include #include @@ -42,31 +41,13 @@ struct DescInfo; struct PendingMessage { mtx::events::MessageType ty; - int txn_id; + std::string txn_id; QString body; QString filename; QString mime; uint64_t media_size; QString event_id; TimelineItem *widget; - - PendingMessage(mtx::events::MessageType ty, - int txn_id, - QString body, - QString filename, - QString mime, - uint64_t media_size, - QString event_id, - TimelineItem *widget) - : ty(ty) - , txn_id(txn_id) - , body(body) - , filename(filename) - , mime(mime) - , media_size(media_size) - , event_id(event_id) - , widget(widget) - {} }; // In which place new TimelineItems should be inserted. @@ -129,7 +110,7 @@ public: const QString &filename, const QString &mime, uint64_t size); - void updatePendingMessage(int txn_id, QString event_id); + void updatePendingMessage(const std::string &txn_id, const QString &event_id); void scrollDown(); QLabel *createDateSeparator(QDateTime datetime); @@ -142,18 +123,21 @@ public slots: void fetchHistory(); // Add old events at the top of the timeline. - void addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs); + void addBackwardsEvents(const mtx::responses::Messages &msgs); // Whether or not the initial batch has been loaded. bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; } - void handleFailedMessage(int txnid); + void handleFailedMessage(const std::string &txn_id); private slots: void sendNextPendingMessage(); signals: void updateLastTimelineMessage(const QString &user, const DescInfo &info); + void messagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(const std::string &txn_id); + void messageSent(const std::string &txn_id, const QString &event_id); protected: void paintEvent(QPaintEvent *event) override; @@ -165,6 +149,13 @@ private: QWidget *relativeWidget(TimelineItem *item, int dt) const; + //! Callback for all message sending. + void sendRoomMessageHandler(const std::string &txn_id, + const mtx::responses::EventId &res, + mtx::http::RequestErr err); + + //! Call the /messages endpoint to fill the timeline. + void getMessages(); //! HACK: Fixing layout flickering when adding to the bottom //! of the timeline. void pushTimelineItem(TimelineItem *item) @@ -230,8 +221,10 @@ private: uint64_t origin_server_ts, TimelineDirection direction); - bool isPendingMessage(const QString &txnid, const QString &sender, const QString &userid); - void removePendingMessage(const QString &txnid); + bool isPendingMessage(const std::string &txn_id, + const QString &sender, + const QString &userid); + void removePendingMessage(const std::string &txn_id); bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } @@ -320,9 +313,15 @@ TimelineView::addUserMessage(const QString &url, // Keep track of the sender and the timestamp of the current message. saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - int txn_id = http::client()->incrementTransactionId(); + PendingMessage message; + message.ty = MsgType; + message.txn_id = mtx::client::utils::random_token(); + message.body = url; + message.filename = trimmed; + message.mime = mime; + message.media_size = size; + message.widget = view_item; - PendingMessage message(MsgType, txn_id, url, trimmed, mime, size, "", view_item); handleNewUserMessage(message); } @@ -351,10 +350,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio const auto event_id = QString::fromStdString(event.event_id); const auto sender = QString::fromStdString(event.sender); - const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); - if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) || + const auto txn_id = event.unsigned_data.transaction_id; + if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || isDuplicate(event_id)) { - removePendingMessage(txnid); + removePendingMessage(txn_id); return nullptr; } @@ -376,10 +375,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio const auto event_id = QString::fromStdString(event.event_id); const auto sender = QString::fromStdString(event.sender); - const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); - if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) || + const auto txn_id = event.unsigned_data.transaction_id; + if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || isDuplicate(event_id)) { - removePendingMessage(txnid); + removePendingMessage(txn_id); return nullptr; } diff --git a/include/timeline/TimelineViewManager.h b/include/timeline/TimelineViewManager.h index 308b83aa..9e31ecbf 100644 --- a/include/timeline/TimelineViewManager.h +++ b/include/timeline/TimelineViewManager.h @@ -56,6 +56,8 @@ signals: void updateRoomsLastMessage(const QString &user, const DescInfo &info); public slots: + void removeTimelineEvent(const QString &room_id, const QString &event_id); + void setHistoryView(const QString &room_id); void queueTextMessage(const QString &msg); void queueEmoteMessage(const QString &msg); @@ -80,10 +82,6 @@ public slots: const QString &mime, uint64_t dsize); -private slots: - void messageSent(const QString &eventid, const QString &roomid, int txnid); - void messageSendFailed(const QString &roomid, int txnid); - private: //! Check if the given room id is managed by a TimelineView. bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } diff --git a/include/timeline/widgets/AudioItem.h b/include/timeline/widgets/AudioItem.h index b31385d1..7b0781a2 100644 --- a/include/timeline/widgets/AudioItem.h +++ b/include/timeline/widgets/AudioItem.h @@ -69,9 +69,14 @@ protected: void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *event) override; +signals: + void fileDownloadedCb(const QByteArray &data); + +private slots: + void fileDownloaded(const QByteArray &data); + private: void init(); - void fileDownloaded(const QByteArray &data); enum class AudioState { diff --git a/include/timeline/widgets/FileItem.h b/include/timeline/widgets/FileItem.h index 09181d32..66543e79 100644 --- a/include/timeline/widgets/FileItem.h +++ b/include/timeline/widgets/FileItem.h @@ -52,15 +52,20 @@ public: QColor iconColor() const { return iconColor_; } QColor backgroundColor() const { return backgroundColor_; } +signals: + void fileDownloadedCb(const QByteArray &data); + protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; +private slots: + void fileDownloaded(const QByteArray &data); + private: void openUrl(); void init(); - void fileDownloaded(const QByteArray &data); QUrl url_; QString text_; diff --git a/include/timeline/widgets/ImageItem.h b/include/timeline/widgets/ImageItem.h index b17b2d8b..e9d823f4 100644 --- a/include/timeline/widgets/ImageItem.h +++ b/include/timeline/widgets/ImageItem.h @@ -40,13 +40,17 @@ public: uint64_t size, QWidget *parent = nullptr); - void setImage(const QPixmap &image); - QSize sizeHint() const override; public slots: //! Show a save as dialog for the image. void saveAs(); + void setImage(const QPixmap &image); + void saveImage(const QString &filename, const QByteArray &data); + +signals: + void imageDownloaded(const QPixmap &img); + void imageSaved(const QString &filename, const QByteArray &data); protected: void paintEvent(QPaintEvent *event) override; @@ -57,7 +61,9 @@ protected: bool isInteractive_ = true; private: + void init(); void openUrl(); + void downloadMedia(const QUrl &url); int max_width_ = 500; int max_height_ = 300; diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc index 49e52a82..ad095023 100644 --- a/src/AvatarProvider.cc +++ b/src/AvatarProvider.cc @@ -16,17 +16,17 @@ */ #include -#include +#include #include "AvatarProvider.h" #include "Cache.h" +#include "Logging.hpp" #include "MatrixClient.h" +namespace AvatarProvider { + void -AvatarProvider::resolve(const QString &room_id, - const QString &user_id, - QObject *receiver, - std::function callback) +resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback) { const auto key = QString("%1 %2").arg(room_id).arg(user_id); const auto avatarUrl = Cache::avatarUrl(room_id, user_id); @@ -43,24 +43,30 @@ AvatarProvider::resolve(const QString &room_id, return; } - auto proxy = http::client()->fetchUserAvatar(avatarUrl); + auto proxy = std::make_shared(); + QObject::connect(proxy.get(), + &AvatarProxy::avatarDownloaded, + receiver, + [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); - if (proxy.isNull()) - return; + mtx::http::ThumbOpts opts; + opts.mxc_url = avatarUrl.toStdString(); - connect(proxy.data(), - &DownloadMediaProxy::avatarDownloaded, - receiver, - [user_id, proxy, callback, avatarUrl](const QImage &img) { - proxy->deleteLater(); - QtConcurrent::run([img, avatarUrl]() { - QByteArray data; - QBuffer buffer(&data); - buffer.open(QIODevice::WriteOnly); - img.save(&buffer, "PNG"); + http::v2::client()->get_thumbnail( + opts, + [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to download avatar: {} - ({} {})", + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } - cache::client()->saveImage(avatarUrl, data); - }); - callback(img); - }); + cache::client()->saveImage(opts.mxc_url, res); + + auto data = QByteArray(res.data(), res.size()); + emit proxy->avatarDownloaded(data); + }); +} } diff --git a/src/Cache.cc b/src/Cache.cc index c055ab05..2a555425 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -27,6 +26,7 @@ #include #include "Cache.h" +#include "Logging.hpp" #include "Utils.h" //! Should be changed when a breaking change occurs in the cache format. @@ -62,6 +62,14 @@ namespace cache { void init(const QString &user_id) { + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType>(); + if (!instance_) instance_ = std::make_unique(user_id); } @@ -88,7 +96,7 @@ Cache::Cache(const QString &userId, QObject *parent) void Cache::setup() { - qDebug() << "Setting up cache"; + log::db()->debug("setting up cache"); auto statePath = QString("%1/%2/state") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) @@ -105,7 +113,7 @@ Cache::setup() env_.set_max_dbs(1024UL); if (isInitial) { - qDebug() << "First time initializing LMDB"; + log::db()->info("initializing LMDB"); if (!QDir().mkpath(statePath)) { throw std::runtime_error( @@ -121,7 +129,7 @@ Cache::setup() std::string(e.what())); } - qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what(); + log::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); QDir stateDir(statePath); @@ -142,29 +150,34 @@ Cache::setup() readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); txn.commit(); - - qRegisterMetaType(); } void -Cache::saveImage(const QString &url, const QByteArray &image) +Cache::saveImage(const std::string &url, const std::string &img_data) { - auto key = url.toUtf8(); + if (url.empty() || img_data.empty()) + return; try { auto txn = lmdb::txn::begin(env_); lmdb::dbi_put(txn, mediaDb_, - lmdb::val(key.data(), key.size()), - lmdb::val(image.data(), image.size())); + lmdb::val(url.data(), url.size()), + lmdb::val(img_data.data(), img_data.size())); txn.commit(); } catch (const lmdb::error &e) { - qCritical() << "saveImage:" << e.what(); + log::db()->critical("saveImage: {}", e.what()); } } +void +Cache::saveImage(const QString &url, const QByteArray &image) +{ + saveImage(url.toStdString(), std::string(image.constData(), image.length())); +} + QByteArray Cache::image(lmdb::txn &txn, const std::string &url) const { @@ -180,7 +193,7 @@ Cache::image(lmdb::txn &txn, const std::string &url) const return QByteArray(image.data(), image.size()); } catch (const lmdb::error &e) { - qCritical() << "image:" << e.what() << QString::fromStdString(url); + log::db()->critical("image: {}, {}", e.what(), url); } return QByteArray(); @@ -208,7 +221,7 @@ Cache::image(const QString &url) const return QByteArray(image.data(), image.size()); } catch (const lmdb::error &e) { - qCritical() << "image:" << e.what() << url; + log::db()->critical("image: {} {}", e.what(), url.toStdString()); } return QByteArray(); @@ -271,7 +284,7 @@ Cache::isInitialized() const return res; } -QString +std::string Cache::nextBatchToken() const { auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); @@ -281,13 +294,13 @@ Cache::nextBatchToken() const txn.commit(); - return QString::fromUtf8(token.data(), token.size()); + return std::string(token.data(), token.size()); } void Cache::deleteData() { - qInfo() << "Deleting cache data"; + log::db()->info("deleting data"); if (!cacheDirectory_.isEmpty()) QDir(cacheDirectory_).removeRecursively(); @@ -309,8 +322,9 @@ Cache::isFormatValid() std::string stored_version(current_version.data(), current_version.size()); if (stored_version != CURRENT_CACHE_FORMAT_VERSION) { - qWarning() << "Stored format version" << QString::fromStdString(stored_version); - qWarning() << "There are breaking changes in the cache format."; + log::db()->warn("breaking changes in the cache format. stored: {}, current: {}", + stored_version, + CURRENT_CACHE_FORMAT_VERSION); return false; } @@ -360,7 +374,7 @@ Cache::readReceipts(const QString &event_id, const QString &room_id) } } catch (const lmdb::error &e) { - qCritical() << "readReceipts:" << e.what(); + log::db()->critical("readReceipts: {}", e.what()); } return receipts; @@ -410,7 +424,7 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei lmdb::val(merged_receipts.data(), merged_receipts.size())); } catch (const lmdb::error &e) { - qCritical() << "updateReadReceipts:" << e.what(); + log::db()->critical("updateReadReceipts: {}", e.what()); } } } @@ -568,9 +582,9 @@ Cache::singleRoomInfo(const std::string &room_id) return tmp; } catch (const json::exception &e) { - qWarning() - << "failed to parse room info:" << QString::fromStdString(room_id) - << QString::fromStdString(std::string(data.data(), data.size())); + log::db()->warn("failed to parse room info: room_id ({}), {}", + room_id, + std::string(data.data(), data.size())); } } @@ -600,9 +614,9 @@ Cache::getRoomInfo(const std::vector &rooms) room_info.emplace(QString::fromStdString(room), std::move(tmp)); } catch (const json::exception &e) { - qWarning() - << "failed to parse room info:" << QString::fromStdString(room) - << QString::fromStdString(std::string(data.data(), data.size())); + log::db()->warn("failed to parse room info: room_id ({}), {}", + room, + std::string(data.data(), data.size())); } } else { // Check if the room is an invite. @@ -615,10 +629,10 @@ Cache::getRoomInfo(const std::vector &rooms) room_info.emplace(QString::fromStdString(room), std::move(tmp)); } catch (const json::exception &e) { - qWarning() << "failed to parse room info for invite:" - << QString::fromStdString(room) - << QString::fromStdString( - std::string(data.data(), data.size())); + log::db()->warn( + "failed to parse room info for invite: room_id ({}), {}", + room, + std::string(data.data(), data.size())); } } } @@ -703,7 +717,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn, return QString::fromStdString(msg.content.url); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } } @@ -726,7 +740,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn, cursor.close(); return QString::fromStdString(m.avatar_url); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse member info: {}", e.what()); } } @@ -753,7 +767,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) if (!msg.content.name.empty()) return QString::fromStdString(msg.content.name); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse m.room.name event: {}", e.what()); } } @@ -768,7 +782,8 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) if (!msg.content.alias.empty()) return QString::fromStdString(msg.content.alias); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse m.room.canonical_alias event: {}", + e.what()); } } @@ -784,7 +799,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) try { members.emplace(user_id, json::parse(member_data)); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse member info: {}", e.what()); } ii++; @@ -828,7 +843,7 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) json::parse(std::string(event.data(), event.size())); return msg.content.join_rule; } catch (const json::exception &e) { - qWarning() << e.what(); + log::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); } } return JoinRule::Knock; @@ -850,7 +865,7 @@ Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) json::parse(std::string(event.data(), event.size())); return msg.content.guest_access == AccessState::CanJoin; } catch (const json::exception &e) { - qWarning() << e.what(); + log::db()->warn("failed to parse m.room.guest_access event: {}", e.what()); } } return false; @@ -874,7 +889,7 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) if (!msg.content.topic.empty()) return QString::fromStdString(msg.content.topic); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse m.room.topic event: {}", e.what()); } } @@ -897,7 +912,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members json::parse(std::string(event.data(), event.size())); return QString::fromStdString(msg.content.name); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse m.room.name event: {}", e.what()); } } @@ -914,7 +929,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members return QString::fromStdString(tmp.name); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse member info: {}", e.what()); } } @@ -939,7 +954,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me json::parse(std::string(event.data(), event.size())); return QString::fromStdString(msg.content.url); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } } @@ -956,7 +971,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me return QString::fromStdString(tmp.avatar_url); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse member info: {}", e.what()); } } @@ -981,7 +996,7 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) json::parse(std::string(event.data(), event.size())); return QString::fromStdString(msg.content.topic); } catch (const json::exception &e) { - qWarning() << QString::fromStdString(e.what()); + log::db()->warn("failed to parse m.room.topic event: {}", e.what()); } } @@ -1017,8 +1032,9 @@ Cache::getRoomAvatar(const std::string &room_id) return QImage(); } } catch (const json::exception &e) { - qWarning() << "failed to parse room info" << e.what() - << QString::fromStdString(std::string(response.data(), response.size())); + log::db()->warn("failed to parse room info: {}, {}", + e.what(), + std::string(response.data(), response.size())); } if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) { @@ -1054,7 +1070,7 @@ void Cache::populateMembers() { auto rooms = joinedRooms(); - qDebug() << "loading" << rooms.size() << "rooms"; + log::db()->info("loading {} rooms", rooms.size()); auto txn = lmdb::txn::begin(env_); @@ -1182,7 +1198,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_ QString::fromStdString(tmp.name), QImage::fromData(image(txn, tmp.avatar_url))}); } catch (const json::exception &e) { - qWarning() << e.what(); + log::db()->warn("{}", e.what()); } currentIndex += 1; @@ -1253,7 +1269,7 @@ Cache::hasEnoughPowerLevel(const std::vector &eventTypes std::min(min_event_level, (uint16_t)msg.content.state_level(to_string(ty))); } catch (const json::exception &e) { - qWarning() << "hasEnoughPowerLevel: " << e.what(); + log::db()->warn("failed to parse m.room.power_levels event: {}", e.what()); } } diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 9ae860fb..64ce69d6 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -16,13 +16,13 @@ */ #include -#include #include #include #include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" +#include "Logging.hpp" #include "MainWindow.h" #include "MatrixClient.h" #include "OverlayModal.h" @@ -43,13 +43,12 @@ #include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" -constexpr int SYNC_RETRY_TIMEOUT = 40 * 1000; -constexpr int INITIAL_SYNC_RETRY_TIMEOUT = 240 * 1000; - -ChatPage *ChatPage::instance_ = nullptr; +ChatPage *ChatPage::instance_ = nullptr; +constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) : QWidget(parent) + , isConnected_(true) , userSettings_{userSettings} { setObjectName("chatPage"); @@ -78,13 +77,12 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) sidebarActions_ = new SideBarActions(this); connect( sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage); - connect( - sidebarActions_, &SideBarActions::joinRoom, http::client(), &MatrixClient::joinRoom); - connect( - sidebarActions_, &SideBarActions::createRoom, http::client(), &MatrixClient::createRoom); + connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); + connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); user_info_widget_ = new UserInfoWidget(sideBar_); room_list_ = new RoomList(userSettings_, sideBar_); + connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); sideBarLayout_->addWidget(user_info_widget_); sideBarLayout_->addWidget(room_list_); @@ -107,6 +105,11 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) contentLayout_->addWidget(top_bar_); contentLayout_->addWidget(view_manager_); + connect(this, + &ChatPage::removeTimelineEvent, + view_manager_, + &TimelineViewManager::removeTimelineEvent); + // Splitter splitter->addWidget(sideBar_); splitter->addWidget(content_); @@ -120,16 +123,81 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) typingRefresher_ = new QTimer(this); typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); + connect(this, &ChatPage::connectionLost, this, [this]() { + log::net()->info("connectivity lost"); + isConnected_ = false; + http::v2::client()->shutdown(); + text_input_->disableInput(); + }); + connect(this, &ChatPage::connectionRestored, this, [this]() { + log::net()->info("trying to re-connect"); + text_input_->enableInput(); + isConnected_ = true; + + // Drop all pending connections. + http::v2::client()->shutdown(); + trySync(); + }); + + connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); + connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { + if (http::v2::client()->access_token().empty()) { + connectivityTimer_.stop(); + return; + } + + http::v2::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + emit connectionLost(); + return; + } + + if (!isConnected_) + emit connectionRestored(); + }); + }); + + connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); connect(user_info_widget_, &UserInfoWidget::logout, this, [this]() { - http::client()->logout(); + http::v2::client()->logout([this](const mtx::responses::Logout &, + mtx::http::RequestErr err) { + if (err) { + // TODO: handle special errors + emit contentLoaded(); + log::net()->warn("failed to logout: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + emit loggedOut(); + }); + emit showOverlayProgressBar(); }); - connect(http::client(), &MatrixClient::loggedOut, this, &ChatPage::logout); connect(top_bar_, &TopRoomBar::inviteUsers, this, [this](QStringList users) { + const auto room_id = current_room_.toStdString(); + for (int ii = 0; ii < users.size(); ++ii) { - QTimer::singleShot(ii * 1000, this, [this, ii, users]() { - http::client()->inviteUser(current_room_, users.at(ii)); + QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { + const auto user = users.at(ii); + + http::v2::client()->invite_user( + room_id, + user.toStdString(), + [this, user](const mtx::responses::RoomInvite &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + QString("Failed to invite user: %1").arg(user)); + return; + } + + emit showNotification( + QString("Invited user: %1").arg(user)); + }); }); } }); @@ -155,36 +223,30 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) { view_manager_->addRoom(room_id); - http::client()->joinRoom(room_id); + joinRoom(room_id); room_list_->removeRoom(room_id, currentRoom() == room_id); }); connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) { - http::client()->leaveRoom(room_id); + leaveRoom(room_id); room_list_->removeRoom(room_id, currentRoom() == room_id); }); - connect(text_input_, &TextInputWidget::startedTyping, this, [this]() { - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - typingRefresher_->start(); - http::client()->sendTypingNotification(current_room_); - }); - + connect( + text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications); + connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications); connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() { if (!userSettings_->isTypingNotificationsEnabled()) return; typingRefresher_->stop(); - http::client()->removeTypingNotification(current_room_); - }); - - connect(typingRefresher_, &QTimer::timeout, this, [this]() { - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - http::client()->sendTypingNotification(current_room_); + http::v2::client()->stop_typing( + current_room_.toStdString(), [](mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to stop typing notifications: {}", + err->matrix_error.error); + } + }); }); connect(view_manager_, @@ -207,142 +269,242 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) view_manager_, SLOT(queueEmoteMessage(const QString &))); - connect(text_input_, - &TextInputWidget::sendJoinRoomRequest, - http::client(), - &MatrixClient::joinRoom); + connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); connect(text_input_, &TextInputWidget::uploadImage, this, - [this](QSharedPointer data, const QString &fn) { - http::client()->uploadImage(current_room_, fn, data); + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload image. Please try again.")); + log::net()->warn("failed to upload image: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit imageUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); connect(text_input_, &TextInputWidget::uploadFile, this, - [this](QSharedPointer data, const QString &fn) { - http::client()->uploadFile(current_room_, fn, data); + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload file. Please try again.")); + log::net()->warn("failed to upload file: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit fileUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); connect(text_input_, &TextInputWidget::uploadAudio, this, - [this](QSharedPointer data, const QString &fn) { - http::client()->uploadAudio(current_room_, fn, data); + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload audio. Please try again.")); + log::net()->warn("failed to upload audio: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit audioUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); connect(text_input_, &TextInputWidget::uploadVideo, this, - [this](QSharedPointer data, const QString &fn) { - http::client()->uploadVideo(current_room_, fn, data); + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::v2::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload video. Please try again.")); + log::net()->warn("failed to upload video: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit videoUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); }); - connect( - http::client(), &MatrixClient::roomCreationFailed, this, &ChatPage::showNotification); - connect(http::client(), &MatrixClient::joinFailed, this, &ChatPage::showNotification); - connect(http::client(), &MatrixClient::uploadFailed, this, [this](int, const QString &msg) { + connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { text_input_->hideUploadSpinner(); emit showNotification(msg); }); - connect( - http::client(), - &MatrixClient::imageUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage(roomid, filename, url, mime, dsize); - }); - connect( - http::client(), - &MatrixClient::fileUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); - }); - connect( - http::client(), - &MatrixClient::audioUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); - }); - connect( - http::client(), - &MatrixClient::videoUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, uint64_t dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); - }); + connect(this, + &ChatPage::imageUploaded, + this, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueImageMessage(roomid, filename, url, mime, dsize); + }); + connect(this, + &ChatPage::fileUploaded, + this, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); + }); + connect(this, + &ChatPage::audioUploaded, + this, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); + }); + connect(this, + &ChatPage::videoUploaded, + this, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); + }); connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); - connect(http::client(), - &MatrixClient::initialSyncCompleted, - this, - &ChatPage::initialSyncCompleted); - connect( - http::client(), &MatrixClient::initialSyncFailed, this, &ChatPage::retryInitialSync); - connect(http::client(), &MatrixClient::syncCompleted, this, &ChatPage::syncCompleted); - connect(http::client(), - &MatrixClient::getOwnProfileResponse, - this, - &ChatPage::updateOwnProfileInfo); - connect(http::client(), - SIGNAL(getOwnCommunitiesResponse(QList)), - this, - SLOT(updateOwnCommunitiesInfo(QList))); - connect(http::client(), - &MatrixClient::communityProfileRetrieved, - this, - [this](QString communityId, QJsonObject profile) { - communities_[communityId]->parseProfile(profile); - }); - connect(http::client(), - &MatrixClient::communityRoomsRetrieved, - this, - [this](QString communityId, QJsonObject rooms) { - communities_[communityId]->parseRooms(rooms); + // connect(http::client(), + // SIGNAL(getOwnCommunitiesResponse(QList)), + // this, + // SLOT(updateOwnCommunitiesInfo(QList))); + // connect(http::client(), + // &MatrixClient::communityProfileRetrieved, + // this, + // [this](QString communityId, QJsonObject profile) { + // communities_[communityId]->parseProfile(profile); + // }); + // connect(http::client(), + // &MatrixClient::communityRoomsRetrieved, + // this, + // [this](QString communityId, QJsonObject rooms) { + // communities_[communityId]->parseRooms(rooms); - if (communityId == current_community_) { - if (communityId == "world") { - room_list_->setFilterRooms(false); - } else { - room_list_->setRoomFilter( - communities_[communityId]->getRoomList()); - } - } - }); + // if (communityId == current_community_) { + // if (communityId == "world") { + // room_list_->setFilterRooms(false); + // } else { + // room_list_->setRoomFilter( + // communities_[communityId]->getRoomList()); + // } + // } + // }); - connect(http::client(), &MatrixClient::joinedRoom, this, [this](const QString &room_id) { - emit showNotification("You joined the room."); - - // We remove any invites with the same room_id. - try { - cache::client()->removeInvite(room_id.toStdString()); - } catch (const lmdb::error &e) { - emit showNotification(QString("Failed to remove invite: %1") - .arg(QString::fromStdString(e.what()))); - } - }); - connect(http::client(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom); - connect(http::client(), &MatrixClient::invitedUser, this, [this](QString, QString user) { - emit showNotification(QString("Invited user %1").arg(user)); - }); - connect(http::client(), &MatrixClient::roomCreated, this, [this](QString room_id) { - emit showNotification(QString("Room %1 created").arg(room_id)); - }); - connect(http::client(), &MatrixClient::redactionFailed, this, [this](const QString &error) { - emit showNotification(QString("Message redaction failed: %1").arg(error)); - }); - connect(http::client(), - &MatrixClient::notificationsRetrieved, - this, - &ChatPage::sendDesktopNotifications); + connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); + connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications); showContentTimer_ = new QTimer(this); showContentTimer_->setSingleShot(true); @@ -361,20 +523,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) } }); - initialSyncTimer_ = new QTimer(this); - connect(initialSyncTimer_, &QTimer::timeout, this, [this]() { retryInitialSync(); }); - - syncTimeoutTimer_ = new QTimer(this); - connect(syncTimeoutTimer_, &QTimer::timeout, this, [this]() { - if (http::client()->getHomeServer().isEmpty()) { - syncTimeoutTimer_->stop(); - return; - } - - qDebug() << "Sync took too long. Retrying..."; - http::client()->sync(); - }); - connect(communitiesList_, &CommunitiesList::communityChanged, this, @@ -394,12 +542,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, &ChatPage::setGroupViewState); - connect(this, &ChatPage::continueSync, this, [this](const QString &next_batch) { - syncTimeoutTimer_->start(SYNC_RETRY_TIMEOUT); - http::client()->setNextBatchToken(next_batch); - http::client()->sync(); - }); - connect(this, &ChatPage::startConsesusTimer, this, [this]() { consensusTimer_->start(CONSENSUS_TIMEOUT); showContentTimer_->start(SHOW_CONTENT_TIMEOUT); @@ -418,7 +560,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) try { room_list_->cleanupInvites(cache::client()->invites()); } catch (const lmdb::error &e) { - qWarning() << "failed to retrieve invites" << e.what(); + log::db()->error("failed to retrieve invites: {}", e.what()); } view_manager_->initialize(rooms); @@ -437,7 +579,20 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) } if (hasNotifications) - http::client()->getNotifications(); + http::v2::client()->notifications( + 5, + [this](const mtx::responses::Notifications &res, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to retrieve notifications: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit notificationsRetrieved(std::move(res)); + }); }); connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); connect( @@ -446,12 +601,21 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) changeTopRoomInfo(currentRoom()); }); - instance_ = this; + // Callbacks to update the user info (top left corner of the page). + connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar); + connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) { + QSettings settings; + auto userid = settings.value("auth/user_id").toString(); + user_info_widget_->setUserId(userid); + user_info_widget_->setDisplayName(name); + }); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType(); - qRegisterMetaType>(); + connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync); + connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync); + + connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); + + instance_ = this; } void @@ -462,6 +626,19 @@ ChatPage::logout() resetUI(); emit closing(); + connectivityTimer_.stop(); +} + +void +ChatPage::dropToLoginPage(const QString &msg) +{ + deleteConfigs(); + resetUI(); + + http::v2::client()->shutdown(); + connectivityTimer_.stop(); + + emit showLoginPage(msg); } void @@ -490,17 +667,66 @@ ChatPage::deleteConfigs() settings.endGroup(); cache::client()->deleteData(); - - http::client()->reset(); + http::v2::client()->clear(); } void ChatPage::bootstrap(QString userid, QString homeserver, QString token) { - http::client()->setServer(homeserver); - http::client()->setAccessToken(token); - http::client()->getOwnProfile(); - http::client()->getOwnCommunities(); + using namespace mtx::identifiers; + + try { + http::v2::client()->set_user(parse(userid.toStdString())); + } catch (const std::invalid_argument &e) { + log::main()->critical("bootstrapped with invalid user_id: {}", + userid.toStdString()); + } + + http::v2::client()->set_server(homeserver.toStdString()); + http::v2::client()->set_access_token(token.toStdString()); + http::v2::client()->get_profile( + userid.toStdString(), + [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to retrieve own profile info"); + return; + } + + emit setUserDisplayName(QString::fromStdString(res.display_name)); + + if (cache::client()) { + auto data = cache::client()->image(res.avatar_url); + if (!data.isNull()) { + emit setUserAvatar(QImage::fromData(data)); + return; + } + } + + if (res.avatar_url.empty()) + return; + + http::v2::client()->download( + res.avatar_url, + [this, res](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to download user avatar: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + if (cache::client()) + cache::client()->saveImage(res.avatar_url, data); + + emit setUserAvatar( + QImage::fromData(QByteArray(data.data(), data.size()))); + }); + }); + // TODO http::client()->getOwnCommunities(); cache::init(userid); @@ -518,62 +744,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) return; } } catch (const lmdb::error &e) { - qCritical() << "Cache failure" << e.what(); + log::db()->critical("failure during boot: {}", e.what()); cache::client()->deleteData(); - qInfo() << "Falling back to initial sync ..."; + log::net()->info("falling back to initial sync"); } - http::client()->initialSync(); - - initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT); -} - -void -ChatPage::syncCompleted(const mtx::responses::Sync &response) -{ - syncTimeoutTimer_->stop(); - - QtConcurrent::run([this, res = std::move(response)]() { - try { - cache::client()->saveState(res); - emit syncUI(res.rooms); - - auto updates = cache::client()->roomUpdates(res); - - emit syncTopBar(updates); - emit syncRoomlist(updates); - - } catch (const lmdb::error &e) { - std::cout << "save cache error:" << e.what() << '\n'; - // TODO: retry sync. - return; - } - - emit continueSync(cache::client()->nextBatchToken()); - }); -} - -void -ChatPage::initialSyncCompleted(const mtx::responses::Sync &response) -{ - initialSyncTimer_->stop(); - - qDebug() << "initial sync completed"; - - QtConcurrent::run([this, res = std::move(response)]() { - try { - cache::client()->saveState(res); - emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::client()->roomInfo()); - } catch (const lmdb::error &e) { - qWarning() << "cache error:" << QString::fromStdString(e.what()); - emit retryInitialSync(); - return; - } - - emit continueSync(cache::client()->nextBatchToken()); - emit contentLoaded(); - }); + tryInitialSync(); } void @@ -585,41 +761,6 @@ ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) top_bar_->updateRoomAvatar(img.toImage()); } -void -ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name) -{ - QSettings settings; - auto userid = settings.value("auth/user_id").toString(); - - user_info_widget_->setUserId(userid); - user_info_widget_->setDisplayName(display_name); - - if (!avatar_url.isValid()) - return; - - if (cache::client()) { - auto data = cache::client()->image(avatar_url.toString()); - if (!data.isNull()) { - user_info_widget_->setAvatar(QImage::fromData(data)); - return; - } - } - - auto proxy = http::client()->fetchUserAvatar(avatar_url); - - if (proxy.isNull()) - return; - - proxy->setParent(this); - connect(proxy.data(), - &DownloadMediaProxy::avatarDownloaded, - this, - [this, proxy](const QImage &img) { - proxy->deleteLater(); - user_info_widget_->setAvatar(img); - }); -} - void ChatPage::updateOwnCommunitiesInfo(const QList &own_communities) { @@ -636,7 +777,7 @@ void ChatPage::changeTopRoomInfo(const QString &room_id) { if (room_id.isEmpty()) { - qWarning() << "can't switch to empty room_id"; + log::main()->warn("cannot switch to empty room_id"); return; } @@ -660,7 +801,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) top_bar_->updateRoomAvatar(img); } catch (const lmdb::error &e) { - qWarning() << "failed to change top bar room info" << e.what(); + log::main()->error("failed to change top bar room info: {}", e.what()); } current_room_ = room_id; @@ -681,7 +822,7 @@ ChatPage::showUnreadMessageNotification(int count) void ChatPage::loadStateFromCache() { - qDebug() << "restoring state from cache"; + log::db()->info("restoring state from cache"); QtConcurrent::run([this]() { try { @@ -696,7 +837,7 @@ ChatPage::loadStateFromCache() } // Start receiving events. - emit continueSync(cache::client()->nextBatchToken()); + emit trySyncCb(); // Check periodically if the timelines have been loaded. emit startConsesusTimer(); @@ -740,7 +881,7 @@ ChatPage::removeRoom(const QString &room_id) cache::client()->removeRoom(room_id); cache::client()->removeInvite(room_id.toStdString()); } catch (const lmdb::error &e) { - qCritical() << "The cache couldn't be updated: " << e.what(); + log::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. } @@ -823,33 +964,6 @@ ChatPage::setGroupViewState(bool isEnabled) communitiesList_->show(); } -void -ChatPage::retryInitialSync(int status_code) -{ - initialSyncTimer_->stop(); - - if (http::client()->getHomeServer().isEmpty()) { - deleteConfigs(); - resetUI(); - emit showLoginPage("Sync error. Please try again."); - return; - } - - // Retry on Bad-Gateway & Gateway-Timeout errors - if (status_code == -1 || status_code == 504 || status_code == 502 || status_code == 524) { - qWarning() << "retrying initial sync"; - - http::client()->initialSync(); - initialSyncTimer_->start(INITIAL_SYNC_RETRY_TIMEOUT); - } else { - // Drop into the login screen. - deleteConfigs(); - resetUI(); - - emit showLoginPage(QString("Sync error %1. Please try again.").arg(status_code)); - } -} - void ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count) { @@ -886,7 +1000,197 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) utils::event_body(item.event)); } } catch (const lmdb::error &e) { - qWarning() << e.what(); + log::db()->warn("error while sending desktop notification: {}", e.what()); } } } + +void +ChatPage::tryInitialSync() +{ + mtx::http::SyncOpts opts; + opts.timeout = 0; + + log::net()->info("trying initial sync"); + + http::v2::client()->sync( + opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast(err->status_code); + + log::net()->error("sync error: {} {}", status_code, err_code); + + switch (status_code) { + case 502: + case 504: + case 524: { + emit tryInitialSyncCb(); + return; + } + default: { + emit dropToLoginPageCb(msg); + return; + } + } + } + + log::net()->info("initial sync completed"); + + try { + cache::client()->saveState(res); + emit initializeViews(std::move(res.rooms)); + emit initializeRoomList(cache::client()->roomInfo()); + } catch (const lmdb::error &e) { + log::db()->error("{}", e.what()); + emit tryInitialSyncCb(); + return; + } + + emit trySyncCb(); + emit contentLoaded(); + }); +} + +void +ChatPage::trySync() +{ + mtx::http::SyncOpts opts; + + if (!connectivityTimer_.isActive()) + connectivityTimer_.start(); + + try { + opts.since = cache::client()->nextBatchToken(); + } catch (const lmdb::error &e) { + log::db()->error("failed to retrieve next batch token: {}", e.what()); + return; + } + + http::v2::client()->sync( + opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast(err->status_code); + + log::net()->error("sync error: {} {}", status_code, err_code); + + switch (status_code) { + case 502: + case 504: + case 524: { + emit trySync(); + return; + } + case 401: + case 403: { + // We are logged out. + if (http::v2::client()->access_token().empty()) + return; + + emit dropToLoginPageCb(msg); + return; + } + default: { + emit trySync(); + return; + } + } + } + + log::net()->debug("sync completed: {}", res.next_batch); + + // TODO: fine grained error handling + try { + cache::client()->saveState(res); + emit syncUI(res.rooms); + + auto updates = cache::client()->roomUpdates(res); + + emit syncTopBar(updates); + emit syncRoomlist(updates); + } catch (const lmdb::error &e) { + log::db()->error("saving sync response: {}", e.what()); + } + + emit trySyncCb(); + }); +} + +void +ChatPage::joinRoom(const QString &room) +{ + const auto room_id = room.toStdString(); + + http::v2::client()->join_room( + room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + QString("Failed to join room: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit showNotification("You joined the room"); + + // We remove any invites with the same room_id. + try { + cache::client()->removeInvite(room_id); + } catch (const lmdb::error &e) { + emit showNotification( + QString("Failed to remove invite: %1").arg(e.what())); + } + }); +} + +void +ChatPage::createRoom(const mtx::requests::CreateRoom &req) +{ + http::v2::client()->create_room( + req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Room creation failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit showNotification(QString("Room %1 created") + .arg(QString::fromStdString(res.room_id.to_string()))); + }); +} + +void +ChatPage::leaveRoom(const QString &room_id) +{ + http::v2::client()->leave_room( + room_id.toStdString(), [this, room_id](const json &, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to leave room: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit leftRoom(room_id); + }); +} + +void +ChatPage::sendTypingNotifications() +{ + if (!userSettings_->isTypingNotificationsEnabled()) + return; + + http::v2::client()->start_typing( + current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to send typing notification: {}", + err->matrix_error.error); + } + }); +} diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc index 0d7f5aab..8ccd5e9d 100644 --- a/src/CommunitiesList.cc +++ b/src/CommunitiesList.cc @@ -1,4 +1,6 @@ +#include "Cache.h" #include "CommunitiesList.h" +#include "Logging.hpp" #include "MatrixClient.h" #include @@ -38,17 +40,14 @@ CommunitiesList::CommunitiesList(QWidget *parent) scrollArea_->setWidget(scrollAreaContents_); topLayout_->addWidget(scrollArea_); - connect(http::client(), - &MatrixClient::communityProfileRetrieved, - this, - [](QString communityId, QJsonObject profile) { - http::client()->fetchCommunityAvatar( - communityId, QUrl(profile["avatar_url"].toString())); - }); - connect(http::client(), - SIGNAL(communityAvatarRetrieved(const QString &, const QPixmap &)), - this, - SLOT(updateCommunityAvatar(const QString &, const QPixmap &))); + // connect(http::client(), + // &MatrixClient::communityProfileRetrieved, + // this, + // [this](QString communityId, QJsonObject profile) { + // fetchCommunityAvatar(communityId, profile["avatar_url"].toString()); + // }); + connect( + this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar); } void @@ -61,8 +60,8 @@ CommunitiesList::setCommunities(const std::mapfetchCommunityProfile(community.first); - http::client()->fetchCommunityRooms(community.first); + // http::client()->fetchCommunityProfile(community.first); + // http::client()->fetchCommunityRooms(community.first); } communities_["world"]->setPressedState(true); @@ -77,7 +76,7 @@ CommunitiesList::addCommunity(QSharedPointer community, const QString communities_.emplace(community_id, QSharedPointer(list_item)); - http::client()->fetchCommunityAvatar(community_id, community->getAvatar()); + fetchCommunityAvatar(community_id, community->getAvatar().toString()); contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item); @@ -117,3 +116,37 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id) } } } + +void +CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) +{ + auto savedImgData = cache::client()->image(avatarUrl); + if (!savedImgData.isNull()) { + QPixmap pix; + pix.loadFromData(savedImgData); + emit avatarRetrieved(id, pix); + return; + } + + mtx::http::ThumbOpts opts; + opts.mxc_url = avatarUrl.toStdString(); + http::v2::client()->get_thumbnail( + opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to download avatar: {} - ({} {})", + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + cache::client()->saveImage(opts.mxc_url, res); + + auto data = QByteArray(res.data(), res.size()); + + QPixmap pix; + pix.loadFromData(data); + + emit avatarRetrieved(id, pix); + }); +} diff --git a/src/Logging.cpp b/src/Logging.cpp new file mode 100644 index 00000000..c6c1c502 --- /dev/null +++ b/src/Logging.cpp @@ -0,0 +1,50 @@ +#include "Logging.hpp" + +#include +#include + +namespace { +std::shared_ptr db_logger = nullptr; +std::shared_ptr net_logger = nullptr; +std::shared_ptr main_logger = nullptr; + +constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6; +constexpr auto MAX_LOG_FILES = 3; +} + +namespace log { +void +init(const std::string &file_path) +{ + auto file_sink = std::make_shared( + file_path, MAX_FILE_SIZE, MAX_LOG_FILES); + + auto console_sink = std::make_shared(); + + std::vector sinks; + sinks.push_back(file_sink); + sinks.push_back(console_sink); + + net_logger = std::make_shared("net", std::begin(sinks), std::end(sinks)); + main_logger = std::make_shared("main", std::begin(sinks), std::end(sinks)); + db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); +} + +std::shared_ptr +main() +{ + return main_logger; +} + +std::shared_ptr +net() +{ + return net_logger; +} + +std::shared_ptr +db() +{ + return db_logger; +} +} diff --git a/src/LoginPage.cc b/src/LoginPage.cc index c7f9b042..d695a759 100644 --- a/src/LoginPage.cc +++ b/src/LoginPage.cc @@ -137,16 +137,16 @@ LoginPage::LoginPage(QWidget *parent) setLayout(top_layout_); + connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk); + connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError); + connect(this, &LoginPage::loginErrorCb, this, &LoginPage::loginError); + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(http::client(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString))); - connect(http::client(), SIGNAL(loginError(QString)), this, SIGNAL(errorOccurred())); connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); - connect(http::client(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString))); - connect(http::client(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess())); connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); } @@ -180,17 +180,47 @@ LoginPage::onMatrixIdEntered() inferredServerAddress_ = homeServer; serverInput_->setText(homeServer); - http::client()->setServer(homeServer); - http::client()->versions(); + + http::v2::client()->set_server(user.hostname()); + checkHomeserverVersion(); } } +void +LoginPage::checkHomeserverVersion() +{ + http::v2::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + using namespace boost::beast::http; + + if (err->status_code == status::not_found) { + emit versionErrorCb(tr("The required endpoints were not found. " + "Possibly not a Matrix server.")); + return; + } + + if (!err->parse_error.empty()) { + emit versionErrorCb(tr("Received malformed response. Make sure " + "the homeserver domain is valid.")); + return; + } + + emit versionErrorCb(tr( + "An unknown error occured. Make sure the homeserver domain is valid.")); + return; + } + + emit versionOkCb(); + }); +} + void LoginPage::onServerAddressEntered() { error_label_->setText(""); - http::client()->setServer(serverInput_->text()); - http::client()->versions(); + http::v2::client()->set_server(serverInput_->text().toStdString()); + checkHomeserverVersion(); serverLayout_->removeWidget(errorIcon_); errorIcon_->hide(); @@ -199,11 +229,8 @@ LoginPage::onServerAddressEntered() } void -LoginPage::versionError(QString error) +LoginPage::versionError(const QString &error) { - QUrl currentServer = http::client()->getHomeServer(); - QString mxidAddress = matrixid_input_->text().split(":").at(1); - error_label_->setText(error); serverInput_->show(); @@ -215,7 +242,7 @@ LoginPage::versionError(QString error) } void -LoginPage::versionSuccess() +LoginPage::versionOk() { serverLayout_->removeWidget(spinner_); matrixidLayout_->removeWidget(spinner_); @@ -241,8 +268,20 @@ LoginPage::onLoginButtonClicked() if (password_input_->text().isEmpty()) return loginError(tr("Empty password")); - http::client()->setServer(serverInput_->text()); - http::client()->login(QString::fromStdString(user.localpart()), password_input_->text()); + http::v2::client()->set_server(serverInput_->text().toStdString()); + http::v2::client()->login( + user.localpart(), + password_input_->text().toStdString(), + initialDeviceName(), + [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { + if (err) { + emit loginError(QString::fromStdString(err->matrix_error.error)); + emit errorOccurred(); + return; + } + + emit loginOk(res); + }); emit loggingIn(); } diff --git a/src/MainWindow.cc b/src/MainWindow.cc index c46cbff1..9ba8b28e 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -26,6 +25,7 @@ #include "ChatPage.h" #include "Config.h" #include "LoadingIndicator.h" +#include "Logging.hpp" #include "LoginPage.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -46,6 +46,15 @@ MainWindow *MainWindow::instance_ = nullptr; +MainWindow::~MainWindow() +{ + if (http::v2::client() != nullptr) { + http::v2::client()->shutdown(); + // TODO: find out why waiting for the threads to join is slow. + http::v2::client()->close(); + } +} + MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , progressModal_{nullptr} @@ -54,9 +63,6 @@ MainWindow::MainWindow(QWidget *parent) setWindowTitle("nheko"); setObjectName("MainWindow"); - // Initialize the http client. - http::init(); - restoreWindowSize(); QFont font("Open Sans"); @@ -124,21 +130,13 @@ MainWindow::MainWindow(QWidget *parent) connect( chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); - connect(http::client(), - SIGNAL(loginSuccess(QString, QString, QString)), - this, - SLOT(showChatPage(QString, QString, QString))); - - connect(http::client(), - SIGNAL(registerSuccess(QString, QString, QString)), - this, - SLOT(showChatPage(QString, QString, QString))); - connect(http::client(), &MatrixClient::invalidToken, this, [this]() { - chat_page_->deleteConfigs(); - showLoginPage(); - login_page_->loginError("Invalid token detected. Please try to login again."); + connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { + http::v2::client()->set_user(res.user_id); + showChatPage(); }); + connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage); + QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); @@ -157,7 +155,18 @@ MainWindow::MainWindow(QWidget *parent) QString home_server = settings.value("auth/home_server").toString(); QString user_id = settings.value("auth/user_id").toString(); - showChatPage(user_id, home_server, token); + http::v2::client()->set_access_token(token.toStdString()); + http::v2::client()->set_server(home_server.toStdString()); + + try { + using namespace mtx::identifiers; + http::v2::client()->set_user(parse(user_id.toStdString())); + } catch (const std::invalid_argument &e) { + log::main()->critical("bootstrapped with invalid user_id: {}", + user_id.toStdString()); + } + + showChatPage(); } } @@ -216,8 +225,13 @@ MainWindow::removeOverlayProgressBar() } void -MainWindow::showChatPage(QString userid, QString homeserver, QString token) +MainWindow::showChatPage() { + auto userid = QString::fromStdString(http::v2::client()->user_id().to_string()); + auto homeserver = QString::fromStdString(http::v2::client()->server() + ":" + + std::to_string(http::v2::client()->port())); + auto token = QString::fromStdString(http::v2::client()->access_token()); + QSettings settings; settings.setValue("auth/access_token", token); settings.setValue("auth/home_server", homeserver); @@ -317,7 +331,7 @@ MainWindow::openLeaveRoomDialog(const QString &room_id) leaveRoomModal_->hide(); if (leaving) - http::client()->leaveRoom(roomToLeave); + chat_page_->leaveRoom(roomToLeave); }); leaveRoomModal_ = diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index c4eaf347..0eb4658a 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -1,1371 +1,32 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include "MatrixClient.h" -#include + +#include namespace { -std::unique_ptr instance_ = nullptr; +auto v2_client_ = std::make_shared("matrix.org"); } namespace http { +namespace v2 { -std::shared_ptr client_ = nullptr; +mtx::http::Client * +client() +{ + return v2_client_.get(); +} + +} // namespace v2 void init() { - if (!instance_) - instance_ = std::make_unique(); -} - -MatrixClient * -client() -{ - return instance_.get(); -} -} - -MatrixClient::MatrixClient(QObject *parent) - : QNetworkAccessManager(parent) - , clientApiUrl_{"/_matrix/client/r0"} - , mediaApiUrl_{"/_matrix/media/r0"} - , serverProtocol_{"https"} -{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); - - QSettings settings; - txn_id_ = settings.value("client/transaction_id", 1).toInt(); - - auto env = QProcessEnvironment::systemEnvironment(); - - auto allowInsecureConnections = env.value("NHEKO_ALLOW_INSECURE_CONNECTIONS", "0"); - - if (allowInsecureConnections == "1") { - qWarning() << "Insecure connections are allowed: SSL errors will be ignored"; - connect( - this, - &QNetworkAccessManager::sslErrors, - this, - [](QNetworkReply *reply, const QList &) { reply->ignoreSslErrors(); }); - } - - QJsonObject default_filter{ - { - "room", - QJsonObject{ - {"include_leave", true}, - { - "account_data", - QJsonObject{ - {"not_types", QJsonArray{"*"}}, - }, - }, - }, - }, - { - "account_data", - QJsonObject{ - {"not_types", QJsonArray{"*"}}, - }, - }, - { - "presence", - QJsonObject{ - {"not_types", QJsonArray{"*"}}, - }, - }, - }; - - filter_ = settings - .value("client/sync_filter", - QJsonDocument(default_filter).toJson(QJsonDocument::Compact)) - .toString(); - - connect(this, - &QNetworkAccessManager::networkAccessibleChanged, - this, - [this](NetworkAccessibility status) { - if (status != NetworkAccessibility::Accessible) - setNetworkAccessible(NetworkAccessibility::Accessible); - }); + qRegisterMetaType(); + qRegisterMetaType>(); } -void -MatrixClient::reset() noexcept -{ - next_batch_.clear(); - server_.clear(); - token_.clear(); - - txn_id_ = 0; -} - -void -MatrixClient::login(const QString &username, const QString &password) noexcept -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/login"); - - QNetworkRequest request(endpoint); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - mtx::requests::Login login; - login.user = username.toStdString(); - login.password = password.toStdString(); - login.initial_device_display_name = "nheko"; - -#if defined(Q_OS_MAC) - login.initial_device_display_name = "nheko on Mac OS"; -#elif defined(Q_OS_LINUX) - login.initial_device_display_name = "nheko on Linux"; -#elif defined(Q_OS_WIN) - login.initial_device_display_name = "nheko on Windows"; -#endif - - json j = login; - - auto data = QByteArray::fromStdString(j.dump()); - auto reply = post(request, data); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status_code = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status_code == 403) { - emit loginError(tr("Wrong username or password")); - return; - } - - if (status_code == 404) { - emit loginError(tr("Login endpoint was not found on the server")); - return; - } - - if (status_code >= 400) { - qWarning() << "Login error: " << reply->errorString(); - emit loginError(tr("An unknown error occured. Please try again.")); - return; - } - - if (reply->error()) { - emit loginError(reply->errorString()); - return; - } - - try { - mtx::responses::Login login = - nlohmann::json::parse(reply->readAll().data()); - - auto hostname = server_.host(); - - if (server_.port() > 0) - hostname = QString("%1:%2").arg(server_.host()).arg(server_.port()); - - emit loginSuccess(QString::fromStdString(login.user_id.to_string()), - hostname, - QString::fromStdString(login.access_token)); - } catch (std::exception &e) { - qWarning() << "Malformed JSON response" << e.what(); - emit loginError(tr("Malformed response. Possibly not a Matrix server")); - } - }); -} -void -MatrixClient::logout() noexcept -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/logout"); - - QNetworkRequest request(endpoint); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - setupAuth(request); - - QJsonObject body{}; - auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); - - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status != 200) { - qWarning() << "Logout error: " << reply->errorString(); - return; - } - - emit loggedOut(); - }); -} - -void -MatrixClient::registerUser(const QString &user, - const QString &pass, - const QString &server, - const QString &session) noexcept -{ - setServer(server); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/register"); - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - QJsonObject body{{"username", user}, {"password", pass}}; - - // We trying to register using the response from the recaptcha. - if (!session.isEmpty()) - body = QJsonObject{ - {"username", user}, - {"password", pass}, - {"auth", QJsonObject{{"type", "m.login.recaptcha"}, {"session", session}}}}; - - auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); - - connect(reply, &QNetworkReply::finished, this, [this, reply, user, pass, server]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - auto data = reply->readAll(); - - // Try to parse a regular register response. - try { - mtx::responses::Register res = nlohmann::json::parse(data); - emit registerSuccess(QString::fromStdString(res.user_id.to_string()), - QString::fromStdString(res.user_id.hostname()), - QString::fromStdString(res.access_token)); - } catch (const std::exception &e) { - qWarning() << "Register" << e.what(); - } - - // Check if the server requires a registration flow. - try { - mtx::responses::RegistrationFlows res = nlohmann::json::parse(data); - emit registrationFlow( - user, pass, server, QString::fromStdString(res.session)); - return; - } catch (const std::exception &) { - } - - // We encountered an unknown error. - if (status == 0 || status >= 400) { - try { - mtx::errors::Error res = nlohmann::json::parse(data); - emit registerError(QString::fromStdString(res.error)); - return; - } catch (const std::exception &) { - } - - emit registerError(reply->errorString()); - } - }); -} - -void -MatrixClient::sync() noexcept -{ - // the filter is not uploaded yet (so it is a json with { at the beginning) - // ignore for now that the filter might be uploaded multiple times as we expect - // servers to do deduplication - if (filter_.startsWith("{")) { - uploadFilter(filter_); - } - - QUrlQuery query; - query.addQueryItem("set_presence", "online"); - query.addQueryItem("filter", filter_); - query.addQueryItem("timeout", "30000"); - - if (next_batch_.isEmpty()) { - qDebug() << "Sync requires a valid next_batch token. Initial sync should " - "be performed."; - return; - } - - query.addQueryItem("since", next_batch_); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/sync"); - endpoint.setQuery(query); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - auto reply = get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - auto data = reply->readAll(); - - if (status == 0 || status >= 400) { - try { - mtx::errors::Error res = nlohmann::json::parse(data); - - if (res.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) { - emit invalidToken(); - return; - } - - emit syncError(QString::fromStdString(res.error)); - - return; - } catch (const nlohmann::json::exception &e) { - qWarning() << e.what(); - } - } - - try { - emit syncCompleted(nlohmann::json::parse(std::move(data))); - } catch (std::exception &e) { - qWarning() << "Sync error: " << e.what(); - } - }); -} - -void -MatrixClient::sendRoomMessage(mtx::events::MessageType ty, - int txnId, - const QString &roomid, - const QString &msg, - const QString &mime, - uint64_t media_size, - const QString &url) noexcept -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + - QString("/rooms/%1/send/m.room.message/%2").arg(roomid).arg(txnId)); - - QJsonObject body; - QJsonObject info = {{"size", static_cast(media_size)}, {"mimetype", mime}}; - - switch (ty) { - case mtx::events::MessageType::Text: - body = {{"msgtype", "m.text"}, {"body", msg}}; - break; - case mtx::events::MessageType::Emote: - body = {{"msgtype", "m.emote"}, {"body", msg}}; - break; - case mtx::events::MessageType::Image: - body = {{"msgtype", "m.image"}, {"body", msg}, {"url", url}, {"info", info}}; - break; - case mtx::events::MessageType::File: - body = {{"msgtype", "m.file"}, {"body", msg}, {"url", url}, {"info", info}}; - break; - case mtx::events::MessageType::Audio: - body = {{"msgtype", "m.audio"}, {"body", msg}, {"url", url}, {"info", info}}; - break; - case mtx::events::MessageType::Video: - body = {{"msgtype", "m.video"}, {"body", msg}, {"url", url}, {"info", info}}; - break; - default: - qDebug() << "SendRoomMessage: Unknown message type for" << msg; - return; - } - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - setupAuth(request); - - auto reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); - - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, txnId]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - emit messageSendFailed(roomid, txnId); - return; - } - - auto data = reply->readAll(); - - if (data.isEmpty()) { - emit messageSendFailed(roomid, txnId); - return; - } - - auto json = QJsonDocument::fromJson(data); - - if (!json.isObject()) { - qDebug() << "Send message response is not a JSON object"; - emit messageSendFailed(roomid, txnId); - return; - } - - auto object = json.object(); - - if (!object.contains("event_id")) { - qDebug() << "SendTextMessage: missing event_id from response"; - emit messageSendFailed(roomid, txnId); - return; - } - - emit messageSent(object.value("event_id").toString(), roomid, txnId); - }); -} - -void -MatrixClient::initialSync() noexcept -{ - QUrlQuery query; - query.addQueryItem("timeout", "0"); - query.addQueryItem("filter", filter_); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/sync"); - endpoint.setQuery(query); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - auto reply = get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qDebug() << "Error code received" << status; - emit initialSyncFailed(status); - return; - } - - QtConcurrent::run([data = reply->readAll(), this]() { - try { - emit initialSyncCompleted(nlohmann::json::parse(std::move(data))); - } catch (std::exception &e) { - qWarning() << "Initial sync error:" << e.what(); - emit initialSyncFailed(); - } - }); - }); -} - -void -MatrixClient::versions() noexcept -{ - QUrl endpoint(server_); - endpoint.setPath("/_matrix/client/versions"); - - QNetworkRequest request(endpoint); - - auto reply = get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status_code = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (reply->error()) { - emit versionError(reply->errorString()); - return; - } - - if (status_code == 404) { - emit versionError("Versions endpoint was not found on the server. Possibly " - "not a Matrix server"); - return; - } - - if (status_code >= 400) { - emit versionError("An unknown error occured. Please try again."); - return; - } - - try { - mtx::responses::Versions versions = - nlohmann::json::parse(reply->readAll().data()); - - emit versionSuccess(); - } catch (std::exception &e) { - emit versionError("Malformed response. Possibly not a Matrix server"); - } - }); -} - -void -MatrixClient::getOwnProfile() noexcept -{ - // FIXME: Remove settings from the matrix client. The class should store the - // user's matrix ID. - QSettings settings; - auto userid = settings.value("auth/user_id", "").toString(); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/profile/" + userid); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - QNetworkReply *reply = get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status >= 400) { - qWarning() << reply->errorString(); - return; - } - - try { - mtx::responses::Profile profile = - nlohmann::json::parse(reply->readAll().data()); - - emit getOwnProfileResponse(QUrl(QString::fromStdString(profile.avatar_url)), - QString::fromStdString(profile.display_name)); - } catch (std::exception &e) { - qWarning() << "Profile:" << e.what(); - } - }); -} - -void -MatrixClient::getOwnCommunities() noexcept -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/joined_groups"); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - QNetworkReply *reply = get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status >= 400) { - qWarning() << reply->errorString(); - return; - } - - auto data = reply->readAll(); - auto json = QJsonDocument::fromJson(data).object(); - - if (!json.contains("groups")) { - qWarning() << "failed to parse own communities. 'groups' key not found"; - return; - } - - QList response; - for (auto group : json["groups"].toArray()) - response.append(group.toString()); - - emit getOwnCommunitiesResponse(response); - }); -} - -void -MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url) -{ - QList url_parts = avatar_url.toString().split("mxc://"); - - if (url_parts.size() != 2) { - qDebug() << "Invalid format for room avatar " << avatar_url.toString(); - return; - } - - QUrlQuery query; - query.addQueryItem("width", "512"); - query.addQueryItem("height", "512"); - query.addQueryItem("method", "crop"); - - QString media_url = - QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]); - - QUrl endpoint(media_url); - endpoint.setQuery(query); - - QNetworkRequest avatar_request(endpoint); - - QNetworkReply *reply = get(avatar_request); - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, avatar_url]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString(); - return; - } - - auto img = reply->readAll(); - - if (img.size() == 0) - return; - - QPixmap pixmap; - pixmap.loadFromData(img); - - emit roomAvatarRetrieved(roomid, pixmap, avatar_url.toString(), img); - }); -} - -void -MatrixClient::fetchCommunityAvatar(const QString &communityId, const QUrl &avatar_url) -{ - if (avatar_url.isEmpty()) - return; - - QList url_parts = avatar_url.toString().split("mxc://"); - - if (url_parts.size() != 2) { - qDebug() << "Invalid format for community avatar " << avatar_url.toString(); - return; - } - - QUrlQuery query; - query.addQueryItem("width", "512"); - query.addQueryItem("height", "512"); - query.addQueryItem("method", "crop"); - - QString media_url = - QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]); - - QUrl endpoint(media_url); - endpoint.setQuery(query); - - QNetworkRequest avatar_request(endpoint); - - QNetworkReply *reply = get(avatar_request); - connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString(); - return; - } - - auto img = reply->readAll(); - - if (img.size() == 0) - return; - - QPixmap pixmap; - pixmap.loadFromData(img); - - emit communityAvatarRetrieved(communityId, pixmap); - }); -} - -void -MatrixClient::fetchCommunityProfile(const QString &communityId) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/profile"); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - QNetworkReply *reply = get(request); - - connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status >= 400) { - qWarning() << reply->errorString(); - return; - } - - auto data = reply->readAll(); - const auto json = QJsonDocument::fromJson(data).object(); - - emit communityProfileRetrieved(communityId, json); - }); -} - -void -MatrixClient::fetchCommunityRooms(const QString &communityId) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/rooms"); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - QNetworkReply *reply = get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status >= 400) { - qWarning() << reply->errorString(); - return; - } - - auto data = reply->readAll(); - const auto json = QJsonDocument::fromJson(data).object(); - - emit communityRoomsRetrieved(communityId, json); - }); -} - -QSharedPointer -MatrixClient::fetchUserAvatar(const QUrl &avatarUrl) -{ - QList url_parts = avatarUrl.toString().split("mxc://"); - - if (url_parts.size() != 2) - return QSharedPointer(); - - QUrlQuery query; - query.addQueryItem("width", "128"); - query.addQueryItem("height", "128"); - query.addQueryItem("method", "crop"); - - QString media_url = - QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]); - - QUrl endpoint(media_url); - endpoint.setQuery(query); - - QNetworkRequest avatar_request(endpoint); - - auto reply = get(avatar_request); - auto proxy = QSharedPointer(new DownloadMediaProxy, - [](auto proxy) { proxy->deleteLater(); }); - connect(reply, &QNetworkReply::finished, this, [reply, proxy, avatarUrl]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString() << avatarUrl; - return; - } - - auto data = reply->readAll(); - - if (data.size() == 0) { - qWarning() << "received avatar with no data:" << avatarUrl; - return; - } - - QImage img; - img.loadFromData(data); - - emit proxy->avatarDownloaded(img); - }); - - return proxy; -} - -QSharedPointer -MatrixClient::downloadImage(const QUrl &url) -{ - QNetworkRequest image_request(url); - - auto reply = get(image_request); - auto proxy = QSharedPointer(new DownloadMediaProxy, - [](auto proxy) { proxy->deleteLater(); }); - connect(reply, &QNetworkReply::finished, this, [reply, proxy]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString(); - return; - } - - auto img = reply->readAll(); - - if (img.size() == 0) - return; - - QPixmap pixmap; - pixmap.loadFromData(img); - - emit proxy->imageDownloaded(pixmap); - }); - - return proxy; -} - -QSharedPointer -MatrixClient::downloadFile(const QUrl &url) -{ - QNetworkRequest fileRequest(url); - - auto reply = get(fileRequest); - auto proxy = QSharedPointer(new DownloadMediaProxy, - [](auto proxy) { proxy->deleteLater(); }); - connect(reply, &QNetworkReply::finished, this, [reply, proxy]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - // TODO: Handle error - qWarning() << reply->errorString(); - return; - } - - auto data = reply->readAll(); - - if (data.size() == 0) - return; - - emit proxy->fileDownloaded(data); - }); - - return proxy; -} - -void -MatrixClient::messages(const QString &roomid, const QString &from_token, int limit) noexcept -{ - QUrlQuery query; - query.addQueryItem("from", from_token); - query.addQueryItem("dir", "b"); - query.addQueryItem("limit", QString::number(limit)); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/messages").arg(roomid)); - endpoint.setQuery(query); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - auto reply = get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString(); - return; - } - - try { - mtx::responses::Messages messages = - nlohmann::json::parse(reply->readAll().data()); - - emit messagesRetrieved(roomid, messages); - } catch (std::exception &e) { - qWarning() << "Room messages from" << roomid << e.what(); - return; - } - }); -} - -void -MatrixClient::uploadImage(const QString &roomid, - const QString &filename, - const QSharedPointer data) -{ - auto reply = makeUploadRequest(data); - - if (reply == nullptr) - return; - - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() { - auto json = getUploadReply(reply); - if (json.isEmpty()) - return; - - auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString(); - auto size = - reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong(); - - emit imageUploaded( - roomid, filename, json.value("content_uri").toString(), mime, size); - }); -} - -void -MatrixClient::uploadFile(const QString &roomid, - const QString &filename, - const QSharedPointer data) -{ - auto reply = makeUploadRequest(data); - - if (reply == nullptr) - return; - - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() { - auto json = getUploadReply(reply); - if (json.isEmpty()) - return; - - auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString(); - auto size = - reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong(); - - emit fileUploaded( - roomid, filename, json.value("content_uri").toString(), mime, size); - }); -} - -void -MatrixClient::uploadAudio(const QString &roomid, - const QString &filename, - const QSharedPointer data) -{ - auto reply = makeUploadRequest(data); - - if (reply == nullptr) - return; - - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() { - auto json = getUploadReply(reply); - if (json.isEmpty()) - return; - - auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString(); - auto size = - reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong(); - - emit audioUploaded( - roomid, filename, json.value("content_uri").toString(), mime, size); - }); -} - -void -MatrixClient::uploadVideo(const QString &roomid, - const QString &filename, - const QSharedPointer data) -{ - auto reply = makeUploadRequest(data); - - if (reply == nullptr) - return; - - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename, data]() { - auto json = getUploadReply(reply); - if (json.isEmpty()) - return; - - auto mime = reply->request().header(QNetworkRequest::ContentTypeHeader).toString(); - auto size = - reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong(); - - emit videoUploaded( - roomid, filename, json.value("content_uri").toString(), mime, size); - }); -} - -void -MatrixClient::uploadFilter(const QString &filter) noexcept -{ - // validate that filter is a Json-String - QJsonDocument doc = QJsonDocument::fromJson(filter.toUtf8()); - if (doc.isNull() || !doc.isObject()) { - qWarning() << "Input which should be uploaded as filter is no JsonObject"; - return; - } - - QSettings settings; - auto userid = settings.value("auth/user_id", "").toString(); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/user/%1/filter").arg(userid)); - - QNetworkRequest request(endpoint); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - setupAuth(request); - - auto reply = post(request, doc.toJson(QJsonDocument::Compact)); - - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString() << "42"; - return; - } - - auto data = reply->readAll(); - auto response = QJsonDocument::fromJson(data); - auto filter_id = response.object()["filter_id"].toString(); - - qDebug() << "Filter with ID" << filter_id << "created."; - QSettings settings; - settings.setValue("client/sync_filter", filter_id); - settings.sync(); - - // set the filter_ var so following syncs will use it - filter_ = filter_id; - }); -} - -void -MatrixClient::joinRoom(const QString &roomIdOrAlias) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/join/%1").arg(roomIdOrAlias)); - - QNetworkRequest request(endpoint); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - setupAuth(request); - - auto reply = post(request, "{}"); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - auto data = reply->readAll(); - auto response = QJsonDocument::fromJson(data); - auto json = response.object(); - - if (json.contains("error")) - emit joinFailed(json["error"].toString()); - else - qDebug() << reply->errorString(); - - return; - } - - auto data = reply->readAll(); - auto response = QJsonDocument::fromJson(data); - auto room_id = response.object()["room_id"].toString(); - - emit joinedRoom(room_id); - }); -} - -void -MatrixClient::leaveRoom(const QString &roomId) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/leave").arg(roomId)); - - QNetworkRequest request(endpoint); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - setupAuth(request); - - auto reply = post(request, "{}"); - - connect(reply, &QNetworkReply::finished, this, [this, reply, roomId]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString(); - return; - } - - emit leftRoom(roomId); - }); -} - -void -MatrixClient::inviteUser(const QString &roomId, const QString &user) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/invite").arg(roomId)); - - QNetworkRequest request(endpoint); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - setupAuth(request); - - QJsonObject body{{"user_id", user}}; - auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); - - connect(reply, &QNetworkReply::finished, this, [this, reply, roomId, user]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - // TODO: Handle failure. - qWarning() << reply->errorString(); - return; - } - - emit invitedUser(roomId, user); - }); -} - -void -MatrixClient::createRoom(const mtx::requests::CreateRoom &create_room_request) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/createRoom")); - - QNetworkRequest request(endpoint); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - setupAuth(request); - - nlohmann::json body = create_room_request; - auto reply = post(request, QString::fromStdString(body.dump()).toUtf8()); - - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - auto data = reply->readAll(); - auto response = QJsonDocument::fromJson(data); - auto json = response.object(); - - if (json.contains("error")) - emit roomCreationFailed(json["error"].toString()); - else - qDebug() << reply->errorString(); - - return; - } - - auto data = reply->readAll(); - auto response = QJsonDocument::fromJson(data); - auto room_id = response.object()["room_id"].toString(); - - emit roomCreated(room_id); - }); -} - -void -MatrixClient::sendTypingNotification(const QString &roomid, int timeoutInMillis) -{ - QSettings settings; - QString user_id = settings.value("auth/user_id").toString(); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/typing/%2").arg(roomid).arg(user_id)); - - QString msgType(""); - QJsonObject body; - - body = {{"typing", true}, {"timeout", timeoutInMillis}}; - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - setupAuth(request); - - put(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); -} - -void -MatrixClient::removeTypingNotification(const QString &roomid) -{ - QSettings settings; - QString user_id = settings.value("auth/user_id").toString(); - - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/typing/%2").arg(roomid).arg(user_id)); - - QString msgType(""); - QJsonObject body; - - body = {{"typing", false}}; - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - setupAuth(request); - - put(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); -} - -void -MatrixClient::readEvent(const QString &room_id, const QString &event_id) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/read_markers").arg(room_id)); - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - setupAuth(request); - - QJsonObject body({{"m.fully_read", event_id}, {"m.read", event_id}}); - auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); - - connect(reply, &QNetworkReply::finished, this, [reply]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - qWarning() << reply->errorString(); - return; - } - }); -} - -QNetworkReply * -MatrixClient::makeUploadRequest(QSharedPointer iodev) -{ - QUrl endpoint(server_); - endpoint.setPath(mediaApiUrl_ + "/upload"); - - if (!iodev->open(QIODevice::ReadOnly)) { - qWarning() << "Error while reading device:" << iodev->errorString(); - return nullptr; - } - - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(iodev.data()); - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentTypeHeader, mime.name()); - setupAuth(request); - - auto reply = post(request, iodev.data()); - - return reply; -} - -QJsonObject -MatrixClient::getUploadReply(QNetworkReply *reply) -{ - QJsonObject object; - - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (status == 0 || status >= 400) { - emit uploadFailed(status, - QString("Media upload failed - %1").arg(reply->errorString())); - return object; - } - - auto res_data = reply->readAll(); - - if (res_data.isEmpty()) { - emit uploadFailed(status, "Media upload failed - Empty response"); - return object; - } - - auto json = QJsonDocument::fromJson(res_data); - - if (!json.isObject()) { - emit uploadFailed(status, "Media upload failed - Invalid response"); - return object; - } - - object = json.object(); - if (!object.contains("content_uri")) { - emit uploadFailed(status, "Media upload failed - Missing 'content_uri'"); - return QJsonObject{}; - } - - return object; -} - -void -MatrixClient::redactEvent(const QString &room_id, const QString &event_id) -{ - QUrl endpoint(server_); - endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/redact/%2/%3") - .arg(room_id) - .arg(event_id) - .arg(incrementTransactionId())); - - QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - setupAuth(request); - - // TODO: no reason specified - QJsonObject body{}; - auto reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); - - connect(reply, &QNetworkReply::finished, this, [reply, this, room_id, event_id]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - auto data = reply->readAll(); - - if (status == 0 || status >= 400) { - try { - mtx::errors::Error res = nlohmann::json::parse(data); - emit redactionFailed(QString::fromStdString(res.error)); - return; - } catch (const std::exception &) { - } - } - - try { - mtx::responses::EventId res = nlohmann::json::parse(data); - emit redactionCompleted(room_id, event_id); - } catch (const std::exception &e) { - emit redactionFailed(QString::fromStdString(e.what())); - } - }); -} - -void -MatrixClient::getNotifications() noexcept -{ - QUrlQuery query; - query.addQueryItem("limit", "5"); - - QUrl endpoint(server_); - endpoint.setQuery(query); - endpoint.setPath(clientApiUrl_ + "/notifications"); - - QNetworkRequest request(QString(endpoint.toEncoded())); - setupAuth(request); - - auto reply = get(request); - connect(reply, &QNetworkReply::finished, this, [reply, this]() { - reply->deleteLater(); - - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - auto data = reply->readAll(); - - if (status == 0 || status >= 400) { - try { - mtx::errors::Error res = nlohmann::json::parse(data); - std::cout << nlohmann::json::parse(data).dump(2) << '\n'; - // TODO: Response with an error signal - return; - } catch (const std::exception &) { - } - } - - try { - emit notificationsRetrieved(nlohmann::json::parse(data)); - } catch (const std::exception &e) { - qWarning() << "failed to parse /notifications response" << e.what(); - } - }); -} +} // namespace http diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc index 7d80b727..a5960b83 100644 --- a/src/RegisterPage.cc +++ b/src/RegisterPage.cc @@ -20,6 +20,7 @@ #include "Config.h" #include "FlatButton.h" +#include "Logging.hpp" #include "MainWindow.h" #include "MatrixClient.h" #include "RaisedButton.h" @@ -125,35 +126,53 @@ RegisterPage::RegisterPage(QWidget *parent) connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(http::client(), - SIGNAL(registerError(const QString &)), - this, - SLOT(registerError(const QString &))); - connect(http::client(), - &MatrixClient::registrationFlow, - this, - [this](const QString &user, - const QString &pass, - const QString &server, - const QString &session) { - emit errorOccurred(); + connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError); + connect( + this, + &RegisterPage::registrationFlow, + this, + [this](const std::string &user, const std::string &pass, const std::string &session) { + emit errorOccurred(); - if (!captchaDialog_) { - captchaDialog_ = - std::make_shared(server, session, this); - connect(captchaDialog_.get(), - &dialogs::ReCaptcha::closing, - this, - [this, user, pass, server, session]() { - captchaDialog_->close(); - emit registering(); - http::client()->registerUser( - user, pass, server, session); - }); - } + if (!captchaDialog_) { + captchaDialog_ = std::make_shared( + QString::fromStdString(session), this); + connect( + captchaDialog_.get(), + &dialogs::ReCaptcha::closing, + this, + [this, user, pass, session]() { + captchaDialog_->close(); + emit registering(); - QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); }); - }); + http::v2::client()->flow_response( + user, + pass, + session, + "m.login.recaptcha", + [this](const mtx::responses::Register &res, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to retrieve registration flows: {}", + err->matrix_error.error); + emit errorOccurred(); + emit registerErrorCb(QString::fromStdString( + err->matrix_error.error)); + return; + } + + http::v2::client()->set_user(res.user_id); + http::v2::client()->set_access_token( + res.access_token); + + emit registerOk(); + }); + }); + } + + QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); }); + }); setLayout(top_layout_); } @@ -185,11 +204,56 @@ RegisterPage::onRegisterButtonClicked() } else if (!server_input_->hasAcceptableInput()) { registerError(tr("Invalid server name")); } else { - QString username = username_input_->text(); - QString password = password_input_->text(); - QString server = server_input_->text(); + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + auto server = server_input_->text().toStdString(); + + http::v2::client()->set_server(server); + http::v2::client()->registration( + username, + password, + [this, username, password](const mtx::responses::Register &res, + mtx::http::RequestErr err) { + if (!err) { + http::v2::client()->set_user(res.user_id); + http::v2::client()->set_access_token(res.access_token); + + emit registerOk(); + return; + } + + // The server requires registration flows. + if (err->status_code == boost::beast::http::status::unauthorized) { + http::v2::client()->flow_register( + username, + password, + [this, username, password]( + const mtx::responses::RegistrationFlows &res, + mtx::http::RequestErr err) { + if (res.session.empty() && err) { + log::net()->warn( + "failed to retrieve registration flows: ({}) " + "{}", + static_cast(err->status_code), + err->matrix_error.error); + emit errorOccurred(); + emit registerErrorCb(QString::fromStdString( + err->matrix_error.error)); + return; + } + + emit registrationFlow(username, password, res.session); + }); + return; + } + + log::net()->warn("failed to register: status_code ({})", + static_cast(err->status_code)); + + emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); + emit errorOccurred(); + }); - http::client()->registerUser(username, password, server); emit registering(); } } diff --git a/src/RoomList.cc b/src/RoomList.cc index e7c5ef30..d3ed2e66 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc @@ -16,11 +16,11 @@ */ #include -#include #include #include #include "Cache.h" +#include "Logging.hpp" #include "MainWindow.h" #include "MatrixClient.h" #include "OverlayModal.h" @@ -55,18 +55,7 @@ RoomList::RoomList(QSharedPointer userSettings, QWidget *parent) scrollArea_->setWidget(scrollAreaContents_); topLayout_->addWidget(scrollArea_); - connect(http::client(), - &MatrixClient::roomAvatarRetrieved, - this, - [this](const QString &room_id, - const QPixmap &img, - const QString &url, - const QByteArray &data) { - if (cache::client()) - cache::client()->saveImage(url, data); - - updateRoomAvatar(room_id, img); - }); + connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar); } void @@ -101,7 +90,28 @@ RoomList::updateAvatar(const QString &room_id, const QString &url) savedImgData = cache::client()->image(url); if (savedImgData.isEmpty()) { - http::client()->fetchRoomAvatar(room_id, url); + mtx::http::ThumbOpts opts; + opts.mxc_url = url.toStdString(); + http::v2::client()->get_thumbnail( + opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to download thumbnail: {}, {} - {}", + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + if (cache::client()) + cache::client()->saveImage(opts.mxc_url, res); + + auto data = QByteArray(res.data(), res.size()); + QPixmap pixmap; + pixmap.loadFromData(data); + + emit updateRoomAvatarCb(room_id, pixmap); + }); } else { QPixmap img; img.loadFromData(savedImgData); @@ -131,7 +141,8 @@ void RoomList::updateUnreadMessageCount(const QString &roomid, int count) { if (!roomExists(roomid)) { - qWarning() << "UpdateUnreadMessageCount: Unknown roomid"; + log::main()->warn("updateUnreadMessageCount: unknown room_id {}", + roomid.toStdString()); return; } @@ -156,7 +167,7 @@ RoomList::calculateUnreadMessageCount() void RoomList::initialize(const QMap &info) { - qDebug() << "initialize room list"; + log::main()->info("initialize room list"); rooms_.clear(); @@ -209,7 +220,7 @@ RoomList::highlightSelectedRoom(const QString &room_id) emit roomChanged(room_id); if (!roomExists(room_id)) { - qDebug() << "RoomList: clicked unknown roomid"; + log::main()->warn("roomlist: clicked unknown room_id"); return; } @@ -232,7 +243,8 @@ void RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) { if (!roomExists(roomid)) { - qWarning() << "Avatar update on non existent room" << roomid; + log::main()->warn("avatar update on non-existent room_id: {}", + roomid.toStdString()); return; } @@ -246,7 +258,9 @@ void RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info) { if (!roomExists(roomid)) { - qWarning() << "Description update on non existent room" << roomid << info.body; + log::main()->warn("description update on non-existent room_id: {}, {}", + roomid.toStdString(), + info.body.toStdString()); return; } @@ -314,7 +328,7 @@ RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias) joinRoomModal_->hide(); if (isJoining) - http::client()->joinRoom(roomAlias); + emit joinRoom(roomAlias); } void diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc index f3753971..acb33fa7 100644 --- a/src/TextInputWidget.cc +++ b/src/TextInputWidget.cc @@ -71,8 +71,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) this, &FilteredTextEdit::uploadData); - qRegisterMetaType(); - qRegisterMetaType>(); connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { popup_.hide(); diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp index ba487cea..6b1143b5 100644 --- a/src/dialogs/ReCaptcha.cpp +++ b/src/dialogs/ReCaptcha.cpp @@ -6,6 +6,7 @@ #include "Config.h" #include "FlatButton.h" +#include "MatrixClient.h" #include "RaisedButton.h" #include "Theme.h" @@ -13,7 +14,7 @@ using namespace dialogs; -ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *parent) +ReCaptcha::ReCaptcha(const QString &session, QWidget *parent) : QWidget(parent) { setAutoFillBackground(true); @@ -51,12 +52,12 @@ ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *par layout->addWidget(label); layout->addLayout(buttonLayout); - connect(openCaptchaBtn_, &QPushButton::clicked, [server, session]() { - const auto url = - QString( - "https://%1/_matrix/client/r0/auth/m.login.recaptcha/fallback/web?session=%2") - .arg(server) - .arg(session); + connect(openCaptchaBtn_, &QPushButton::clicked, [session]() { + const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/" + "fallback/web?session=%3") + .arg(QString::fromStdString(http::v2::client()->server())) + .arg(http::v2::client()->port()) + .arg(session); QDesktopServices::openUrl(url); }); diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 4d2f304b..2396fc19 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -67,6 +67,20 @@ EditModal::EditModal(const QString &roomId, QWidget *parent) labelLayout->addWidget(errorField_); layout->addLayout(labelLayout); + connect(this, &EditModal::stateEventErrorCb, this, [this](const QString &msg) { + errorField_->setText(msg); + errorField_->show(); + }); + connect(this, &EditModal::nameEventSentCb, this, [this](const QString &newName) { + errorField_->hide(); + emit nameChanged(newName); + close(); + }); + connect(this, &EditModal::topicEventSentCb, this, [this]() { + errorField_->hide(); + close(); + }); + connect(applyBtn_, &QPushButton::clicked, [this]() { // Check if the values are changed from the originals. auto newName = nameInput_->text().trimmed(); @@ -85,53 +99,37 @@ EditModal::EditModal(const QString &roomId, QWidget *parent) state::Name body; body.name = newName.toStdString(); - auto proxy = - http::client()->sendStateEvent(body, - roomId_); - connect(proxy.get(), - &StateEventProxy::stateEventSent, - this, - [this, proxy, newName]() { - Q_UNUSED(proxy); - errorField_->hide(); - emit nameChanged(newName); - close(); - }); + http::v2::client()->send_state_event( + roomId_.toStdString(), + body, + [this, newName](const mtx::responses::EventId &, + mtx::http::RequestErr err) { + if (err) { + emit stateEventErrorCb( + QString::fromStdString(err->matrix_error.error)); + return; + } - connect(proxy.get(), - &StateEventProxy::stateEventError, - this, - [this, proxy, newName](const QString &msg) { - Q_UNUSED(proxy); - errorField_->setText(msg); - errorField_->show(); - }); + emit nameEventSentCb(newName); + }); } if (newTopic != initialTopic_ && !newTopic.isEmpty()) { state::Topic body; body.topic = newTopic.toStdString(); - auto proxy = - http::client()->sendStateEvent( - body, roomId_); - connect(proxy.get(), - &StateEventProxy::stateEventSent, - this, - [this, proxy, newTopic]() { - Q_UNUSED(proxy); - errorField_->hide(); - close(); - }); + http::v2::client()->send_state_event( + roomId_.toStdString(), + body, + [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit stateEventErrorCb( + QString::fromStdString(err->matrix_error.error)); + return; + } - connect(proxy.get(), - &StateEventProxy::stateEventError, - this, - [this, proxy, newTopic](const QString &msg) { - Q_UNUSED(proxy); - errorField_->setText(msg); - errorField_->show(); - }); + emit topicEventSentCb(); + }); } }); connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close); diff --git a/src/main.cc b/src/main.cc index bd3a212c..1df8d0c9 100644 --- a/src/main.cc +++ b/src/main.cc @@ -22,15 +22,17 @@ #include #include #include -#include #include #include #include #include +#include #include #include "Config.h" +#include "Logging.hpp" #include "MainWindow.h" +#include "MatrixClient.h" #include "RaisedButton.h" #include "RunGuard.h" #include "version.hpp" @@ -46,32 +48,6 @@ screenCenter(int width, int height) return QPoint(x, y); } -void -setupProxy() -{ - QSettings settings; - - /** - To set up a SOCKS proxy: - [user] - proxy\socks\host=<> - proxy\socks\port=<> - proxy\socks\user=<> - proxy\socks\password=<> - **/ - if (settings.contains("user/proxy/socks/host")) { - QNetworkProxy proxy; - proxy.setType(QNetworkProxy::Socks5Proxy); - proxy.setHostName(settings.value("user/proxy/socks/host").toString()); - proxy.setPort(settings.value("user/proxy/socks/port").toInt()); - if (settings.contains("user/proxy/socks/user")) - proxy.setUser(settings.value("user/proxy/socks/user").toString()); - if (settings.contains("user/proxy/socks/password")) - proxy.setPassword(settings.value("user/proxy/socks/password").toString()); - QNetworkProxy::setApplicationProxy(proxy); - } -} - int main(int argc, char *argv[]) { @@ -133,7 +109,17 @@ main(int argc, char *argv[]) QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf"); app.setWindowIcon(QIcon(":/logos/nheko.png")); - qSetMessagePattern("%{time process}: [%{type}] - %{message}"); + + http::init(); + + try { + log::init(QString("%1/nheko.log") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .toStdString()); + } catch (const spdlog::spdlog_ex &ex) { + std::cout << "Log initialization failed: " << ex.what() << std::endl; + std::exit(1); + } QSettings settings; @@ -154,8 +140,6 @@ main(int argc, char *argv[]) appTranslator.load("nheko_" + lang, ":/translations"); app.installTranslator(&appTranslator); - setupProxy(); - MainWindow w; // Move the MainWindow to the center @@ -167,5 +151,7 @@ main(int argc, char *argv[]) QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize); + log::main()->info("starting nheko {}", nheko::version); + return app.exec(); } diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc index 250373e4..83a0aaed 100644 --- a/src/timeline/TimelineItem.cc +++ b/src/timeline/TimelineItem.cc @@ -62,9 +62,27 @@ TimelineItem::init() ChatPage::instance()->showReadReceipts(event_id_); }); + connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) { + emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id); + }); + connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); connect(redactMsg_, &QAction::triggered, this, [this]() { if (!event_id_.isEmpty()) - http::client()->redactEvent(room_id_, event_id_); + http::v2::client()->redact_event( + room_id_.toStdString(), + event_id_.toStdString(), + [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed(tr("Message redaction failed: %1") + .arg(QString::fromStdString( + err->matrix_error.error))); + return; + } + + emit eventRedacted(event_id_); + }); }); connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); }); diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 71058d74..5ef390a9 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -23,6 +23,7 @@ #include "ChatPage.h" #include "Config.h" #include "FloatingButton.h" +#include "Logging.hpp" #include "UserSettingsPage.h" #include "Utils.h" @@ -100,7 +101,7 @@ TimelineView::TimelineView(const QString &room_id, QWidget *parent) , room_id_{room_id} { init(); - http::client()->messages(room_id_, ""); + getMessages(); } void @@ -140,7 +141,7 @@ TimelineView::fetchHistory() return; isPaginationInProgress_ = true; - http::client()->messages(room_id_, prev_batch_token_); + getMessages(); paginationTimer_->start(5000); return; @@ -189,18 +190,13 @@ TimelineView::sliderMoved(int position) isPaginationInProgress_ = true; - // FIXME: Maybe move this to TimelineViewManager to remove the - // extra calls? - http::client()->messages(room_id_, prev_batch_token_); + getMessages(); } } void -TimelineView::addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs) +TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) { - if (room_id_ != room_id) - return; - // We've reached the start of the timline and there're no more messages. if ((msgs.end == msgs.start) && msgs.chunk.size() == 0) { isTimelineFinished = true; @@ -427,10 +423,10 @@ TimelineView::init() paginationTimer_ = new QTimer(this); connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - connect(http::client(), - &MatrixClient::messagesRetrieved, - this, - &TimelineView::addBackwardsEvents); + connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); + + connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); + connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); connect(scroll_area_->verticalScrollBar(), SIGNAL(valueChanged(int)), @@ -442,6 +438,27 @@ TimelineView::init() SLOT(sliderRangeChanged(int, int))); } +void +TimelineView::getMessages() +{ + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + http::v2::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + log::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + emit messagesRetrieved(std::move(res)); + }); +} + void TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) { @@ -513,7 +530,7 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction) } void -TimelineView::updatePendingMessage(int txn_id, QString event_id) +TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) { if (!pending_msgs_.isEmpty() && pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet @@ -548,8 +565,11 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - int txn_id = http::client()->incrementTransactionId(); - PendingMessage message(ty, txn_id, body, "", "", -1, "", view_item); + PendingMessage message; + message.ty = ty; + message.txn_id = mtx::client::utils::random_token(); + message.body = body; + message.widget = view_item; handleNewUserMessage(message); } @@ -567,19 +587,119 @@ TimelineView::sendNextPendingMessage() if (pending_msgs_.size() == 0) return; + using namespace mtx::events; + PendingMessage &m = pending_msgs_.head(); switch (m.ty) { - case mtx::events::MessageType::Audio: - case mtx::events::MessageType::Image: - case mtx::events::MessageType::Video: - case mtx::events::MessageType::File: - // FIXME: Improve the API - http::client()->sendRoomMessage( - m.ty, m.txn_id, room_id_, m.filename, m.mime, m.media_size, m.body); + case mtx::events::MessageType::Audio: { + msg::Audio audio; + audio.info.mimetype = m.mime.toStdString(); + audio.info.size = m.media_size; + audio.body = m.filename.toStdString(); + audio.url = m.body.toStdString(); + + http::v2::client()->send_room_message( + room_id_.toStdString(), + m.txn_id, + audio, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + break; + } + case mtx::events::MessageType::Image: { + msg::Image image; + image.info.mimetype = m.mime.toStdString(); + image.info.size = m.media_size; + image.body = m.filename.toStdString(); + image.url = m.body.toStdString(); + + http::v2::client()->send_room_message( + room_id_.toStdString(), + m.txn_id, + image, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::Video: { + msg::Video video; + video.info.mimetype = m.mime.toStdString(); + video.info.size = m.media_size; + video.body = m.filename.toStdString(); + video.url = m.body.toStdString(); + + http::v2::client()->send_room_message( + room_id_.toStdString(), + m.txn_id, + video, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::File: { + msg::File file; + file.info.mimetype = m.mime.toStdString(); + file.info.size = m.media_size; + file.body = m.filename.toStdString(); + file.url = m.body.toStdString(); + + http::v2::client()->send_room_message( + room_id_.toStdString(), + m.txn_id, + file, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::Text: { + msg::Text text; + text.body = m.body.toStdString(); + + http::v2::client()->send_room_message( + room_id_.toStdString(), + m.txn_id, + text, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::Emote: { + msg::Emote emote; + emote.body = m.body.toStdString(); + + http::v2::client()->send_room_message( + room_id_.toStdString(), + m.txn_id, + emote, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + break; + } default: - http::client()->sendRoomMessage( - m.ty, m.txn_id, room_id_, m.body, m.mime, m.media_size); + log::main()->warn("cannot send unknown message type: {}", m.body.toStdString()); break; } } @@ -593,7 +713,7 @@ TimelineView::notifyForLastEvent() if (lastTimelineItem) emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); else - qWarning() << "Cast to TimelineView failed" << room_id_; + log::main()->warn("cast to TimelineView failed: {}", room_id_.toStdString()); } void @@ -606,29 +726,27 @@ TimelineView::notifyForLastEvent(const TimelineEvent &event) } bool -TimelineView::isPendingMessage(const QString &txnid, +TimelineView::isPendingMessage(const std::string &txn_id, const QString &sender, const QString &local_userid) { if (sender != local_userid) return false; - auto match_txnid = [txnid](const auto &msg) -> bool { - return QString::number(msg.txn_id) == txnid; - }; + auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); } void -TimelineView::removePendingMessage(const QString &txnid) +TimelineView::removePendingMessage(const std::string &txn_id) { - if (txnid.isEmpty()) + if (txn_id.empty()) return; for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (QString::number(it->txn_id) == txnid) { + if (it->txn_id == txn_id) { int index = std::distance(pending_sent_msgs_.begin(), it); pending_sent_msgs_.removeAt(index); @@ -639,7 +757,7 @@ TimelineView::removePendingMessage(const QString &txnid) } } for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (QString::number(it->txn_id) == txnid) { + if (it->txn_id == txn_id) { int index = std::distance(pending_msgs_.begin(), it); pending_msgs_.removeAt(index); return; @@ -648,9 +766,9 @@ TimelineView::removePendingMessage(const QString &txnid) } void -TimelineView::handleFailedMessage(int txnid) +TimelineView::handleFailedMessage(const std::string &txn_id) { - Q_UNUSED(txnid); + Q_UNUSED(txn_id); // Note: We do this even if the message has already been echoed. QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); } @@ -673,7 +791,16 @@ TimelineView::readLastEvent() const const auto eventId = getLastEventId(); if (!eventId.isEmpty()) - http::client()->readEvent(room_id_, eventId); + http::v2::client()->read_event(room_id_.toStdString(), + eventId.toStdString(), + [this, eventId](mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to read event ({}, {})", + room_id_.toStdString(), + eventId.toStdString()); + } + }); } QString @@ -743,7 +870,8 @@ void TimelineView::removeEvent(const QString &event_id) { if (!eventIds_.contains(event_id)) { - qWarning() << "unknown event_id couldn't be removed:" << event_id; + log::main()->warn("cannot remove widget with unknown event_id: {}", + event_id.toStdString()); return; } @@ -860,3 +988,16 @@ TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) return diffInSeconds > fifteenMins; } + +void +TimelineView::sendRoomMessageHandler(const std::string &txn_id, + const mtx::responses::EventId &res, + mtx::http::RequestErr err) +{ + if (err) { + emit messageFailed(txn_id); + return; + } + + emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); +} diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc index b7ce53ae..9026463d 100644 --- a/src/timeline/TimelineViewManager.cc +++ b/src/timeline/TimelineViewManager.cc @@ -35,42 +35,15 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) : QStackedWidget(parent) { setStyleSheet("border: none;"); - - connect( - http::client(), &MatrixClient::messageSent, this, &TimelineViewManager::messageSent); - - connect(http::client(), - &MatrixClient::messageSendFailed, - this, - &TimelineViewManager::messageSendFailed); - - connect(http::client(), - &MatrixClient::redactionCompleted, - this, - [this](const QString &room_id, const QString &event_id) { - auto view = views_[room_id]; - - if (view) - view->removeEvent(event_id); - }); } void -TimelineViewManager::messageSent(const QString &event_id, const QString &roomid, int txn_id) +TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) { - // We save the latest valid transaction ID for later use. - QSettings settings; - settings.setValue("client/transaction_id", txn_id + 1); + auto view = views_[room_id]; - auto view = views_[roomid]; - view->updatePendingMessage(txn_id, event_id); -} - -void -TimelineViewManager::messageSendFailed(const QString &roomid, int txn_id) -{ - auto view = views_[roomid]; - view->handleFailedMessage(txn_id); + if (view) + view->removeEvent(event_id); } void diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cc index 65ca401b..1ad47747 100644 --- a/src/timeline/widgets/AudioItem.cc +++ b/src/timeline/widgets/AudioItem.cc @@ -50,21 +50,12 @@ AudioItem::init() playIcon_.addFile(":/icons/icons/ui/play-sign.png"); pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png"); - QList url_parts = url_.toString().split("mxc://"); - if (url_parts.size() != 2) { - qDebug() << "Invalid format for image" << url_.toString(); - return; - } - - QString media_params = url_parts[1]; - url_ = QString("%1/_matrix/media/r0/download/%2") - .arg(http::client()->getHomeServer().toString(), media_params); - player_ = new QMediaPlayer; player_->setMedia(QUrl(url_)); player_->setVolume(100); player_->setNotifyInterval(1000); + connect(this, &AudioItem::fileDownloadedCb, this, &AudioItem::fileDownloaded); connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) { if (state == QMediaPlayer::StoppedState) { state_ = AudioState::Play; @@ -129,14 +120,19 @@ AudioItem::mousePressEvent(QMouseEvent *event) if (filenameToSave_.isEmpty()) return; - auto proxy = http::client()->downloadFile(url_); - connect(proxy.data(), - &DownloadMediaProxy::fileDownloaded, - this, - [proxy, this](const QByteArray &data) { - proxy->deleteLater(); - fileDownloaded(data); - }); + http::v2::client()->download( + url_.toString().toStdString(), + [this](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + qWarning() << "failed to retrieve m.audio content:" << url_; + return; + } + + emit fileDownloadedCb(QByteArray(data.data(), data.size())); + }); } } diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cc index f3906a04..43689243 100644 --- a/src/timeline/widgets/FileItem.cc +++ b/src/timeline/widgets/FileItem.cc @@ -49,17 +49,9 @@ FileItem::init() icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); - QList url_parts = url_.toString().split("mxc://"); - if (url_parts.size() != 2) { - qDebug() << "Invalid format for image" << url_.toString(); - return; - } - - QString media_params = url_parts[1]; - url_ = QString("%1/_matrix/media/r0/download/%2") - .arg(http::client()->getHomeServer().toString(), media_params); - setFixedHeight(Height); + + connect(this, &FileItem::fileDownloadedCb, this, &FileItem::fileDownloaded); } FileItem::FileItem(const mtx::events::RoomEvent &event, QWidget *parent) @@ -89,8 +81,15 @@ FileItem::openUrl() if (url_.toString().isEmpty()) return; - if (!QDesktopServices::openUrl(url_)) - qWarning() << "Could not open url" << url_.toString(); + auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString()); + auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4") + .arg(QString::fromStdString(http::v2::client()->server())) + .arg(http::v2::client()->port()) + .arg(QString::fromStdString(mxc_parts.server)) + .arg(QString::fromStdString(mxc_parts.media_id)); + + if (!QDesktopServices::openUrl(urlToOpen)) + qWarning() << "Could not open url" << urlToOpen; } QSize @@ -115,14 +114,19 @@ FileItem::mousePressEvent(QMouseEvent *event) if (filenameToSave_.isEmpty()) return; - auto proxy = http::client()->downloadFile(url_); - connect(proxy.data(), - &DownloadMediaProxy::fileDownloaded, - this, - [proxy, this](const QByteArray &data) { - proxy->deleteLater(); - fileDownloaded(data); - }); + http::v2::client()->download( + url_.toString().toStdString(), + [this](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + qWarning() << "failed to retrieve m.file content:" << url_; + return; + } + + emit fileDownloadedCb(QByteArray(data.data(), data.size())); + }); } else { openUrl(); } diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cc index 66cd31ab..6aa010a4 100644 --- a/src/timeline/widgets/ImageItem.cc +++ b/src/timeline/widgets/ImageItem.cc @@ -30,37 +30,62 @@ #include "dialogs/ImageOverlay.h" #include "timeline/widgets/ImageItem.h" -ImageItem::ImageItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , event_{event} +void +ImageItem::downloadMedia(const QUrl &url) +{ + http::v2::client()->download(url.toString().toStdString(), + [this, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + qWarning() + << "failed to retrieve image:" << url; + return; + } + + QPixmap img; + img.loadFromData(QByteArray(data.data(), data.size())); + emit imageDownloaded(img); + }); +} + +void +ImageItem::saveImage(const QString &filename, const QByteArray &data) +{ + try { + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(data); + file.close(); + } catch (const std::exception &ex) { + qDebug() << "Error while saving file to:" << ex.what(); + } +} + +void +ImageItem::init() { setMouseTracking(true); setCursor(Qt::PointingHandCursor); setAttribute(Qt::WA_Hover, true); + connect(this, &ImageItem::imageDownloaded, this, &ImageItem::setImage); + connect(this, &ImageItem::imageSaved, this, &ImageItem::saveImage); + downloadMedia(url_); +} + +ImageItem::ImageItem(const mtx::events::RoomEvent &event, QWidget *parent) + : QWidget(parent) + , event_{event} +{ url_ = QString::fromStdString(event.content.url); text_ = QString::fromStdString(event.content.body); - QList url_parts = url_.toString().split("mxc://"); - - if (url_parts.size() != 2) { - qDebug() << "Invalid format for image" << url_.toString(); - return; - } - - QString media_params = url_parts[1]; - url_ = QString("%1/_matrix/media/r0/download/%2") - .arg(http::client()->getHomeServer().toString(), media_params); - - auto proxy = http::client()->downloadImage(url_); - - connect(proxy.data(), - &DownloadMediaProxy::imageDownloaded, - this, - [this, proxy](const QPixmap &img) { - proxy->deleteLater(); - setImage(img); - }); + init(); } ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) @@ -69,31 +94,7 @@ ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, , text_{filename} { Q_UNUSED(size); - - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - QList url_parts = url_.toString().split("mxc://"); - - if (url_parts.size() != 2) { - qDebug() << "Invalid format for image" << url_.toString(); - return; - } - - QString media_params = url_parts[1]; - url_ = QString("%1/_matrix/media/r0/download/%2") - .arg(http::client()->getHomeServer().toString(), media_params); - - auto proxy = http::client()->downloadImage(url_); - - connect(proxy.data(), - &DownloadMediaProxy::imageDownloaded, - this, - [proxy, this](const QPixmap &img) { - proxy->deleteLater(); - setImage(img); - }); + init(); } void @@ -102,8 +103,15 @@ ImageItem::openUrl() if (url_.toString().isEmpty()) return; - if (!QDesktopServices::openUrl(url_)) - qWarning() << "Could not open url" << url_.toString(); + auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString()); + auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4") + .arg(QString::fromStdString(http::v2::client()->server())) + .arg(http::v2::client()->port()) + .arg(QString::fromStdString(mxc_parts.server)) + .arg(QString::fromStdString(mxc_parts.media_id)); + + if (!QDesktopServices::openUrl(urlToOpen)) + qWarning() << "Could not open url" << urlToOpen; } QSize @@ -231,23 +239,17 @@ ImageItem::saveAs() if (filename.isEmpty()) return; - auto proxy = http::client()->downloadFile(url_); - connect(proxy.data(), - &DownloadMediaProxy::fileDownloaded, - this, - [proxy, filename](const QByteArray &data) { - proxy->deleteLater(); + http::v2::client()->download( + url_.toString().toStdString(), + [this, filename](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + qWarning() << "failed to retrieve image:" << url_; + return; + } - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &ex) { - qDebug() << "Error while saving file to:" << ex.what(); - } - }); + emit imageSaved(filename, QByteArray(data.data(), data.size())); + }); } diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cc index f5bcfd6e..c1f68847 100644 --- a/src/timeline/widgets/VideoItem.cc +++ b/src/timeline/widgets/VideoItem.cc @@ -27,15 +27,15 @@ void VideoItem::init() { - QList url_parts = url_.toString().split("mxc://"); - if (url_parts.size() != 2) { - qDebug() << "Invalid format for image" << url_.toString(); - return; - } + // QList url_parts = url_.toString().split("mxc://"); + // if (url_parts.size() != 2) { + // qDebug() << "Invalid format for image" << url_.toString(); + // return; + // } - QString media_params = url_parts[1]; - url_ = QString("%1/_matrix/media/r0/download/%2") - .arg(http::client()->getHomeServer().toString(), media_params); + // QString media_params = url_parts[1]; + // url_ = QString("%1/_matrix/media/r0/download/%2") + // .arg(http::client()->getHomeServer().toString(), media_params); } VideoItem::VideoItem(const mtx::events::RoomEvent &event, QWidget *parent) From 626c68091126f84819091840a011c50e26dcbd8d Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sun, 10 Jun 2018 20:03:45 +0300 Subject: [PATCH 04/18] Add support for displaying decrypted messages --- CMakeLists.txt | 1 + deps/CMakeLists.txt | 2 +- include/Cache.h | 131 ++++++++++++++++- include/ChatPage.h | 8 +- include/Logging.hpp | 3 + include/MainWindow.h | 1 - include/MatrixClient.h | 5 +- include/Olm.hpp | 65 +++++++++ include/timeline/TimelineView.h | 3 + src/Cache.cc | 250 +++++++++++++++++++++++++++++++- src/ChatPage.cc | 199 +++++++++++++++++++------ src/CommunitiesList.cc | 3 + src/Logging.cpp | 15 +- src/MainWindow.cc | 13 +- src/MatrixClient.cc | 8 +- src/Olm.cpp | 139 ++++++++++++++++++ src/RoomList.cc | 2 +- src/main.cc | 8 +- src/timeline/TimelineView.cc | 112 ++++++++++---- 19 files changed, 869 insertions(+), 99 deletions(-) create mode 100644 include/Olm.hpp create mode 100644 src/Olm.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index eedf9a69..ef20be39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,6 +184,7 @@ set(SRC_FILES src/MainWindow.cc src/MatrixClient.cc src/QuickSwitcher.cc + src/Olm.cpp src/RegisterPage.cc src/RoomInfoListItem.cc src/RoomList.cc diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index d6bab7e5..5f9b48ca 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs) set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de) set(MTXCLIENT_URL https://github.com/mujx/mtxclient) -set(MTXCLIENT_TAG 57f56d1fe73989dbe041a7ac0a28bf2e3286bf98) +set(MTXCLIENT_TAG 26aad7088b9532808ded9919d55f58711c0138e3) set(OLM_URL https://git.matrix.org/git/olm.git) set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) diff --git a/include/Cache.h b/include/Cache.h index afc7a148..994a6da7 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -17,13 +17,16 @@ #pragma once -#include #include #include + #include #include #include #include +#include +#include + using mtx::events::state::JoinRule; struct RoomMember @@ -140,6 +143,83 @@ struct RoomSearchResult Q_DECLARE_METATYPE(RoomSearchResult) Q_DECLARE_METATYPE(RoomInfo) +// Extra information associated with an outbound megolm session. +struct OutboundGroupSessionData +{ + std::string session_id; + std::string session_key; + uint64_t message_index = 0; +}; + +inline void +to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +{ + obj["session_id"] = msg.session_id; + obj["session_key"] = msg.session_key; + obj["message_index"] = msg.message_index; +} + +inline void +from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) +{ + msg.session_id = obj.at("session_id"); + msg.session_key = obj.at("session_key"); + msg.message_index = obj.at("message_index"); +} + +struct OutboundGroupSessionDataRef +{ + OlmOutboundGroupSession *session; + OutboundGroupSessionData data; +}; + +struct DevicePublicKeys +{ + std::string ed25519; + std::string curve25519; +}; + +inline void +to_json(nlohmann::json &obj, const DevicePublicKeys &msg) +{ + obj["ed25519"] = msg.ed25519; + obj["curve25519"] = msg.curve25519; +} + +inline void +from_json(const nlohmann::json &obj, DevicePublicKeys &msg) +{ + msg.ed25519 = obj.at("ed25519"); + msg.curve25519 = obj.at("curve25519"); +} + +//! Represents a unique megolm session identifier. +struct MegolmSessionIndex +{ + //! The room in which this session exists. + std::string room_id; + //! The session_id of the megolm session. + std::string session_id; + //! The curve25519 public key of the sender. + std::string sender_key; + + //! Representation to be used in a hash map. + std::string to_hash() const { return room_id + session_id + sender_key; } +}; + +struct OlmSessionStorage +{ + std::map outbound_sessions; + std::map group_inbound_sessions; + std::map group_outbound_sessions; + std::map group_outbound_session_data; + + // Guards for accessing critical data. + std::mutex outbound_mtx; + std::mutex group_outbound_mtx; + std::mutex group_inbound_mtx; +}; + class Cache : public QObject { Q_OBJECT @@ -260,6 +340,48 @@ public: //! Check if we have sent a desktop notification for the given event id. bool isNotificationSent(const std::string &event_id); + //! Mark a room that uses e2e encryption. + void setEncryptedRoom(const std::string &room_id); + //! Save the public keys for a device. + void saveDeviceKeys(const std::string &device_id); + void getDeviceKeys(const std::string &device_id); + + //! Save the device list for a user. + void setDeviceList(const std::string &user_id, const std::vector &devices); + std::vector getDeviceList(const std::string &user_id); + + // + // Outbound Megolm Sessions + // + void saveOutboundMegolmSession(const MegolmSessionIndex &index, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); + OutboundGroupSessionDataRef getOutboundMegolmSession(const MegolmSessionIndex &index); + bool outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; + + // + // Inbound Megolm Sessions + // + void saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); + OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); + bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; + + // + // Outbound Olm Sessions + // + void saveOutboundOlmSession(const std::string &curve25519, + mtx::crypto::OlmSessionPtr session); + OlmSession *getOutboundOlmSession(const std::string &curve25519); + bool outboundOlmSessionsExists(const std::string &curve25519) noexcept; + + void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); + + void restoreSessions(); + + OlmSessionStorage session_storage; + private: //! Save an invited room. void saveInvite(lmdb::txn &txn, @@ -451,6 +573,13 @@ private: lmdb::dbi readReceiptsDb_; lmdb::dbi notificationsDb_; + lmdb::dbi devicesDb_; + lmdb::dbi deviceKeysDb_; + + lmdb::dbi inboundMegolmSessionDb_; + lmdb::dbi outboundMegolmSessionDb_; + lmdb::dbi outboundOlmSessionDb_; + QString localUserId_; QString cacheDirectory_; }; diff --git a/include/ChatPage.h b/include/ChatPage.h index e99e94ba..d8582993 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -29,8 +29,7 @@ #include "Cache.h" #include "CommunitiesList.h" #include "Community.h" - -#include +#include "MatrixClient.h" class OverlayModal; class QuickSwitcher; @@ -119,6 +118,7 @@ signals: void loggedOut(); void trySyncCb(); + void tryDelayedSyncCb(); void tryInitialSyncCb(); void leftRoom(const QString &room_id); @@ -146,8 +146,12 @@ private slots: private: static ChatPage *instance_; + //! Handler callback for initial sync. It doesn't run on the main thread so all + //! communication with the GUI should be done through signals. + void initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err); void tryInitialSync(); void trySync(); + void ensureOneTimeKeyCount(const std::map &counts); //! Check if the given room is currently open. bool isRoomActive(const QString &room_id) diff --git a/include/Logging.hpp b/include/Logging.hpp index c301d80d..bdbd3e2c 100644 --- a/include/Logging.hpp +++ b/include/Logging.hpp @@ -15,4 +15,7 @@ net(); std::shared_ptr db(); + +std::shared_ptr +crypto(); } diff --git a/include/MainWindow.h b/include/MainWindow.h index f0fa9a08..b068e8f6 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -59,7 +59,6 @@ class MainWindow : public QMainWindow public: explicit MainWindow(QWidget *parent = 0); - ~MainWindow(); static MainWindow *instance() { return instance_; }; void saveCurrentWindowSize(); diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 832d6cad..7ea5e0b7 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -11,12 +11,15 @@ Q_DECLARE_METATYPE(mtx::responses::Notifications) Q_DECLARE_METATYPE(mtx::responses::Rooms) Q_DECLARE_METATYPE(mtx::responses::Sync) Q_DECLARE_METATYPE(std::string) -Q_DECLARE_METATYPE(std::vector); +Q_DECLARE_METATYPE(std::vector) namespace http { namespace v2 { mtx::http::Client * client(); + +bool +is_logged_in(); } //! Initialize the http module diff --git a/include/Olm.hpp b/include/Olm.hpp new file mode 100644 index 00000000..2f7b1d64 --- /dev/null +++ b/include/Olm.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; + +namespace olm { + +struct OlmCipherContent +{ + std::string body; + uint8_t type; +}; + +inline void +from_json(const nlohmann::json &obj, OlmCipherContent &msg) +{ + msg.body = obj.at("body"); + msg.type = obj.at("type"); +} + +struct OlmMessage +{ + std::string sender_key; + std::string sender; + + using RecipientKey = std::string; + std::map ciphertext; +}; + +inline void +from_json(const nlohmann::json &obj, OlmMessage &msg) +{ + if (obj.at("type") != "m.room.encrypted") + throw std::invalid_argument("invalid type for olm message"); + + if (obj.at("content").at("algorithm") != OLM_ALGO) + throw std::invalid_argument("invalid algorithm for olm message"); + + msg.sender = obj.at("sender"); + msg.sender_key = obj.at("content").at("sender_key"); + msg.ciphertext = + obj.at("content").at("ciphertext").get>(); +} + +mtx::crypto::OlmClient * +client(); + +void +handle_to_device_messages(const std::vector &msgs); + +void +handle_olm_message(const OlmMessage &msg); + +void +handle_olm_normal_message(const std::string &sender, + const std::string &sender_key, + const OlmCipherContent &content); + +void +handle_pre_key_olm_message(const std::string &sender, + const std::string &sender_key, + const OlmCipherContent &content); +} // namespace olm diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index 30af97fb..88857222 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -149,6 +149,9 @@ private: QWidget *relativeWidget(TimelineItem *item, int dt) const; + TimelineEvent parseEncryptedEvent( + const mtx::events::EncryptedEvent &e); + //! Callback for all message sending. void sendRoomMessageHandler(const std::string &txn_id, const mtx::responses::EventId &res, diff --git a/src/Cache.cc b/src/Cache.cc index 2a555425..150990b7 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -31,25 +31,42 @@ //! Should be changed when a breaking change occurs in the cache format. //! This will reset client's data. -static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.05.11"); +static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.06.10"); +static const std::string SECRET("secret"); static const lmdb::val NEXT_BATCH_KEY("next_batch"); +static const lmdb::val OLM_ACCOUNT_KEY("olm_account"); static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); //! Cache databases and their format. //! //! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc). //! Format: room_id -> RoomInfo -static constexpr const char *ROOMS_DB = "rooms"; -static constexpr const char *INVITES_DB = "invites"; +constexpr auto ROOMS_DB("rooms"); +constexpr auto INVITES_DB("invites"); //! Keeps already downloaded media for reuse. //! Format: matrix_url -> binary data. -static constexpr const char *MEDIA_DB = "media"; +constexpr auto MEDIA_DB("media"); //! Information that must be kept between sync requests. -static constexpr const char *SYNC_STATE_DB = "sync_state"; +constexpr auto SYNC_STATE_DB("sync_state"); //! Read receipts per room/event. -static constexpr const char *READ_RECEIPTS_DB = "read_receipts"; -static constexpr const char *NOTIFICATIONS_DB = "sent_notifications"; +constexpr auto READ_RECEIPTS_DB("read_receipts"); +constexpr auto NOTIFICATIONS_DB("sent_notifications"); + +//! Encryption related databases. + +//! user_id -> list of devices +constexpr auto DEVICES_DB("devices"); +//! device_id -> device keys +constexpr auto DEVICE_KEYS_DB("device_keys"); +//! room_ids that have encryption enabled. +// constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); + +//! MegolmSessionIndex -> pickled OlmInboundGroupSession +constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); +//! MegolmSessionIndex -> pickled OlmOutboundGroupSession +constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); +constexpr auto OUTBOUND_OLM_SESSIONS_DB("outbound_olm_sessions"); using CachedReceipts = std::multimap>; using Receipts = std::map>; @@ -79,7 +96,7 @@ client() { return instance_.get(); } -} +} // namespace cache Cache::Cache(const QString &userId, QObject *parent) : QObject{parent} @@ -90,6 +107,11 @@ Cache::Cache(const QString &userId, QObject *parent) , mediaDb_{0} , readReceiptsDb_{0} , notificationsDb_{0} + , devicesDb_{0} + , deviceKeysDb_{0} + , inboundMegolmSessionDb_{0} + , outboundMegolmSessionDb_{0} + , outboundOlmSessionDb_{0} , localUserId_{userId} {} @@ -149,9 +171,221 @@ Cache::setup() mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE); readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); + + // Device management + devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE); + deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE); + + // Session management + inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + outboundOlmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_OLM_SESSIONS_DB, MDB_CREATE); + txn.commit(); } +// +// Device Management +// + +// +// Session Management +// + +void +Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session) +{ + using namespace mtx::crypto; + const auto key = index.to_hash(); + const auto pickled = pickle(session.get(), SECRET); + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled)); + txn.commit(); + + { + std::unique_lock lock(session_storage.group_inbound_mtx); + session_storage.group_inbound_sessions[key] = std::move(session); + } +} + +OlmInboundGroupSession * +Cache::getInboundMegolmSession(const MegolmSessionIndex &index) +{ + std::unique_lock lock(session_storage.group_inbound_mtx); + return session_storage.group_inbound_sessions[index.to_hash()].get(); +} + +bool +Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept +{ + std::unique_lock lock(session_storage.group_inbound_mtx); + return session_storage.group_inbound_sessions.find(index.to_hash()) != + session_storage.group_inbound_sessions.end(); +} + +void +Cache::saveOutboundMegolmSession(const MegolmSessionIndex &index, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session) +{ + using namespace mtx::crypto; + const auto key = index.to_hash(); + const auto pickled = pickle(session.get(), SECRET); + + json j; + j["data"] = data; + j["session"] = pickled; + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(key), lmdb::val(j.dump())); + txn.commit(); + + { + std::unique_lock lock(session_storage.group_outbound_mtx); + session_storage.group_outbound_session_data[key] = data; + session_storage.group_outbound_sessions[key] = std::move(session); + } +} + +bool +Cache::outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept +{ + const auto key = index.to_hash(); + + std::unique_lock lock(session_storage.group_outbound_mtx); + return (session_storage.group_outbound_sessions.find(key) != + session_storage.group_outbound_sessions.end()) && + (session_storage.group_outbound_session_data.find(key) != + session_storage.group_outbound_session_data.end()); +} + +OutboundGroupSessionDataRef +Cache::getOutboundMegolmSession(const MegolmSessionIndex &index) +{ + const auto key = index.to_hash(); + std::unique_lock lock(session_storage.group_outbound_mtx); + return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[key].get(), + session_storage.group_outbound_session_data[key]}; +} + +void +Cache::saveOutboundOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +{ + using namespace mtx::crypto; + const auto pickled = pickle(session.get(), SECRET); + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(curve25519), lmdb::val(pickled)); + txn.commit(); + + { + std::unique_lock lock(session_storage.outbound_mtx); + session_storage.outbound_sessions[curve25519] = std::move(session); + } +} + +bool +Cache::outboundOlmSessionsExists(const std::string &curve25519) noexcept +{ + std::unique_lock lock(session_storage.outbound_mtx); + return session_storage.outbound_sessions.find(curve25519) != + session_storage.outbound_sessions.end(); +} + +OlmSession * +Cache::getOutboundOlmSession(const std::string &curve25519) +{ + std::unique_lock lock(session_storage.outbound_mtx); + return session_storage.outbound_sessions.at(curve25519).get(); +} + +void +Cache::saveOlmAccount(const std::string &data) +{ + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, syncStateDb_, OLM_ACCOUNT_KEY, lmdb::val(data)); + txn.commit(); +} + +void +Cache::restoreSessions() +{ + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + std::string key, value; + + // + // Inbound Megolm Sessions + // + { + auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_); + while (cursor.get(key, value, MDB_NEXT)) { + auto session = unpickle(value, SECRET); + session_storage.group_inbound_sessions[key] = std::move(session); + } + cursor.close(); + } + + // + // Outbound Megolm Sessions + // + { + auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_); + while (cursor.get(key, value, MDB_NEXT)) { + json obj; + + try { + obj = json::parse(value); + + session_storage.group_outbound_session_data[key] = + obj.at("data").get(); + + auto session = + unpickle(obj.at("session"), SECRET); + session_storage.group_outbound_sessions[key] = std::move(session); + } catch (const nlohmann::json::exception &e) { + log::db()->warn("failed to parse outbound megolm session data: {}", + e.what()); + } + } + cursor.close(); + } + + // + // Outbound Olm Sessions + // + { + auto cursor = lmdb::cursor::open(txn, outboundOlmSessionDb_); + while (cursor.get(key, value, MDB_NEXT)) { + auto session = unpickle(value, SECRET); + session_storage.outbound_sessions[key] = std::move(session); + } + cursor.close(); + } + + txn.commit(); + + log::db()->info("sessions restored"); +} + +std::string +Cache::restoreOlmAccount() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val pickled; + lmdb::dbi_get(txn, syncStateDb_, OLM_ACCOUNT_KEY, pickled); + txn.commit(); + + return std::string(pickled.data(), pickled.size()); +} + +// +// Media Management +// + void Cache::saveImage(const std::string &url, const std::string &img_data) { diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 64ce69d6..a5a6a8c0 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -25,6 +25,7 @@ #include "Logging.hpp" #include "MainWindow.h" #include "MatrixClient.h" +#include "Olm.hpp" #include "OverlayModal.h" #include "QuickSwitcher.h" #include "RoomList.h" @@ -43,8 +44,12 @@ #include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" +// TODO: Needs to be updated with an actual secret. +static const std::string STORAGE_SECRET_KEY("secret"); + ChatPage *ChatPage::instance_ = nullptr; constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; +constexpr size_t MAX_ONETIME_KEYS = 50; ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) : QWidget(parent) @@ -612,6 +617,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync); connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync); + connect(this, &ChatPage::tryDelayedSyncCb, this, [this]() { + QTimer::singleShot(5000, this, &ChatPage::trySync); + }); connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); @@ -728,6 +736,11 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) }); // TODO http::client()->getOwnCommunities(); + // The Olm client needs the user_id & device_id that will be included + // in the generated payloads & keys. + olm::client()->set_user_id(http::v2::client()->user_id().to_string()); + olm::client()->set_device_id(http::v2::client()->device_id()); + cache::init(userid); try { @@ -741,6 +754,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) if (cache::client()->isInitialized()) { loadStateFromCache(); + // TODO: Bootstrap olm client with saved data. return; } } catch (const lmdb::error &e) { @@ -749,6 +763,22 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) log::net()->info("falling back to initial sync"); } + try { + // It's the first time syncing with this device + // There isn't a saved olm account to restore. + log::crypto()->info("creating new olm account"); + olm::client()->create_new_account(); + cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + } catch (const lmdb::error &e) { + log::crypto()->critical("failed to save olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } catch (const mtx::crypto::olm_exception &e) { + log::crypto()->critical("failed to create new olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } + tryInitialSync(); } @@ -826,16 +856,29 @@ ChatPage::loadStateFromCache() QtConcurrent::run([this]() { try { + cache::client()->restoreSessions(); + olm::client()->load(cache::client()->restoreOlmAccount(), + STORAGE_SECRET_KEY); + cache::client()->populateMembers(); emit initializeEmptyViews(cache::client()->joinedRooms()); emit initializeRoomList(cache::client()->roomInfo()); + } catch (const mtx::crypto::olm_exception &e) { + log::crypto()->critical("failed to restore olm account: {}", e.what()); + emit dropToLoginPageCb( + tr("Failed to restore OLM account. Please login again.")); + return; } catch (const lmdb::error &e) { - std::cout << "load cache error:" << e.what() << '\n'; - // TODO Clear cache and restart. + log::db()->critical("failed to restore cache: {}", e.what()); + emit dropToLoginPageCb( + tr("Failed to restore save data. Please login again.")); return; } + log::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + log::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + // Start receiving events. emit trySyncCb(); @@ -1008,49 +1051,40 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) void ChatPage::tryInitialSync() { - mtx::http::SyncOpts opts; - opts.timeout = 0; + log::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + log::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); - log::net()->info("trying initial sync"); + // Upload one time keys for the device. + log::crypto()->info("generating one time keys"); + olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS); - http::v2::client()->sync( - opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { + http::v2::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { if (err) { - const auto error = QString::fromStdString(err->matrix_error.error); - const auto msg = tr("Please try to login again: %1").arg(error); - const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); const int status_code = static_cast(err->status_code); - - log::net()->error("sync error: {} {}", status_code, err_code); - - switch (status_code) { - case 502: - case 504: - case 524: { - emit tryInitialSyncCb(); - return; - } - default: { - emit dropToLoginPageCb(msg); - return; - } - } - } - - log::net()->info("initial sync completed"); - - try { - cache::client()->saveState(res); - emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::client()->roomInfo()); - } catch (const lmdb::error &e) { - log::db()->error("{}", e.what()); + log::crypto()->critical("failed to upload one time keys: {} {}", + err->matrix_error.error, + status_code); + // TODO We should have a timeout instead of keeping hammering the server. emit tryInitialSyncCb(); return; } - emit trySyncCb(); - emit contentLoaded(); + olm::client()->mark_keys_as_published(); + for (const auto &entry : res.one_time_key_counts) + log::net()->info( + "uploaded {} {} one-time keys", entry.second, entry.first); + + log::net()->info("trying initial sync"); + + mtx::http::SyncOpts opts; + opts.timeout = 0; + http::v2::client()->sync(opts, + std::bind(&ChatPage::initialSyncHandler, + this, + std::placeholders::_1, + std::placeholders::_2)); }); } @@ -1079,24 +1113,31 @@ ChatPage::trySync() log::net()->error("sync error: {} {}", status_code, err_code); + if (status_code <= 0 || status_code >= 600) { + if (!http::v2::is_logged_in()) + return; + + emit dropToLoginPageCb(msg); + return; + } + switch (status_code) { case 502: case 504: case 524: { - emit trySync(); + emit trySyncCb(); return; } case 401: case 403: { - // We are logged out. - if (http::v2::client()->access_token().empty()) + if (!http::v2::is_logged_in()) return; emit dropToLoginPageCb(msg); return; } default: { - emit trySync(); + emit tryDelayedSyncCb(); return; } } @@ -1104,9 +1145,14 @@ ChatPage::trySync() log::net()->debug("sync completed: {}", res.next_batch); + // Ensure that we have enough one-time keys available. + ensureOneTimeKeyCount(res.device_one_time_keys_count); + // TODO: fine grained error handling try { cache::client()->saveState(res); + olm::handle_to_device_messages(res.to_device); + emit syncUI(res.rooms); auto updates = cache::client()->roomUpdates(res); @@ -1194,3 +1240,74 @@ ChatPage::sendTypingNotifications() } }); } + +void +ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) +{ + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast(err->status_code); + + log::net()->error("sync error: {} {}", status_code, err_code); + + switch (status_code) { + case 502: + case 504: + case 524: { + emit tryInitialSyncCb(); + return; + } + default: { + emit dropToLoginPageCb(msg); + return; + } + } + } + + log::net()->info("initial sync completed"); + + try { + cache::client()->saveState(res); + + olm::handle_to_device_messages(res.to_device); + + emit initializeViews(std::move(res.rooms)); + emit initializeRoomList(cache::client()->roomInfo()); + } catch (const lmdb::error &e) { + log::db()->error("{}", e.what()); + emit tryInitialSyncCb(); + return; + } + + emit trySyncCb(); + emit contentLoaded(); +} + +void +ChatPage::ensureOneTimeKeyCount(const std::map &counts) +{ + for (const auto &entry : counts) { + if (entry.second < MAX_ONETIME_KEYS) { + const int nkeys = MAX_ONETIME_KEYS - entry.second; + + log::crypto()->info("uploading {} {} keys", nkeys, entry.first); + olm::client()->generate_one_time_keys(nkeys); + + http::v2::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { + if (err) { + log::crypto()->warn( + "failed to update one-time keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + olm::client()->mark_keys_as_published(); + }); + } + } +} diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc index 8ccd5e9d..49affcb7 100644 --- a/src/CommunitiesList.cc +++ b/src/CommunitiesList.cc @@ -128,6 +128,9 @@ CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUr return; } + if (avatarUrl.isEmpty()) + return; + mtx::http::ThumbOpts opts; opts.mxc_url = avatarUrl.toStdString(); http::v2::client()->get_thumbnail( diff --git a/src/Logging.cpp b/src/Logging.cpp index c6c1c502..77e61e09 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -4,9 +4,10 @@ #include namespace { -std::shared_ptr db_logger = nullptr; -std::shared_ptr net_logger = nullptr; -std::shared_ptr main_logger = nullptr; +std::shared_ptr db_logger = nullptr; +std::shared_ptr net_logger = nullptr; +std::shared_ptr crypto_logger = nullptr; +std::shared_ptr main_logger = nullptr; constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6; constexpr auto MAX_LOG_FILES = 3; @@ -28,6 +29,8 @@ init(const std::string &file_path) net_logger = std::make_shared("net", std::begin(sinks), std::end(sinks)); main_logger = std::make_shared("main", std::begin(sinks), std::end(sinks)); db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); + crypto_logger = + std::make_shared("crypto", std::begin(sinks), std::end(sinks)); } std::shared_ptr @@ -47,4 +50,10 @@ db() { return db_logger; } + +std::shared_ptr +crypto() +{ + return crypto_logger; +} } diff --git a/src/MainWindow.cc b/src/MainWindow.cc index 9ba8b28e..cca51f03 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc @@ -46,15 +46,6 @@ MainWindow *MainWindow::instance_ = nullptr; -MainWindow::~MainWindow() -{ - if (http::v2::client() != nullptr) { - http::v2::client()->shutdown(); - // TODO: find out why waiting for the threads to join is slow. - http::v2::client()->close(); - } -} - MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , progressModal_{nullptr} @@ -154,9 +145,11 @@ MainWindow::MainWindow(QWidget *parent) QString token = settings.value("auth/access_token").toString(); QString home_server = settings.value("auth/home_server").toString(); QString user_id = settings.value("auth/user_id").toString(); + QString device_id = settings.value("auth/device_id").toString(); http::v2::client()->set_access_token(token.toStdString()); http::v2::client()->set_server(home_server.toStdString()); + http::v2::client()->set_device_id(device_id.toStdString()); try { using namespace mtx::identifiers; @@ -228,6 +221,7 @@ void MainWindow::showChatPage() { auto userid = QString::fromStdString(http::v2::client()->user_id().to_string()); + auto device_id = QString::fromStdString(http::v2::client()->device_id()); auto homeserver = QString::fromStdString(http::v2::client()->server() + ":" + std::to_string(http::v2::client()->port())); auto token = QString::fromStdString(http::v2::client()->access_token()); @@ -236,6 +230,7 @@ MainWindow::showChatPage() settings.setValue("auth/access_token", token); settings.setValue("auth/home_server", homeserver); settings.setValue("auth/user_id", userid); + settings.setValue("auth/device_id", device_id); showOverlayProgressBar(); diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index 0eb4658a..d4ab8e33 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -3,7 +3,7 @@ #include namespace { -auto v2_client_ = std::make_shared("matrix.org"); +auto v2_client_ = std::make_shared(); } namespace http { @@ -15,6 +15,12 @@ client() return v2_client_.get(); } +bool +is_logged_in() +{ + return !v2_client_->access_token().empty(); +} + } // namespace v2 void diff --git a/src/Olm.cpp b/src/Olm.cpp new file mode 100644 index 00000000..769b0234 --- /dev/null +++ b/src/Olm.cpp @@ -0,0 +1,139 @@ +#include "Olm.hpp" + +#include "Cache.h" +#include "Logging.hpp" + +using namespace mtx::crypto; + +namespace { +auto client_ = std::make_unique(); +} + +namespace olm { + +mtx::crypto::OlmClient * +client() +{ + return client_.get(); +} + +void +handle_to_device_messages(const std::vector &msgs) +{ + if (msgs.empty()) + return; + + log::crypto()->info("received {} to_device messages", msgs.size()); + + for (const auto &msg : msgs) { + try { + OlmMessage olm_msg = msg; + handle_olm_message(std::move(olm_msg)); + } catch (const nlohmann::json::exception &e) { + log::crypto()->warn( + "parsing error for olm message: {} {}", e.what(), msg.dump(2)); + } catch (const std::invalid_argument &e) { + log::crypto()->warn( + "validation error for olm message: {} {}", e.what(), msg.dump(2)); + } + } +} + +void +handle_olm_message(const OlmMessage &msg) +{ + log::crypto()->info("sender : {}", msg.sender); + log::crypto()->info("sender_key: {}", msg.sender_key); + + const auto my_key = olm::client()->identity_keys().curve25519; + + for (const auto &cipher : msg.ciphertext) { + // We skip messages not meant for the current device. + if (cipher.first != my_key) + continue; + + const auto type = cipher.second.type; + log::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); + + if (type == OLM_MESSAGE_TYPE_PRE_KEY) + handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second); + else + handle_olm_normal_message(msg.sender, msg.sender_key, cipher.second); + } +} + +void +handle_pre_key_olm_message(const std::string &sender, + const std::string &sender_key, + const OlmCipherContent &content) +{ + log::crypto()->info("opening olm session with {}", sender); + + OlmSessionPtr inbound_session = nullptr; + try { + inbound_session = olm::client()->create_inbound_session(content.body); + } catch (const olm_exception &e) { + log::crypto()->critical( + "failed to create inbound session with {}: {}", sender, e.what()); + return; + } + + if (!matches_inbound_session_from(inbound_session.get(), sender_key, content.body)) { + log::crypto()->warn("inbound olm session doesn't match sender's key ({})", sender); + return; + } + + mtx::crypto::BinaryBuf output; + try { + output = olm::client()->decrypt_message( + inbound_session.get(), OLM_MESSAGE_TYPE_PRE_KEY, content.body); + } catch (const olm_exception &e) { + log::crypto()->critical( + "failed to decrypt olm message {}: {}", content.body, e.what()); + return; + } + + auto plaintext = json::parse(std::string((char *)output.data(), output.size())); + log::crypto()->info("decrypted message: \n {}", plaintext.dump(2)); + + std::string room_id, session_id, session_key; + try { + room_id = plaintext.at("content").at("room_id"); + session_id = plaintext.at("content").at("session_id"); + session_key = plaintext.at("content").at("session_key"); + } catch (const nlohmann::json::exception &e) { + log::crypto()->critical( + "failed to parse plaintext olm message: {} {}", e.what(), plaintext.dump(2)); + return; + } + + MegolmSessionIndex index; + index.room_id = room_id; + index.session_id = session_id; + index.sender_key = sender_key; + + if (!cache::client()->inboundMegolmSessionExists(index)) { + auto megolm_session = olm::client()->init_inbound_group_session(session_key); + + try { + cache::client()->saveInboundMegolmSession(index, std::move(megolm_session)); + } catch (const lmdb::error &e) { + log::crypto()->critical("failed to save inbound megolm session: {}", + e.what()); + return; + } + + log::crypto()->info("established inbound megolm session ({}, {})", room_id, sender); + } else { + log::crypto()->warn( + "inbound megolm session already exists ({}, {})", room_id, sender); + } +} + +void +handle_olm_normal_message(const std::string &, const std::string &, const OlmCipherContent &) +{ + log::crypto()->warn("olm(1) not implemeted yet"); +} + +} // namespace olm diff --git a/src/RoomList.cc b/src/RoomList.cc index d3ed2e66..4891f746 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc @@ -96,7 +96,7 @@ RoomList::updateAvatar(const QString &room_id, const QString &url) opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { if (err) { log::net()->warn( - "failed to download thumbnail: {}, {} - {}", + "failed to download room avatar: {} {} {}", opts.mxc_url, mtx::errors::to_string(err->matrix_error.errcode), err->matrix_error.error); diff --git a/src/main.cc b/src/main.cc index 1df8d0c9..13a712f4 100644 --- a/src/main.cc +++ b/src/main.cc @@ -149,7 +149,13 @@ main(int argc, char *argv[]) !settings.value("user/window/tray", true).toBool()) w.show(); - QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize); + QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() { + w.saveCurrentWindowSize(); + if (http::v2::client() != nullptr) { + http::v2::client()->shutdown(); + http::v2::client()->close(); + } + }); log::main()->info("starting nheko {}", nheko::version); diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 5ef390a9..9baa1f4a 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -24,6 +24,7 @@ #include "Config.h" #include "FloatingButton.h" #include "Logging.hpp" +#include "Olm.hpp" #include "UserSettingsPage.h" #include "Utils.h" @@ -235,19 +236,19 @@ TimelineItem * TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, TimelineDirection direction) { - namespace msg = mtx::events::msg; - using AudioEvent = mtx::events::RoomEvent; - using EmoteEvent = mtx::events::RoomEvent; - using FileEvent = mtx::events::RoomEvent; - using ImageEvent = mtx::events::RoomEvent; - using NoticeEvent = mtx::events::RoomEvent; - using TextEvent = mtx::events::RoomEvent; - using VideoEvent = mtx::events::RoomEvent; + using namespace mtx::events; - if (mpark::holds_alternative>(event)) { - auto redaction_event = - mpark::get>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); + using AudioEvent = RoomEvent; + using EmoteEvent = RoomEvent; + using FileEvent = RoomEvent; + using ImageEvent = RoomEvent; + using NoticeEvent = RoomEvent; + using TextEvent = RoomEvent; + using VideoEvent = RoomEvent; + + if (mpark::holds_alternative>(event)) { + auto redaction_event = mpark::get>(event); + const auto event_id = QString::fromStdString(redaction_event.redacts); QTimer::singleShot(0, this, [event_id, this]() { if (eventIds_.contains(event_id)) @@ -255,35 +256,88 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents & }); return nullptr; - } else if (mpark::holds_alternative>(event)) { - auto audio = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto audio = mpark::get>(event); return processMessageEvent(audio, direction); - } else if (mpark::holds_alternative>(event)) { - auto emote = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto emote = mpark::get>(event); return processMessageEvent(emote, direction); - } else if (mpark::holds_alternative>(event)) { - auto file = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto file = mpark::get>(event); return processMessageEvent(file, direction); - } else if (mpark::holds_alternative>(event)) { - auto image = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto image = mpark::get>(event); return processMessageEvent(image, direction); - } else if (mpark::holds_alternative>(event)) { - auto notice = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto notice = mpark::get>(event); return processMessageEvent(notice, direction); - } else if (mpark::holds_alternative>(event)) { - auto text = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto text = mpark::get>(event); return processMessageEvent(text, direction); - } else if (mpark::holds_alternative>(event)) { - auto video = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto video = mpark::get>(event); return processMessageEvent(video, direction); - } else if (mpark::holds_alternative(event)) { - return processMessageEvent( - mpark::get(event), direction); + } else if (mpark::holds_alternative(event)) { + return processMessageEvent(mpark::get(event), + direction); + } else if (mpark::holds_alternative>(event)) { + auto decrypted = + parseEncryptedEvent(mpark::get>(event)); + return parseMessageEvent(decrypted, direction); } return nullptr; } +TimelineEvent +TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent &e) +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = "-- Encrypted Event (No keys found for decryption) --"; + + if (!cache::client()->inboundMegolmSessionExists(index)) { + log::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return dummy; + } + + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + + const auto msg_str = std::string((char *)res.data.data(), res.data.size()); + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + + log::crypto()->info("decrypted data: \n {}", body.dump(2)); + + json event_array = json::array(); + event_array.push_back(body); + + std::vector events; + mtx::responses::utils::parse_timeline_events(event_array, events); + + if (events.size() == 1) + return events.at(0); + + dummy.content.body = "-- Encrypted Event (Unknown event type) --"; + return dummy; +} + void TimelineView::renderBottomEvents(const std::vector &events) { From 36cb62748b1a1c98d64ea3e8cfce0ef1cb5ba0c4 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Tue, 12 Jun 2018 09:45:26 +0300 Subject: [PATCH 05/18] Add menu option to enable encryption in a private room --- include/Cache.h | 2 + include/dialogs/RoomSettings.hpp | 23 ++++--- src/Cache.cc | 26 +++++++- src/ChatPage.cc | 4 ++ src/dialogs/RoomSettings.cpp | 100 +++++++++++++++++++++++++++++-- src/timeline/TimelineView.cc | 2 + 6 files changed, 142 insertions(+), 15 deletions(-) diff --git a/include/Cache.h b/include/Cache.h index 994a6da7..97133b0c 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -342,6 +342,8 @@ public: //! Mark a room that uses e2e encryption. void setEncryptedRoom(const std::string &room_id); + bool isRoomEncrypted(const std::string &room_id); + //! Save the public keys for a device. void saveDeviceKeys(const std::string &device_id); void getDeviceKeys(const std::string &device_id); diff --git a/include/dialogs/RoomSettings.hpp b/include/dialogs/RoomSettings.hpp index 9a01d5c9..6cab03b7 100644 --- a/include/dialogs/RoomSettings.hpp +++ b/include/dialogs/RoomSettings.hpp @@ -5,16 +5,17 @@ #include "Cache.h" -class FlatButton; -class TextField; -class QHBoxLayout; class Avatar; -class QPixmap; -class QLayout; -class QLabel; +class FlatButton; class QComboBox; -class TextField; +class QHBoxLayout; class QLabel; +class QLabel; +class QLayout; +class QPixmap; +class TextField; +class TextField; +class Toggle; template class QSharedPointer; @@ -84,6 +85,7 @@ public: signals: void closing(); + void enableEncryptionError(const QString &msg); protected: void paintEvent(QPaintEvent *event) override; @@ -98,13 +100,15 @@ private: void setupEditButton(); //! Retrieve the current room information from cache. void retrieveRoomInfo(); + void enableEncryption(); //! Whether the user would be able to change the name or the topic of the room. - bool hasEditRights_ = true; + bool hasEditRights_ = true; + bool usesEncryption_ = false; QHBoxLayout *editLayout_; // Button section - FlatButton *saveBtn_; + FlatButton *okBtn_; FlatButton *cancelBtn_; FlatButton *editFieldsBtn_; @@ -116,6 +120,7 @@ private: TopSection *topSection_; QComboBox *accessCombo; + Toggle *encryptionToggle_; }; } // dialogs diff --git a/src/Cache.cc b/src/Cache.cc index 150990b7..48b1fdaf 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -60,7 +60,7 @@ constexpr auto DEVICES_DB("devices"); //! device_id -> device keys constexpr auto DEVICE_KEYS_DB("device_keys"); //! room_ids that have encryption enabled. -// constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); +constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); //! MegolmSessionIndex -> pickled OlmInboundGroupSession constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); @@ -184,6 +184,30 @@ Cache::setup() txn.commit(); } +void +Cache::setEncryptedRoom(const std::string &room_id) +{ + log::db()->info("mark room {} as encrypted", room_id); + + auto txn = lmdb::txn::begin(env_); + auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + lmdb::dbi_put(txn, db, lmdb::val(room_id), lmdb::val("0")); + txn.commit(); +} + +bool +Cache::isRoomEncrypted(const std::string &room_id) +{ + lmdb::val unused; + + auto txn = lmdb::txn::begin(env_); + auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + auto res = lmdb::dbi_get(txn, db, lmdb::val(room_id), unused); + txn.commit(); + + return res; +} + // // Device Management // diff --git a/src/ChatPage.cc b/src/ChatPage.cc index a5a6a8c0..3f43831c 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -245,6 +245,10 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) return; typingRefresher_->stop(); + + if (current_room_.isEmpty()) + return; + http::v2::client()->stop_typing( current_room_.toStdString(), [](mtx::http::RequestErr err) { if (err) { diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 2396fc19..a091c8bc 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -1,16 +1,20 @@ #include "Avatar.h" +#include "ChatPage.h" #include "Config.h" #include "FlatButton.h" +#include "Logging.hpp" #include "MatrixClient.h" #include "Painter.h" #include "TextField.h" #include "Theme.h" #include "Utils.h" #include "dialogs/RoomSettings.hpp" +#include "ui/ToggleButton.h" #include #include #include +#include #include #include #include @@ -188,8 +192,8 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) layout->setSpacing(15); layout->setMargin(20); - saveBtn_ = new FlatButton("SAVE", this); - saveBtn_->setFontSize(conf::btn::fontSize); + okBtn_ = new FlatButton(tr("OK"), this); + okBtn_->setFontSize(conf::btn::fontSize); cancelBtn_ = new FlatButton(tr("CANCEL"), this); cancelBtn_->setFontSize(conf::btn::fontSize); @@ -197,7 +201,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) btnLayout->setSpacing(0); btnLayout->setMargin(0); btnLayout->addStretch(1); - btnLayout->addWidget(saveBtn_); + btnLayout->addWidget(okBtn_); btnLayout->addWidget(cancelBtn_); auto notifOptionLayout_ = new QHBoxLayout; @@ -236,6 +240,61 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) accessOptionLayout->addWidget(accessLabel); accessOptionLayout->addWidget(accessCombo); + auto encryptionOptionLayout = new QHBoxLayout; + encryptionOptionLayout->setMargin(SettingsMargin); + auto encryptionLabel = new QLabel(tr("Encryption"), this); + encryptionLabel->setStyleSheet("font-size: 15px;"); + encryptionToggle_ = new Toggle(this); + connect(encryptionToggle_, &Toggle::toggled, this, [this](bool isOn) { + if (isOn) + return; + + QFont font; + font.setPixelSize(conf::fontSize); + + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Question); + msgBox.setFont(font); + msgBox.setWindowTitle(tr("End-to-End Encryption")); + msgBox.setText(tr( + "Encryption is currently experimental and things might break unexpectedly.
" + "Please take note that it can't be disabled afterwards.")); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Save); + int ret = msgBox.exec(); + + switch (ret) { + case QMessageBox::Ok: { + encryptionToggle_->setState(false); + encryptionToggle_->setEnabled(false); + enableEncryption(); + break; + } + default: { + encryptionToggle_->setState(true); + encryptionToggle_->setEnabled(true); + break; + } + } + }); + + encryptionOptionLayout->addWidget(encryptionLabel); + encryptionOptionLayout->addWidget(encryptionToggle_, 0, Qt::AlignBottom | Qt::AlignRight); + + // Disable encryption button. + if (usesEncryption_) { + encryptionToggle_->setState(false); + encryptionToggle_->setEnabled(false); + } else { + encryptionToggle_->setState(true); + } + + // Hide encryption option for public rooms. + if (!usesEncryption_ && (info_.join_rule == JoinRule::Public)) { + encryptionToggle_->hide(); + encryptionLabel->hide(); + } + QFont font; font.setPixelSize(18); font.setWeight(70); @@ -255,10 +314,18 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) layout->addLayout(editLayout_); layout->addLayout(notifOptionLayout_); layout->addLayout(accessOptionLayout); + layout->addLayout(encryptionOptionLayout); layout->addLayout(btnLayout); connect(cancelBtn_, &QPushButton::clicked, this, &RoomSettings::closing); - connect(saveBtn_, &QPushButton::clicked, this, &RoomSettings::saveSettings); + connect(okBtn_, &QPushButton::clicked, this, &RoomSettings::saveSettings); + + connect(this, &RoomSettings::enableEncryptionError, this, [this](const QString &msg) { + encryptionToggle_->setState(true); + encryptionToggle_->setEnabled(true); + + emit ChatPage::instance()->showNotification(msg); + }); } void @@ -308,7 +375,8 @@ void RoomSettings::retrieveRoomInfo() { try { - info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); + usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); + info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url))); } catch (const lmdb::error &e) { qWarning() << "failed to retrieve room info from cache" << room_id_; @@ -339,6 +407,28 @@ RoomSettings::saveSettings() closing(); } +void +RoomSettings::enableEncryption() +{ + const auto room_id = room_id_.toStdString(); + http::v2::client()->enable_encryption( + room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + int status_code = static_cast(err->status_code); + log::net()->warn("failed to enable encryption in room ({}): {} {}", + room_id, + err->matrix_error.error, + status_code); + emit enableEncryptionError( + tr("Failed to enable encryption: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + log::net()->info("enabled encryption on room ({})", room_id); + }); +} + void RoomSettings::paintEvent(QPaintEvent *) { diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 9baa1f4a..d004a543 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -284,6 +284,8 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents & auto decrypted = parseEncryptedEvent(mpark::get>(event)); return parseMessageEvent(decrypted, direction); + } else if (mpark::holds_alternative>(event)) { + cache::client()->setEncryptedRoom(room_id_.toStdString()); } return nullptr; From a97528b4322a3f114134722f8a4243b2938db6fd Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Tue, 12 Jun 2018 20:36:16 +0300 Subject: [PATCH 06/18] Fix bug where cache was initialized twice in a row --- include/ChatPage.h | 1 + src/Cache.cc | 18 +++---- src/ChatPage.cc | 116 +++++++++++++++++++++++++-------------------- src/main.cc | 14 ++++++ 4 files changed, 89 insertions(+), 60 deletions(-) diff --git a/include/ChatPage.h b/include/ChatPage.h index d8582993..ffea2914 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -152,6 +152,7 @@ private: void tryInitialSync(); void trySync(); void ensureOneTimeKeyCount(const std::map &counts); + void getProfileInfo(); //! Check if the given room is currently open. bool isRoomActive(const QString &room_id) diff --git a/src/Cache.cc b/src/Cache.cc index 48b1fdaf..abc6fae4 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -87,8 +87,7 @@ init(const QString &user_id) qRegisterMetaType>(); qRegisterMetaType>(); - if (!instance_) - instance_ = std::make_unique(user_id); + instance_ = std::make_unique(user_id); } Cache * @@ -113,14 +112,16 @@ Cache::Cache(const QString &userId, QObject *parent) , outboundMegolmSessionDb_{0} , outboundOlmSessionDb_{0} , localUserId_{userId} -{} +{ + setup(); +} void Cache::setup() { log::db()->debug("setting up cache"); - auto statePath = QString("%1/%2/state") + auto statePath = QString("%1/%2") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); @@ -558,10 +559,11 @@ Cache::nextBatchToken() const void Cache::deleteData() { - log::db()->info("deleting data"); - - if (!cacheDirectory_.isEmpty()) + // TODO: We need to remove the env_ while not accepting new requests. + if (!cacheDirectory_.isEmpty()) { QDir(cacheDirectory_).removeRecursively(); + log::db()->info("deleted cache files from disk"); + } } bool @@ -575,7 +577,7 @@ Cache::isFormatValid() txn.commit(); if (!res) - return false; + return true; std::string stored_version(current_version.data(), current_version.size()); diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 3f43831c..c10d4aa2 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -696,69 +696,28 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) http::v2::client()->set_server(homeserver.toStdString()); http::v2::client()->set_access_token(token.toStdString()); - http::v2::client()->get_profile( - userid.toStdString(), - [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { - if (err) { - log::net()->warn("failed to retrieve own profile info"); - return; - } - - emit setUserDisplayName(QString::fromStdString(res.display_name)); - - if (cache::client()) { - auto data = cache::client()->image(res.avatar_url); - if (!data.isNull()) { - emit setUserAvatar(QImage::fromData(data)); - return; - } - } - - if (res.avatar_url.empty()) - return; - - http::v2::client()->download( - res.avatar_url, - [this, res](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - log::net()->warn( - "failed to download user avatar: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(res.avatar_url, data); - - emit setUserAvatar( - QImage::fromData(QByteArray(data.data(), data.size()))); - }); - }); - // TODO http::client()->getOwnCommunities(); // The Olm client needs the user_id & device_id that will be included // in the generated payloads & keys. olm::client()->set_user_id(http::v2::client()->user_id().to_string()); olm::client()->set_device_id(http::v2::client()->device_id()); - cache::init(userid); - try { - cache::client()->setup(); + cache::init(userid); - if (!cache::client()->isFormatValid()) { + const bool isInitialized = cache::client()->isInitialized(); + const bool isValid = cache::client()->isFormatValid(); + + if (isInitialized && !isValid) { + log::db()->warn("breaking changes in cache"); + // TODO: Deleting session data but keep using the + // same device doesn't work. cache::client()->deleteData(); - cache::client()->setup(); - cache::client()->setCurrentFormat(); - } - if (cache::client()->isInitialized()) { + cache::init(userid); + cache::client()->setCurrentFormat(); + } else if (isInitialized) { loadStateFromCache(); - // TODO: Bootstrap olm client with saved data. return; } } catch (const lmdb::error &e) { @@ -783,6 +742,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) return; } + getProfileInfo(); tryInitialSync(); } @@ -858,6 +818,8 @@ ChatPage::loadStateFromCache() { log::db()->info("restoring state from cache"); + getProfileInfo(); + QtConcurrent::run([this]() { try { cache::client()->restoreSessions(); @@ -1315,3 +1277,53 @@ ChatPage::ensureOneTimeKeyCount(const std::map &counts) } } } + +void +ChatPage::getProfileInfo() +{ + QSettings settings; + const auto userid = settings.value("auth/user_id").toString().toStdString(); + + http::v2::client()->get_profile( + userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to retrieve own profile info"); + return; + } + + emit setUserDisplayName(QString::fromStdString(res.display_name)); + + if (cache::client()) { + auto data = cache::client()->image(res.avatar_url); + if (!data.isNull()) { + emit setUserAvatar(QImage::fromData(data)); + return; + } + } + + if (res.avatar_url.empty()) + return; + + http::v2::client()->download( + res.avatar_url, + [this, res](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to download user avatar: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + if (cache::client()) + cache::client()->saveImage(res.avatar_url, data); + + emit setUserAvatar( + QImage::fromData(QByteArray(data.data(), data.size()))); + }); + }); + // TODO http::client()->getOwnCommunities(); +} diff --git a/src/main.cc b/src/main.cc index 13a712f4..0a127962 100644 --- a/src/main.cc +++ b/src/main.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -48,6 +49,17 @@ screenCenter(int width, int height) return QPoint(x, y); } +void +createCacheDirectory() +{ + auto dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + + if (!QDir().mkpath(dir)) { + throw std::runtime_error( + ("Unable to create state directory:" + dir).toStdString().c_str()); + } +} + int main(int argc, char *argv[]) { @@ -112,6 +124,8 @@ main(int argc, char *argv[]) http::init(); + createCacheDirectory(); + try { log::init(QString("%1/nheko.log") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) From e5dd64c63a04df6f0b885c737cc73b383e79bd67 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Tue, 12 Jun 2018 22:35:10 +0300 Subject: [PATCH 07/18] Add method to convert PendingMessage's to event types Add more logging during message sending --- include/Cache.h | 2 +- include/timeline/TimelineView.h | 31 +++++- src/Cache.cc | 15 ++- src/timeline/TimelineView.cc | 159 ++++++++++++++++++++-------- src/timeline/TimelineViewManager.cc | 2 - 5 files changed, 151 insertions(+), 58 deletions(-) diff --git a/include/Cache.h b/include/Cache.h index 97133b0c..76266ebd 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -355,7 +355,7 @@ public: // // Outbound Megolm Sessions // - void saveOutboundMegolmSession(const MegolmSessionIndex &index, + void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr session); OutboundGroupSessionDataRef getOutboundMegolmSession(const MegolmSessionIndex &index); diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index 88857222..2c369d5f 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -48,8 +48,37 @@ struct PendingMessage uint64_t media_size; QString event_id; TimelineItem *widget; + bool is_encrypted = false; }; +template +MessageT +toRoomMessage(const PendingMessage &) = delete; + +template<> +mtx::events::msg::Audio +toRoomMessage(const PendingMessage &m); + +template<> +mtx::events::msg::Emote +toRoomMessage(const PendingMessage &m); + +template<> +mtx::events::msg::File +toRoomMessage(const PendingMessage &); + +template<> +mtx::events::msg::Image +toRoomMessage(const PendingMessage &m); + +template<> +mtx::events::msg::Text +toRoomMessage(const PendingMessage &); + +template<> +mtx::events::msg::Video +toRoomMessage(const PendingMessage &m); + // In which place new TimelineItems should be inserted. enum class TimelineDirection { @@ -318,7 +347,7 @@ TimelineView::addUserMessage(const QString &url, PendingMessage message; message.ty = MsgType; - message.txn_id = mtx::client::utils::random_token(); + message.txn_id = http::v2::client()->generate_txn_id(); message.body = url; message.filename = trimmed; message.mime = mime; diff --git a/src/Cache.cc b/src/Cache.cc index abc6fae4..35ad8f9d 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -251,12 +251,11 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept } void -Cache::saveOutboundMegolmSession(const MegolmSessionIndex &index, +Cache::saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr session) { using namespace mtx::crypto; - const auto key = index.to_hash(); const auto pickled = pickle(session.get(), SECRET); json j; @@ -264,13 +263,13 @@ Cache::saveOutboundMegolmSession(const MegolmSessionIndex &index, j["session"] = pickled; auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(key), lmdb::val(j.dump())); + lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); txn.commit(); { std::unique_lock lock(session_storage.group_outbound_mtx); - session_storage.group_outbound_session_data[key] = data; - session_storage.group_outbound_sessions[key] = std::move(session); + session_storage.group_outbound_session_data[room_id] = data; + session_storage.group_outbound_sessions[room_id] = std::move(session); } } @@ -302,7 +301,7 @@ Cache::saveOutboundOlmSession(const std::string &curve25519, mtx::crypto::OlmSes const auto pickled = pickle(session.get(), SECRET); auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(curve25519), lmdb::val(pickled)); + lmdb::dbi_put(txn, outboundOlmSessionDb_, lmdb::val(curve25519), lmdb::val(pickled)); txn.commit(); { @@ -372,8 +371,8 @@ Cache::restoreSessions() unpickle(obj.at("session"), SECRET); session_storage.group_outbound_sessions[key] = std::move(session); } catch (const nlohmann::json::exception &e) { - log::db()->warn("failed to parse outbound megolm session data: {}", - e.what()); + log::db()->critical( + "failed to parse outbound megolm session data: {}", e.what()); } } cursor.close(); diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index d004a543..67da5dc6 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -285,7 +285,12 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents & parseEncryptedEvent(mpark::get>(event)); return parseMessageEvent(decrypted, direction); } else if (mpark::holds_alternative>(event)) { - cache::client()->setEncryptedRoom(room_id_.toStdString()); + try { + cache::client()->setEncryptedRoom(room_id_.toStdString()); + } catch (const lmdb::error &e) { + log::db()->critical("failed to save room {} as encrypted", + room_id_.toStdString()); + } } return nullptr; @@ -588,6 +593,7 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction) void TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) { + log::main()->info("[{}] message was received by the server", txn_id); if (!pending_msgs_.isEmpty() && pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet auto msg = pending_msgs_.dequeue(); @@ -613,19 +619,28 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) TimelineItem *view_item = new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_); + PendingMessage message; + message.ty = ty; + message.txn_id = http::v2::client()->generate_txn_id(); + message.body = body; + message.widget = view_item; + + try { + message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); + } catch (const lmdb::error &e) { + log::db()->critical("failed to check encryption status of room {}", e.what()); + view_item->deleteLater(); + + // TODO: Send a notification to the user. + + return; + } + addTimelineItem(view_item); lastMessageDirection_ = TimelineDirection::Bottom; - QApplication::processEvents(); - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = ty; - message.txn_id = mtx::client::utils::random_token(); - message.body = body; - message.widget = view_item; handleNewUserMessage(message); } @@ -646,18 +661,21 @@ TimelineView::sendNextPendingMessage() using namespace mtx::events; PendingMessage &m = pending_msgs_.head(); + + log::main()->info("[{}] sending next queued message", m.txn_id); + + if (m.is_encrypted) { + // sendEncryptedMessage(m); + log::main()->info("[{}] sending encrypted event", m.txn_id); + return; + } + switch (m.ty) { case mtx::events::MessageType::Audio: { - msg::Audio audio; - audio.info.mimetype = m.mime.toStdString(); - audio.info.size = m.media_size; - audio.body = m.filename.toStdString(); - audio.url = m.body.toStdString(); - http::v2::client()->send_room_message( room_id_.toStdString(), m.txn_id, - audio, + toRoomMessage(m), std::bind(&TimelineView::sendRoomMessageHandler, this, m.txn_id, @@ -667,16 +685,10 @@ TimelineView::sendNextPendingMessage() break; } case mtx::events::MessageType::Image: { - msg::Image image; - image.info.mimetype = m.mime.toStdString(); - image.info.size = m.media_size; - image.body = m.filename.toStdString(); - image.url = m.body.toStdString(); - http::v2::client()->send_room_message( room_id_.toStdString(), m.txn_id, - image, + toRoomMessage(m), std::bind(&TimelineView::sendRoomMessageHandler, this, m.txn_id, @@ -686,16 +698,10 @@ TimelineView::sendNextPendingMessage() break; } case mtx::events::MessageType::Video: { - msg::Video video; - video.info.mimetype = m.mime.toStdString(); - video.info.size = m.media_size; - video.body = m.filename.toStdString(); - video.url = m.body.toStdString(); - http::v2::client()->send_room_message( room_id_.toStdString(), m.txn_id, - video, + toRoomMessage(m), std::bind(&TimelineView::sendRoomMessageHandler, this, m.txn_id, @@ -705,16 +711,10 @@ TimelineView::sendNextPendingMessage() break; } case mtx::events::MessageType::File: { - msg::File file; - file.info.mimetype = m.mime.toStdString(); - file.info.size = m.media_size; - file.body = m.filename.toStdString(); - file.url = m.body.toStdString(); - http::v2::client()->send_room_message( room_id_.toStdString(), m.txn_id, - file, + toRoomMessage(m), std::bind(&TimelineView::sendRoomMessageHandler, this, m.txn_id, @@ -724,13 +724,10 @@ TimelineView::sendNextPendingMessage() break; } case mtx::events::MessageType::Text: { - msg::Text text; - text.body = m.body.toStdString(); - http::v2::client()->send_room_message( room_id_.toStdString(), m.txn_id, - text, + toRoomMessage(m), std::bind(&TimelineView::sendRoomMessageHandler, this, m.txn_id, @@ -740,13 +737,10 @@ TimelineView::sendNextPendingMessage() break; } case mtx::events::MessageType::Emote: { - msg::Emote emote; - emote.body = m.body.toStdString(); - http::v2::client()->send_room_message( room_id_.toStdString(), m.txn_id, - emote, + toRoomMessage(m), std::bind(&TimelineView::sendRoomMessageHandler, this, m.txn_id, @@ -809,13 +803,15 @@ TimelineView::removePendingMessage(const std::string &txn_id) if (pending_sent_msgs_.isEmpty()) sendNextPendingMessage(); - return; + log::main()->info("[{}] removed message with sync", txn_id); } } for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { if (it->txn_id == txn_id) { int index = std::distance(pending_msgs_.begin(), it); pending_msgs_.removeAt(index); + + log::main()->info("[{}] removed message before sync", txn_id); return; } } @@ -1051,9 +1047,80 @@ TimelineView::sendRoomMessageHandler(const std::string &txn_id, mtx::http::RequestErr err) { if (err) { + const int status_code = static_cast(err->status_code); + log::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); emit messageFailed(txn_id); return; } emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); } + +template<> +mtx::events::msg::Audio +toRoomMessage(const PendingMessage &m) +{ + mtx::events::msg::Audio audio; + audio.info.mimetype = m.mime.toStdString(); + audio.info.size = m.media_size; + audio.body = m.filename.toStdString(); + audio.url = m.body.toStdString(); + return audio; +} + +template<> +mtx::events::msg::Image +toRoomMessage(const PendingMessage &m) +{ + mtx::events::msg::Image image; + image.info.mimetype = m.mime.toStdString(); + image.info.size = m.media_size; + image.body = m.filename.toStdString(); + image.url = m.body.toStdString(); + return image; +} + +template<> +mtx::events::msg::Video +toRoomMessage(const PendingMessage &m) +{ + mtx::events::msg::Video video; + video.info.mimetype = m.mime.toStdString(); + video.info.size = m.media_size; + video.body = m.filename.toStdString(); + video.url = m.body.toStdString(); + return video; +} + +template<> +mtx::events::msg::Emote +toRoomMessage(const PendingMessage &m) +{ + mtx::events::msg::Emote emote; + emote.body = m.body.toStdString(); + return emote; +} + +template<> +mtx::events::msg::File +toRoomMessage(const PendingMessage &m) +{ + mtx::events::msg::File file; + file.info.mimetype = m.mime.toStdString(); + file.info.size = m.media_size; + file.body = m.filename.toStdString(); + file.url = m.body.toStdString(); + return file; +} + +template<> +mtx::events::msg::Text +toRoomMessage(const PendingMessage &m) +{ + mtx::events::msg::Text text; + text.body = m.body.toStdString(); + return text; +} diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc index 9026463d..b6e7d50d 100644 --- a/src/timeline/TimelineViewManager.cc +++ b/src/timeline/TimelineViewManager.cc @@ -22,8 +22,6 @@ #include #include -#include "MatrixClient.h" - #include "timeline/TimelineView.h" #include "timeline/TimelineViewManager.h" #include "timeline/widgets/AudioItem.h" From 5d47cc3940076f6e7da4578d985253c869117f21 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Wed, 13 Jun 2018 12:28:00 +0300 Subject: [PATCH 08/18] Add support for sending encrypted messages --- include/Cache.h | 8 +- include/Olm.hpp | 7 + include/timeline/TimelineView.h | 1 + src/Cache.cc | 65 +++++++-- src/Olm.cpp | 27 ++++ src/timeline/TimelineView.cc | 249 +++++++++++++++++++++++++++++++- 6 files changed, 345 insertions(+), 12 deletions(-) diff --git a/include/Cache.h b/include/Cache.h index 76266ebd..b4dcdb90 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -286,6 +286,9 @@ public: bool isFormatValid(); void setCurrentFormat(); + //! Retrieve all the user ids from a room. + std::vector roomMembers(const std::string &room_id); + //! Check if the given user has power leve greater than than //! lowest power level of the given events. bool hasEnoughPowerLevel(const std::vector &eventTypes, @@ -358,8 +361,9 @@ public: void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr session); - OutboundGroupSessionDataRef getOutboundMegolmSession(const MegolmSessionIndex &index); - bool outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; + OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); + bool outboundMegolmSessionExists(const std::string &room_id) noexcept; + void updateOutboundMegolmSession(const std::string &room_id, int message_index); // // Inbound Megolm Sessions diff --git a/include/Olm.hpp b/include/Olm.hpp index 2f7b1d64..0839f01c 100644 --- a/include/Olm.hpp +++ b/include/Olm.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; @@ -62,4 +63,10 @@ void handle_pre_key_olm_message(const std::string &sender, const std::string &sender_key, const OlmCipherContent &content); + +mtx::events::msg::Encrypted +encrypt_group_message(const std::string &room_id, + const std::string &device_id, + const std::string &body); + } // namespace olm diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index 2c369d5f..5b5c2292 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -185,6 +185,7 @@ private: void sendRoomMessageHandler(const std::string &txn_id, const mtx::responses::EventId &res, mtx::http::RequestErr err); + void prepareEncryptedMessage(const PendingMessage &msg); //! Call the /messages endpoint to fill the timeline. void getMessages(); diff --git a/src/Cache.cc b/src/Cache.cc index 35ad8f9d..7c678b72 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -250,6 +250,36 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept session_storage.group_inbound_sessions.end(); } +void +Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index) +{ + using namespace mtx::crypto; + + if (!outboundMegolmSessionExists(room_id)) + return; + + OutboundGroupSessionData data; + OlmOutboundGroupSession *session; + { + std::unique_lock lock(session_storage.group_outbound_mtx); + data = session_storage.group_outbound_session_data[room_id]; + session = session_storage.group_outbound_sessions[room_id].get(); + + // Update with the current message. + data.message_index = message_index; + session_storage.group_outbound_session_data[room_id] = data; + } + + // Save the updated pickled data for the session. + json j; + j["data"] = data; + j["session"] = pickle(session, SECRET); + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); + txn.commit(); +} + void Cache::saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, @@ -274,24 +304,21 @@ Cache::saveOutboundMegolmSession(const std::string &room_id, } bool -Cache::outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept +Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept { - const auto key = index.to_hash(); - std::unique_lock lock(session_storage.group_outbound_mtx); - return (session_storage.group_outbound_sessions.find(key) != + return (session_storage.group_outbound_sessions.find(room_id) != session_storage.group_outbound_sessions.end()) && - (session_storage.group_outbound_session_data.find(key) != + (session_storage.group_outbound_session_data.find(room_id) != session_storage.group_outbound_session_data.end()); } OutboundGroupSessionDataRef -Cache::getOutboundMegolmSession(const MegolmSessionIndex &index) +Cache::getOutboundMegolmSession(const std::string &room_id) { - const auto key = index.to_hash(); std::unique_lock lock(session_storage.group_outbound_mtx); - return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[key].get(), - session_storage.group_outbound_session_data[key]}; + return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(), + session_storage.group_outbound_session_data[room_id]}; } void @@ -1537,6 +1564,26 @@ Cache::hasEnoughPowerLevel(const std::vector &eventTypes return user_level >= min_event_level; } +std::vector +Cache::roomMembers(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::vector members; + std::string user_id, unused; + + auto db = getMembersDb(txn, room_id); + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) + members.emplace_back(std::move(user_id)); + cursor.close(); + + txn.commit(); + + return members; +} + QHash Cache::DisplayNames; QHash Cache::AvatarUrls; diff --git a/src/Olm.cpp b/src/Olm.cpp index 769b0234..6e130277 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -136,4 +136,31 @@ handle_olm_normal_message(const std::string &, const std::string &, const OlmCip log::crypto()->warn("olm(1) not implemeted yet"); } +mtx::events::msg::Encrypted +encrypt_group_message(const std::string &room_id, + const std::string &device_id, + const std::string &body) +{ + using namespace mtx::events; + + // Always chech before for existence. + auto res = cache::client()->getOutboundMegolmSession(room_id); + auto payload = olm::client()->encrypt_group_message(res.session, body); + + // Prepare the m.room.encrypted event. + msg::Encrypted data; + data.ciphertext = std::string((char *)payload.data(), payload.size()); + data.sender_key = olm::client()->identity_keys().curve25519; + data.session_id = res.data.session_id; + data.device_id = device_id; + + auto message_index = olm_outbound_group_session_message_index(res.session); + log::crypto()->info("next message_index {}", message_index); + + // We need to re-pickle the session after we send a message to save the new message_index. + cache::client()->updateOutboundMegolmSession(room_id, message_index); + + return data; +} + } // namespace olm diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 67da5dc6..9276a7bc 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -34,6 +34,19 @@ #include "timeline/widgets/ImageItem.h" #include "timeline/widgets/VideoItem.h" +class StateKeeper +{ +public: + StateKeeper(std::function &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function fn_; +}; + using TimelineEvent = mtx::events::collections::TimelineEvents; DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) @@ -329,6 +342,7 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEventinfo("decrypted data: \n {}", body.dump(2)); @@ -665,7 +679,7 @@ TimelineView::sendNextPendingMessage() log::main()->info("[{}] sending next queued message", m.txn_id); if (m.is_encrypted) { - // sendEncryptedMessage(m); + prepareEncryptedMessage(std::move(m)); log::main()->info("[{}] sending encrypted event", m.txn_id); return; } @@ -1124,3 +1138,236 @@ toRoomMessage(const PendingMessage &m) text.body = m.body.toStdString(); return text; } + +void +TimelineView::prepareEncryptedMessage(const PendingMessage &msg) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json content; + + // Serialize the message to the plaintext that will be encrypted. + switch (msg.ty) { + case MessageType::Audio: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Emote: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::File: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Image: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Text: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Video: { + content = json(toRoomMessage(msg)); + break; + } + default: + break; + } + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::v2::client()->device_id(), doc.dump()); + + http::v2::client() + ->send_room_message( + room_id, + msg.txn_id, + data, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + msg.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + return; + } + + log::main()->info("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + log::main()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = std::make_shared( + [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::v2::client()->device_id(), doc.dump()); + + http::v2::client() + ->send_room_message( + room_id, + txn_id, + data, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + } catch (const lmdb::error &e) { + log::db()->critical("failed to save megolm outbound session: {}", + e.what()); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::v2::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + return; + } + + for (const auto &entry : res.device_keys) { + for (const auto &dev : entry.second) { + log::net()->info("received device {}", dev.first); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + dev.first; + const auto edKey = "ed25519:" + dev.first; + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + log::net()->info( + "ignoring malformed keys for device {}", + dev.first); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + // Validate signatures + for (const auto &algo : dev.second.keys) { + log::net()->info( + "dev keys {} {}", algo.first, algo.second); + } + + auto room_key = + olm::client() + ->create_room_key_event(UserId(dev.second.user_id), + pks.ed25519, + megolm_payload) + .dump(); + + http::v2::client()->claim_keys( + dev.second.user_id, + {dev.second.device_id}, + [keeper, + room_key, + pks, + user_id = dev.second.user_id, + device_id = dev.second.device_id]( + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "claim keys error: {}", + err->matrix_error.error); + return; + } + + log::net()->info("claimed keys for {} - {}", + user_id, + device_id); + + auto retrieved_devices = + res.one_time_keys.at(user_id); + for (const auto &rd : retrieved_devices) { + log::net()->info("{} : \n {}", + rd.first, + rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + auto id_key = pks.curve25519; + + auto session = + olm::client() + ->create_outbound_session(id_key, + otk); + + auto device_msg = + olm::client() + ->create_olm_encrypted_content( + session.get(), + room_key, + pks.curve25519); + + json body{ + {"messages", + {{user_id, + {{device_id, device_msg}}}}}}; + + http::v2::client()->send_to_device( + "m.room.encrypted", + body, + [keeper](mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to send " + "send_to_device " + "message: {}", + err->matrix_error + .error); + } + }); + } + }); + } + } + }); + + } catch (const lmdb::error &e) { + log::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + return; + } +} From 8e9d210a2a2136d1d517645b2b2412b15da09360 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Thu, 14 Jun 2018 02:28:35 +0300 Subject: [PATCH 09/18] Rename the log namespace to avoid symbol clash with the math function - Patch the olm repo with a CMakeLists.txt file --- deps/CMakeLists.txt | 2 +- deps/cmake/Olm.cmake | 26 +++--- deps/cmake/OlmCMakeLists.txt | 107 ++++++++++++++++++++++ deps/cmake/OlmConfig.cmake.in | 11 +++ include/Logging.hpp | 4 +- include/timeline/TimelineItem.h | 14 +-- src/AvatarProvider.cc | 8 +- src/Cache.cc | 86 +++++++++--------- src/ChatPage.cc | 135 ++++++++++++++-------------- src/CommunitiesList.cc | 2 +- src/Logging.cpp | 14 +-- src/MainWindow.cc | 2 +- src/Olm.cpp | 38 ++++---- src/RegisterPage.cc | 6 +- src/RoomList.cc | 12 +-- src/dialogs/PreviewUploadOverlay.cc | 9 +- src/dialogs/RoomSettings.cpp | 15 ++-- src/main.cc | 8 +- src/timeline/TimelineItem.cc | 17 ++++ src/timeline/TimelineView.cc | 88 +++++++++--------- src/timeline/TimelineViewManager.cc | 16 ++-- src/timeline/widgets/AudioItem.cc | 9 +- src/timeline/widgets/FileItem.cc | 11 +-- src/timeline/widgets/ImageItem.cc | 32 ++++--- src/timeline/widgets/VideoItem.cc | 1 - 25 files changed, 408 insertions(+), 265 deletions(-) create mode 100644 deps/cmake/OlmCMakeLists.txt create mode 100644 deps/cmake/OlmConfig.cmake.in diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 5f9b48ca..c948a097 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs) set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de) set(MTXCLIENT_URL https://github.com/mujx/mtxclient) -set(MTXCLIENT_TAG 26aad7088b9532808ded9919d55f58711c0138e3) +set(MTXCLIENT_TAG 688d5b0fd1fd16319d7fcbdbf938109eaa850545) set(OLM_URL https://git.matrix.org/git/olm.git) set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake index dde18db9..c476f71d 100644 --- a/deps/cmake/Olm.cmake +++ b/deps/cmake/Olm.cmake @@ -1,8 +1,4 @@ -if(MSVC) - set(MAKE_CMD "mingw32-make.exe") -else() - set(MAKE_CMD "make") -endif() +set(OLM_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/patches/olm-CMake-Support.patch) ExternalProject_Add( Olm @@ -12,12 +8,18 @@ ExternalProject_Add( BUILD_IN_SOURCE 1 SOURCE_DIR ${DEPS_BUILD_DIR}/olm - CONFIGURE_COMMAND "" - BUILD_COMMAND ${MAKE_CMD} static - INSTALL_COMMAND - mkdir -p ${DEPS_INSTALL_DIR}/lib && - cp -R ${DEPS_BUILD_DIR}/olm/include ${DEPS_INSTALL_DIR} && - cp ${DEPS_BUILD_DIR}/olm/build/libolm.a ${DEPS_INSTALL_DIR}/lib -) + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/OlmCMakeLists.txt + ${DEPS_BUILD_DIR}/olm/CMakeLists.txt + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/OlmConfig.cmake.in + ${DEPS_BUILD_DIR}/olm/cmake/OlmConfig.cmake.in + COMMAND ${CMAKE_COMMAND} + -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} + -DCMAKE_BUILD_TYPE=Release + ${DEPS_BUILD_DIR}/olm + BUILD_COMMAND ${CMAKE_COMMAND} + --build ${DEPS_BUILD_DIR}/olm + --config Release) list(APPEND THIRD_PARTY_DEPS Olm) diff --git a/deps/cmake/OlmCMakeLists.txt b/deps/cmake/OlmCMakeLists.txt new file mode 100644 index 00000000..529cbb92 --- /dev/null +++ b/deps/cmake/OlmCMakeLists.txt @@ -0,0 +1,107 @@ +cmake_minimum_required(VERSION 3.1) + +project(olm VERSION 2.2.2 LANGUAGES CXX C) + +add_definitions(-DOLMLIB_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}) +add_definitions(-DOLMLIB_VERSION_MINOR=${PROJECT_VERSION_MINOR}) +add_definitions(-DOLMLIB_VERSION_PATCH=${PROJECT_VERSION_PATCH}) + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +add_library(olm + src/account.cpp + src/base64.cpp + src/cipher.cpp + src/crypto.cpp + src/memory.cpp + src/message.cpp + src/pickle.cpp + src/ratchet.cpp + src/session.cpp + src/utility.cpp + + src/ed25519.c + src/error.c + src/inbound_group_session.c + src/megolm.c + src/olm.cpp + src/outbound_group_session.c + src/pickle_encoding.c + + lib/crypto-algorithms/aes.c + lib/crypto-algorithms/sha256.c + lib/curve25519-donna/curve25519-donna.c) +add_library(Olm::Olm ALIAS olm) + +target_include_directories(olm + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/lib) + +set_target_properties(olm PROPERTIES + SOVERSION ${PROJECT_VERSION_MAJOR} + VERSION ${PROJECT_VERSION}) + +set_target_properties(olm PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR} + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR} + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) + +# +# Installation +# +include(GNUInstallDirs) +set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/Olm) +install(TARGETS olm + EXPORT olm-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +# The exported target will be named Olm. +set_target_properties(olm PROPERTIES EXPORT_NAME Olm) +install(FILES + ${CMAKE_SOURCE_DIR}/include/olm/olm.h + ${CMAKE_SOURCE_DIR}/include/olm/outbound_group_session.h + ${CMAKE_SOURCE_DIR}/include/olm/inbound_group_session.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/olm) + +# Export the targets to a script. +install(EXPORT olm-targets + FILE OlmTargets.cmake + NAMESPACE Olm:: + DESTINATION ${INSTALL_CONFIGDIR}) + +# Create a ConfigVersion.cmake file. +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/OlmConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + +configure_package_config_file( + ${CMAKE_CURRENT_LIST_DIR}/cmake/OlmConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/OlmConfig.cmake + INSTALL_DESTINATION ${INSTALL_CONFIGDIR}) + +#Install the config & configversion. +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/OlmConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/OlmConfigVersion.cmake + DESTINATION ${INSTALL_CONFIGDIR}) + +# Register package in user's package registry +export(EXPORT olm-targets + FILE ${CMAKE_CURRENT_BINARY_DIR}/OlmTargets.cmake + NAMESPACE Olm::) +export(PACKAGE Olm) diff --git a/deps/cmake/OlmConfig.cmake.in b/deps/cmake/OlmConfig.cmake.in new file mode 100644 index 00000000..a7541f7a --- /dev/null +++ b/deps/cmake/OlmConfig.cmake.in @@ -0,0 +1,11 @@ +get_filename_component(Olm_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +include(CMakeFindDependencyMacro) + +list(APPEND CMAKE_MODULE_PATH ${Olm_CMAKE_DIR}) +list(REMOVE_AT CMAKE_MODULE_PATH -1) + +if(NOT TARGET Olm::olm) + include("${Olm_CMAKE_DIR}/OlmTargets.cmake") +endif() + +set(Olm_LIBRARIES Olm::olm) diff --git a/include/Logging.hpp b/include/Logging.hpp index bdbd3e2c..2feae60d 100644 --- a/include/Logging.hpp +++ b/include/Logging.hpp @@ -3,12 +3,12 @@ #include #include -namespace log { +namespace nhlog { void init(const std::string &file); std::shared_ptr -main(); +ui(); std::shared_ptr net(); diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h index 4dcca1a5..6623a82c 100644 --- a/include/timeline/TimelineItem.h +++ b/include/timeline/TimelineItem.h @@ -194,19 +194,7 @@ public: void setEventId(const QString &event_id) { event_id_ = event_id; } void markReceived(); void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const - { - if (!event_id_.isEmpty()) - http::v2::client()->read_event( - room_id_.toStdString(), - event_id_.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - qWarning() << QString("failed to read_event (%1, %2)") - .arg(room_id_, event_id_); - } - }); - } + void sendReadReceipt() const; //! Add a user avatar for this event. void addAvatar(); diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc index ad095023..391f57d9 100644 --- a/src/AvatarProvider.cc +++ b/src/AvatarProvider.cc @@ -56,10 +56,10 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata opts, [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn("failed to download avatar: {} - ({} {})", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); + nhlog::net()->warn("failed to download avatar: {} - ({} {})", + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); return; } diff --git a/src/Cache.cc b/src/Cache.cc index 7c678b72..20572ece 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -119,7 +119,7 @@ Cache::Cache(const QString &userId, QObject *parent) void Cache::setup() { - log::db()->debug("setting up cache"); + nhlog::db()->debug("setting up cache"); auto statePath = QString("%1/%2") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) @@ -136,7 +136,7 @@ Cache::setup() env_.set_max_dbs(1024UL); if (isInitial) { - log::db()->info("initializing LMDB"); + nhlog::db()->info("initializing LMDB"); if (!QDir().mkpath(statePath)) { throw std::runtime_error( @@ -152,7 +152,7 @@ Cache::setup() std::string(e.what())); } - log::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); + nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); QDir stateDir(statePath); @@ -188,7 +188,7 @@ Cache::setup() void Cache::setEncryptedRoom(const std::string &room_id) { - log::db()->info("mark room {} as encrypted", room_id); + nhlog::db()->info("mark room {} as encrypted", room_id); auto txn = lmdb::txn::begin(env_); auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); @@ -398,7 +398,7 @@ Cache::restoreSessions() unpickle(obj.at("session"), SECRET); session_storage.group_outbound_sessions[key] = std::move(session); } catch (const nlohmann::json::exception &e) { - log::db()->critical( + nhlog::db()->critical( "failed to parse outbound megolm session data: {}", e.what()); } } @@ -419,7 +419,7 @@ Cache::restoreSessions() txn.commit(); - log::db()->info("sessions restored"); + nhlog::db()->info("sessions restored"); } std::string @@ -453,7 +453,7 @@ Cache::saveImage(const std::string &url, const std::string &img_data) txn.commit(); } catch (const lmdb::error &e) { - log::db()->critical("saveImage: {}", e.what()); + nhlog::db()->critical("saveImage: {}", e.what()); } } @@ -478,7 +478,7 @@ Cache::image(lmdb::txn &txn, const std::string &url) const return QByteArray(image.data(), image.size()); } catch (const lmdb::error &e) { - log::db()->critical("image: {}, {}", e.what(), url); + nhlog::db()->critical("image: {}, {}", e.what(), url); } return QByteArray(); @@ -506,7 +506,7 @@ Cache::image(const QString &url) const return QByteArray(image.data(), image.size()); } catch (const lmdb::error &e) { - log::db()->critical("image: {} {}", e.what(), url.toStdString()); + nhlog::db()->critical("image: {} {}", e.what(), url.toStdString()); } return QByteArray(); @@ -588,7 +588,7 @@ Cache::deleteData() // TODO: We need to remove the env_ while not accepting new requests. if (!cacheDirectory_.isEmpty()) { QDir(cacheDirectory_).removeRecursively(); - log::db()->info("deleted cache files from disk"); + nhlog::db()->info("deleted cache files from disk"); } } @@ -608,9 +608,9 @@ Cache::isFormatValid() std::string stored_version(current_version.data(), current_version.size()); if (stored_version != CURRENT_CACHE_FORMAT_VERSION) { - log::db()->warn("breaking changes in the cache format. stored: {}, current: {}", - stored_version, - CURRENT_CACHE_FORMAT_VERSION); + nhlog::db()->warn("breaking changes in the cache format. stored: {}, current: {}", + stored_version, + CURRENT_CACHE_FORMAT_VERSION); return false; } @@ -660,7 +660,7 @@ Cache::readReceipts(const QString &event_id, const QString &room_id) } } catch (const lmdb::error &e) { - log::db()->critical("readReceipts: {}", e.what()); + nhlog::db()->critical("readReceipts: {}", e.what()); } return receipts; @@ -710,7 +710,7 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei lmdb::val(merged_receipts.data(), merged_receipts.size())); } catch (const lmdb::error &e) { - log::db()->critical("updateReadReceipts: {}", e.what()); + nhlog::db()->critical("updateReadReceipts: {}", e.what()); } } } @@ -868,9 +868,9 @@ Cache::singleRoomInfo(const std::string &room_id) return tmp; } catch (const json::exception &e) { - log::db()->warn("failed to parse room info: room_id ({}), {}", - room_id, - std::string(data.data(), data.size())); + nhlog::db()->warn("failed to parse room info: room_id ({}), {}", + room_id, + std::string(data.data(), data.size())); } } @@ -900,9 +900,9 @@ Cache::getRoomInfo(const std::vector &rooms) room_info.emplace(QString::fromStdString(room), std::move(tmp)); } catch (const json::exception &e) { - log::db()->warn("failed to parse room info: room_id ({}), {}", - room, - std::string(data.data(), data.size())); + nhlog::db()->warn("failed to parse room info: room_id ({}), {}", + room, + std::string(data.data(), data.size())); } } else { // Check if the room is an invite. @@ -915,7 +915,7 @@ Cache::getRoomInfo(const std::vector &rooms) room_info.emplace(QString::fromStdString(room), std::move(tmp)); } catch (const json::exception &e) { - log::db()->warn( + nhlog::db()->warn( "failed to parse room info for invite: room_id ({}), {}", room, std::string(data.data(), data.size())); @@ -1003,7 +1003,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn, return QString::fromStdString(msg.content.url); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.avatar event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } } @@ -1026,7 +1026,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn, cursor.close(); return QString::fromStdString(m.avatar_url); } catch (const json::exception &e) { - log::db()->warn("failed to parse member info: {}", e.what()); + nhlog::db()->warn("failed to parse member info: {}", e.what()); } } @@ -1053,7 +1053,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) if (!msg.content.name.empty()) return QString::fromStdString(msg.content.name); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.name event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); } } @@ -1068,8 +1068,8 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) if (!msg.content.alias.empty()) return QString::fromStdString(msg.content.alias); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.canonical_alias event: {}", - e.what()); + nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", + e.what()); } } @@ -1085,7 +1085,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) try { members.emplace(user_id, json::parse(member_data)); } catch (const json::exception &e) { - log::db()->warn("failed to parse member info: {}", e.what()); + nhlog::db()->warn("failed to parse member info: {}", e.what()); } ii++; @@ -1129,7 +1129,7 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) json::parse(std::string(event.data(), event.size())); return msg.content.join_rule; } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); } } return JoinRule::Knock; @@ -1151,7 +1151,8 @@ Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) json::parse(std::string(event.data(), event.size())); return msg.content.guest_access == AccessState::CanJoin; } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.guest_access event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.guest_access event: {}", + e.what()); } } return false; @@ -1175,7 +1176,7 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) if (!msg.content.topic.empty()) return QString::fromStdString(msg.content.topic); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.topic event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } } @@ -1198,7 +1199,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members json::parse(std::string(event.data(), event.size())); return QString::fromStdString(msg.content.name); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.name event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); } } @@ -1215,7 +1216,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members return QString::fromStdString(tmp.name); } catch (const json::exception &e) { - log::db()->warn("failed to parse member info: {}", e.what()); + nhlog::db()->warn("failed to parse member info: {}", e.what()); } } @@ -1240,7 +1241,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me json::parse(std::string(event.data(), event.size())); return QString::fromStdString(msg.content.url); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.avatar event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } } @@ -1257,7 +1258,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me return QString::fromStdString(tmp.avatar_url); } catch (const json::exception &e) { - log::db()->warn("failed to parse member info: {}", e.what()); + nhlog::db()->warn("failed to parse member info: {}", e.what()); } } @@ -1282,7 +1283,7 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) json::parse(std::string(event.data(), event.size())); return QString::fromStdString(msg.content.topic); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.topic event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } } @@ -1318,9 +1319,9 @@ Cache::getRoomAvatar(const std::string &room_id) return QImage(); } } catch (const json::exception &e) { - log::db()->warn("failed to parse room info: {}, {}", - e.what(), - std::string(response.data(), response.size())); + nhlog::db()->warn("failed to parse room info: {}, {}", + e.what(), + std::string(response.data(), response.size())); } if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) { @@ -1356,7 +1357,7 @@ void Cache::populateMembers() { auto rooms = joinedRooms(); - log::db()->info("loading {} rooms", rooms.size()); + nhlog::db()->info("loading {} rooms", rooms.size()); auto txn = lmdb::txn::begin(env_); @@ -1484,7 +1485,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_ QString::fromStdString(tmp.name), QImage::fromData(image(txn, tmp.avatar_url))}); } catch (const json::exception &e) { - log::db()->warn("{}", e.what()); + nhlog::db()->warn("{}", e.what()); } currentIndex += 1; @@ -1555,7 +1556,8 @@ Cache::hasEnoughPowerLevel(const std::vector &eventTypes std::min(min_event_level, (uint16_t)msg.content.state_level(to_string(ty))); } catch (const json::exception &e) { - log::db()->warn("failed to parse m.room.power_levels event: {}", e.what()); + nhlog::db()->warn("failed to parse m.room.power_levels event: {}", + e.what()); } } diff --git a/src/ChatPage.cc b/src/ChatPage.cc index c10d4aa2..cffb2a46 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -129,13 +129,13 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); connect(this, &ChatPage::connectionLost, this, [this]() { - log::net()->info("connectivity lost"); + nhlog::net()->info("connectivity lost"); isConnected_ = false; http::v2::client()->shutdown(); text_input_->disableInput(); }); connect(this, &ChatPage::connectionRestored, this, [this]() { - log::net()->info("trying to re-connect"); + nhlog::net()->info("trying to re-connect"); text_input_->enableInput(); isConnected_ = true; @@ -165,19 +165,20 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); connect(user_info_widget_, &UserInfoWidget::logout, this, [this]() { - http::v2::client()->logout([this](const mtx::responses::Logout &, - mtx::http::RequestErr err) { - if (err) { - // TODO: handle special errors - emit contentLoaded(); - log::net()->warn("failed to logout: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } + http::v2::client()->logout( + [this](const mtx::responses::Logout &, mtx::http::RequestErr err) { + if (err) { + // TODO: handle special errors + emit contentLoaded(); + nhlog::net()->warn( + "failed to logout: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } - emit loggedOut(); - }); + emit loggedOut(); + }); emit showOverlayProgressBar(); }); @@ -252,8 +253,8 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) http::v2::client()->stop_typing( current_room_.toStdString(), [](mtx::http::RequestErr err) { if (err) { - log::net()->warn("failed to stop typing notifications: {}", - err->matrix_error.error); + nhlog::net()->warn("failed to stop typing notifications: {}", + err->matrix_error.error); } }); }); @@ -309,9 +310,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) if (err) { emit uploadFailed( tr("Failed to upload image. Please try again.")); - log::net()->warn("failed to upload image: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); + nhlog::net()->warn("failed to upload image: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); return; } @@ -352,9 +353,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) if (err) { emit uploadFailed( tr("Failed to upload file. Please try again.")); - log::net()->warn("failed to upload file: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); + nhlog::net()->warn("failed to upload file: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); return; } @@ -395,9 +396,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) if (err) { emit uploadFailed( tr("Failed to upload audio. Please try again.")); - log::net()->warn("failed to upload audio: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); + nhlog::net()->warn("failed to upload audio: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); return; } @@ -437,9 +438,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) if (err) { emit uploadFailed( tr("Failed to upload video. Please try again.")); - log::net()->warn("failed to upload video: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); + nhlog::net()->warn("failed to upload video: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); return; } @@ -569,7 +570,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) try { room_list_->cleanupInvites(cache::client()->invites()); } catch (const lmdb::error &e) { - log::db()->error("failed to retrieve invites: {}", e.what()); + nhlog::db()->error("failed to retrieve invites: {}", e.what()); } view_manager_->initialize(rooms); @@ -593,7 +594,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn( + nhlog::net()->warn( "failed to retrieve notifications: {} ({})", err->matrix_error.error, static_cast(err->status_code)); @@ -690,7 +691,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) try { http::v2::client()->set_user(parse(userid.toStdString())); } catch (const std::invalid_argument &e) { - log::main()->critical("bootstrapped with invalid user_id: {}", + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString()); } @@ -709,7 +710,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) const bool isValid = cache::client()->isFormatValid(); if (isInitialized && !isValid) { - log::db()->warn("breaking changes in cache"); + nhlog::db()->warn("breaking changes in cache"); // TODO: Deleting session data but keep using the // same device doesn't work. cache::client()->deleteData(); @@ -721,23 +722,23 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) return; } } catch (const lmdb::error &e) { - log::db()->critical("failure during boot: {}", e.what()); + nhlog::db()->critical("failure during boot: {}", e.what()); cache::client()->deleteData(); - log::net()->info("falling back to initial sync"); + nhlog::net()->info("falling back to initial sync"); } try { // It's the first time syncing with this device // There isn't a saved olm account to restore. - log::crypto()->info("creating new olm account"); + nhlog::crypto()->info("creating new olm account"); olm::client()->create_new_account(); cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); } catch (const lmdb::error &e) { - log::crypto()->critical("failed to save olm account {}", e.what()); + nhlog::crypto()->critical("failed to save olm account {}", e.what()); emit dropToLoginPageCb(QString::fromStdString(e.what())); return; } catch (const mtx::crypto::olm_exception &e) { - log::crypto()->critical("failed to create new olm account {}", e.what()); + nhlog::crypto()->critical("failed to create new olm account {}", e.what()); emit dropToLoginPageCb(QString::fromStdString(e.what())); return; } @@ -771,7 +772,7 @@ void ChatPage::changeTopRoomInfo(const QString &room_id) { if (room_id.isEmpty()) { - log::main()->warn("cannot switch to empty room_id"); + nhlog::ui()->warn("cannot switch to empty room_id"); return; } @@ -795,7 +796,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) top_bar_->updateRoomAvatar(img); } catch (const lmdb::error &e) { - log::main()->error("failed to change top bar room info: {}", e.what()); + nhlog::ui()->error("failed to change top bar room info: {}", e.what()); } current_room_ = room_id; @@ -816,7 +817,7 @@ ChatPage::showUnreadMessageNotification(int count) void ChatPage::loadStateFromCache() { - log::db()->info("restoring state from cache"); + nhlog::db()->info("restoring state from cache"); getProfileInfo(); @@ -831,19 +832,19 @@ ChatPage::loadStateFromCache() emit initializeEmptyViews(cache::client()->joinedRooms()); emit initializeRoomList(cache::client()->roomInfo()); } catch (const mtx::crypto::olm_exception &e) { - log::crypto()->critical("failed to restore olm account: {}", e.what()); + nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); emit dropToLoginPageCb( tr("Failed to restore OLM account. Please login again.")); return; } catch (const lmdb::error &e) { - log::db()->critical("failed to restore cache: {}", e.what()); + nhlog::db()->critical("failed to restore cache: {}", e.what()); emit dropToLoginPageCb( tr("Failed to restore save data. Please login again.")); return; } - log::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); - log::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); // Start receiving events. emit trySyncCb(); @@ -890,7 +891,7 @@ ChatPage::removeRoom(const QString &room_id) cache::client()->removeRoom(room_id); cache::client()->removeInvite(room_id.toStdString()); } catch (const lmdb::error &e) { - log::db()->critical("failure while removing room: {}", e.what()); + nhlog::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. } @@ -1009,7 +1010,7 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) utils::event_body(item.event)); } } catch (const lmdb::error &e) { - log::db()->warn("error while sending desktop notification: {}", e.what()); + nhlog::db()->warn("error while sending desktop notification: {}", e.what()); } } } @@ -1017,11 +1018,11 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) void ChatPage::tryInitialSync() { - log::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); - log::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); // Upload one time keys for the device. - log::crypto()->info("generating one time keys"); + nhlog::crypto()->info("generating one time keys"); olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS); http::v2::client()->upload_keys( @@ -1029,9 +1030,9 @@ ChatPage::tryInitialSync() [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { if (err) { const int status_code = static_cast(err->status_code); - log::crypto()->critical("failed to upload one time keys: {} {}", - err->matrix_error.error, - status_code); + nhlog::crypto()->critical("failed to upload one time keys: {} {}", + err->matrix_error.error, + status_code); // TODO We should have a timeout instead of keeping hammering the server. emit tryInitialSyncCb(); return; @@ -1039,10 +1040,10 @@ ChatPage::tryInitialSync() olm::client()->mark_keys_as_published(); for (const auto &entry : res.one_time_key_counts) - log::net()->info( + nhlog::net()->info( "uploaded {} {} one-time keys", entry.second, entry.first); - log::net()->info("trying initial sync"); + nhlog::net()->info("trying initial sync"); mtx::http::SyncOpts opts; opts.timeout = 0; @@ -1065,7 +1066,7 @@ ChatPage::trySync() try { opts.since = cache::client()->nextBatchToken(); } catch (const lmdb::error &e) { - log::db()->error("failed to retrieve next batch token: {}", e.what()); + nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); return; } @@ -1077,7 +1078,7 @@ ChatPage::trySync() const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); const int status_code = static_cast(err->status_code); - log::net()->error("sync error: {} {}", status_code, err_code); + nhlog::net()->error("sync error: {} {}", status_code, err_code); if (status_code <= 0 || status_code >= 600) { if (!http::v2::is_logged_in()) @@ -1109,7 +1110,7 @@ ChatPage::trySync() } } - log::net()->debug("sync completed: {}", res.next_batch); + nhlog::net()->debug("sync completed: {}", res.next_batch); // Ensure that we have enough one-time keys available. ensureOneTimeKeyCount(res.device_one_time_keys_count); @@ -1126,7 +1127,7 @@ ChatPage::trySync() emit syncTopBar(updates); emit syncRoomlist(updates); } catch (const lmdb::error &e) { - log::db()->error("saving sync response: {}", e.what()); + nhlog::db()->error("saving sync response: {}", e.what()); } emit trySyncCb(); @@ -1201,8 +1202,8 @@ ChatPage::sendTypingNotifications() http::v2::client()->start_typing( current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) { if (err) { - log::net()->warn("failed to send typing notification: {}", - err->matrix_error.error); + nhlog::net()->warn("failed to send typing notification: {}", + err->matrix_error.error); } }); } @@ -1216,7 +1217,7 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); const int status_code = static_cast(err->status_code); - log::net()->error("sync error: {} {}", status_code, err_code); + nhlog::net()->error("sync error: {} {}", status_code, err_code); switch (status_code) { case 502: @@ -1232,7 +1233,7 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request } } - log::net()->info("initial sync completed"); + nhlog::net()->info("initial sync completed"); try { cache::client()->saveState(res); @@ -1242,7 +1243,7 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request emit initializeViews(std::move(res.rooms)); emit initializeRoomList(cache::client()->roomInfo()); } catch (const lmdb::error &e) { - log::db()->error("{}", e.what()); + nhlog::db()->error("{}", e.what()); emit tryInitialSyncCb(); return; } @@ -1258,14 +1259,14 @@ ChatPage::ensureOneTimeKeyCount(const std::map &counts) if (entry.second < MAX_ONETIME_KEYS) { const int nkeys = MAX_ONETIME_KEYS - entry.second; - log::crypto()->info("uploading {} {} keys", nkeys, entry.first); + nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first); olm::client()->generate_one_time_keys(nkeys); http::v2::client()->upload_keys( olm::client()->create_upload_keys_request(), [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { if (err) { - log::crypto()->warn( + nhlog::crypto()->warn( "failed to update one-time keys: {} {}", err->matrix_error.error, static_cast(err->status_code)); @@ -1287,7 +1288,7 @@ ChatPage::getProfileInfo() http::v2::client()->get_profile( userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn("failed to retrieve own profile info"); + nhlog::net()->warn("failed to retrieve own profile info"); return; } @@ -1311,7 +1312,7 @@ ChatPage::getProfileInfo() const std::string &, mtx::http::RequestErr err) { if (err) { - log::net()->warn( + nhlog::net()->warn( "failed to download user avatar: {} - {}", mtx::errors::to_string(err->matrix_error.errcode), err->matrix_error.error); diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc index 49affcb7..df4d6361 100644 --- a/src/CommunitiesList.cc +++ b/src/CommunitiesList.cc @@ -136,7 +136,7 @@ CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUr http::v2::client()->get_thumbnail( opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn("failed to download avatar: {} - ({} {})", + nhlog::net()->warn("failed to download avatar: {} - ({} {})", opts.mxc_url, mtx::errors::to_string(err->matrix_error.errcode), err->matrix_error.error); diff --git a/src/Logging.cpp b/src/Logging.cpp index 77e61e09..bccbe389 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -7,13 +7,13 @@ namespace { std::shared_ptr db_logger = nullptr; std::shared_ptr net_logger = nullptr; std::shared_ptr crypto_logger = nullptr; -std::shared_ptr main_logger = nullptr; +std::shared_ptr ui_logger = nullptr; constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6; constexpr auto MAX_LOG_FILES = 3; } -namespace log { +namespace nhlog { void init(const std::string &file_path) { @@ -26,17 +26,17 @@ init(const std::string &file_path) sinks.push_back(file_sink); sinks.push_back(console_sink); - net_logger = std::make_shared("net", std::begin(sinks), std::end(sinks)); - main_logger = std::make_shared("main", std::begin(sinks), std::end(sinks)); - db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); + net_logger = std::make_shared("net", std::begin(sinks), std::end(sinks)); + ui_logger = std::make_shared("ui", std::begin(sinks), std::end(sinks)); + db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); crypto_logger = std::make_shared("crypto", std::begin(sinks), std::end(sinks)); } std::shared_ptr -main() +ui() { - return main_logger; + return ui_logger; } std::shared_ptr diff --git a/src/MainWindow.cc b/src/MainWindow.cc index cca51f03..088bb5c0 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc @@ -155,7 +155,7 @@ MainWindow::MainWindow(QWidget *parent) using namespace mtx::identifiers; http::v2::client()->set_user(parse(user_id.toStdString())); } catch (const std::invalid_argument &e) { - log::main()->critical("bootstrapped with invalid user_id: {}", + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", user_id.toStdString()); } diff --git a/src/Olm.cpp b/src/Olm.cpp index 6e130277..f39554f0 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -23,17 +23,17 @@ handle_to_device_messages(const std::vector &msgs) if (msgs.empty()) return; - log::crypto()->info("received {} to_device messages", msgs.size()); + nhlog::crypto()->info("received {} to_device messages", msgs.size()); for (const auto &msg : msgs) { try { OlmMessage olm_msg = msg; handle_olm_message(std::move(olm_msg)); } catch (const nlohmann::json::exception &e) { - log::crypto()->warn( + nhlog::crypto()->warn( "parsing error for olm message: {} {}", e.what(), msg.dump(2)); } catch (const std::invalid_argument &e) { - log::crypto()->warn( + nhlog::crypto()->warn( "validation error for olm message: {} {}", e.what(), msg.dump(2)); } } @@ -42,8 +42,8 @@ handle_to_device_messages(const std::vector &msgs) void handle_olm_message(const OlmMessage &msg) { - log::crypto()->info("sender : {}", msg.sender); - log::crypto()->info("sender_key: {}", msg.sender_key); + nhlog::crypto()->info("sender : {}", msg.sender); + nhlog::crypto()->info("sender_key: {}", msg.sender_key); const auto my_key = olm::client()->identity_keys().curve25519; @@ -53,7 +53,7 @@ handle_olm_message(const OlmMessage &msg) continue; const auto type = cipher.second.type; - log::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); + nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); if (type == OLM_MESSAGE_TYPE_PRE_KEY) handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second); @@ -67,19 +67,20 @@ handle_pre_key_olm_message(const std::string &sender, const std::string &sender_key, const OlmCipherContent &content) { - log::crypto()->info("opening olm session with {}", sender); + nhlog::crypto()->info("opening olm session with {}", sender); OlmSessionPtr inbound_session = nullptr; try { inbound_session = olm::client()->create_inbound_session(content.body); } catch (const olm_exception &e) { - log::crypto()->critical( + nhlog::crypto()->critical( "failed to create inbound session with {}: {}", sender, e.what()); return; } if (!matches_inbound_session_from(inbound_session.get(), sender_key, content.body)) { - log::crypto()->warn("inbound olm session doesn't match sender's key ({})", sender); + nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})", + sender); return; } @@ -88,13 +89,13 @@ handle_pre_key_olm_message(const std::string &sender, output = olm::client()->decrypt_message( inbound_session.get(), OLM_MESSAGE_TYPE_PRE_KEY, content.body); } catch (const olm_exception &e) { - log::crypto()->critical( + nhlog::crypto()->critical( "failed to decrypt olm message {}: {}", content.body, e.what()); return; } auto plaintext = json::parse(std::string((char *)output.data(), output.size())); - log::crypto()->info("decrypted message: \n {}", plaintext.dump(2)); + nhlog::crypto()->info("decrypted message: \n {}", plaintext.dump(2)); std::string room_id, session_id, session_key; try { @@ -102,7 +103,7 @@ handle_pre_key_olm_message(const std::string &sender, session_id = plaintext.at("content").at("session_id"); session_key = plaintext.at("content").at("session_key"); } catch (const nlohmann::json::exception &e) { - log::crypto()->critical( + nhlog::crypto()->critical( "failed to parse plaintext olm message: {} {}", e.what(), plaintext.dump(2)); return; } @@ -118,14 +119,15 @@ handle_pre_key_olm_message(const std::string &sender, try { cache::client()->saveInboundMegolmSession(index, std::move(megolm_session)); } catch (const lmdb::error &e) { - log::crypto()->critical("failed to save inbound megolm session: {}", - e.what()); + nhlog::crypto()->critical("failed to save inbound megolm session: {}", + e.what()); return; } - log::crypto()->info("established inbound megolm session ({}, {})", room_id, sender); + nhlog::crypto()->info( + "established inbound megolm session ({}, {})", room_id, sender); } else { - log::crypto()->warn( + nhlog::crypto()->warn( "inbound megolm session already exists ({}, {})", room_id, sender); } } @@ -133,7 +135,7 @@ handle_pre_key_olm_message(const std::string &sender, void handle_olm_normal_message(const std::string &, const std::string &, const OlmCipherContent &) { - log::crypto()->warn("olm(1) not implemeted yet"); + nhlog::crypto()->warn("olm(1) not implemeted yet"); } mtx::events::msg::Encrypted @@ -155,7 +157,7 @@ encrypt_group_message(const std::string &room_id, data.device_id = device_id; auto message_index = olm_outbound_group_session_message_index(res.session); - log::crypto()->info("next message_index {}", message_index); + nhlog::crypto()->info("next message_index {}", message_index); // We need to re-pickle the session after we send a message to save the new message_index. cache::client()->updateOutboundMegolmSession(room_id, message_index); diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc index a5960b83..f56cd663 100644 --- a/src/RegisterPage.cc +++ b/src/RegisterPage.cc @@ -153,7 +153,7 @@ RegisterPage::RegisterPage(QWidget *parent) [this](const mtx::responses::Register &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn( + nhlog::net()->warn( "failed to retrieve registration flows: {}", err->matrix_error.error); emit errorOccurred(); @@ -231,7 +231,7 @@ RegisterPage::onRegisterButtonClicked() const mtx::responses::RegistrationFlows &res, mtx::http::RequestErr err) { if (res.session.empty() && err) { - log::net()->warn( + nhlog::net()->warn( "failed to retrieve registration flows: ({}) " "{}", static_cast(err->status_code), @@ -247,7 +247,7 @@ RegisterPage::onRegisterButtonClicked() return; } - log::net()->warn("failed to register: status_code ({})", + nhlog::net()->warn("failed to register: status_code ({})", static_cast(err->status_code)); emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); diff --git a/src/RoomList.cc b/src/RoomList.cc index 4891f746..b5bcdad6 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc @@ -95,7 +95,7 @@ RoomList::updateAvatar(const QString &room_id, const QString &url) http::v2::client()->get_thumbnail( opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn( + nhlog::net()->warn( "failed to download room avatar: {} {} {}", opts.mxc_url, mtx::errors::to_string(err->matrix_error.errcode), @@ -141,7 +141,7 @@ void RoomList::updateUnreadMessageCount(const QString &roomid, int count) { if (!roomExists(roomid)) { - log::main()->warn("updateUnreadMessageCount: unknown room_id {}", + nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}", roomid.toStdString()); return; } @@ -167,7 +167,7 @@ RoomList::calculateUnreadMessageCount() void RoomList::initialize(const QMap &info) { - log::main()->info("initialize room list"); + nhlog::ui()->info("initialize room list"); rooms_.clear(); @@ -220,7 +220,7 @@ RoomList::highlightSelectedRoom(const QString &room_id) emit roomChanged(room_id); if (!roomExists(room_id)) { - log::main()->warn("roomlist: clicked unknown room_id"); + nhlog::ui()->warn("roomlist: clicked unknown room_id"); return; } @@ -243,7 +243,7 @@ void RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) { if (!roomExists(roomid)) { - log::main()->warn("avatar update on non-existent room_id: {}", + nhlog::ui()->warn("avatar update on non-existent room_id: {}", roomid.toStdString()); return; } @@ -258,7 +258,7 @@ void RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info) { if (!roomExists(roomid)) { - log::main()->warn("description update on non-existent room_id: {}, {}", + nhlog::ui()->warn("description update on non-existent room_id: {}, {}", roomid.toStdString(), info.body.toStdString()); return; diff --git a/src/dialogs/PreviewUploadOverlay.cc b/src/dialogs/PreviewUploadOverlay.cc index 3c44e911..db1e31f2 100644 --- a/src/dialogs/PreviewUploadOverlay.cc +++ b/src/dialogs/PreviewUploadOverlay.cc @@ -17,7 +17,6 @@ #include #include -#include #include #include #include @@ -25,6 +24,7 @@ #include #include "Config.h" +#include "Logging.hpp" #include "Utils.h" #include "dialogs/PreviewUploadOverlay.h" @@ -142,8 +142,9 @@ PreviewUploadOverlay::setPreview(const QString &path) QFile file{path}; if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file from:" << path; - qWarning() << "Reason:" << file.errorString(); + nhlog::ui()->warn("Failed to open file ({}): {}", + path.toStdString(), + file.errorString().toStdString()); close(); return; } @@ -152,7 +153,7 @@ PreviewUploadOverlay::setPreview(const QString &path) auto mime = db.mimeTypeForFileNameAndData(path, &file); if ((data_ = file.readAll()).isEmpty()) { - qWarning() << "Failed to read media:" << file.errorString(); + nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString()); close(); return; } diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index a091c8bc..74d08478 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -338,7 +338,7 @@ RoomSettings::setupEditButton() hasEditRights_ = cache::client()->hasEnoughPowerLevel( {EventType::RoomName, EventType::RoomTopic}, room_id_.toStdString(), userId); } catch (const lmdb::error &e) { - qWarning() << "lmdb error" << e.what(); + nhlog::db()->warn("lmdb error: {}", e.what()); } constexpr int buttonSize = 36; @@ -379,7 +379,8 @@ RoomSettings::retrieveRoomInfo() info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url))); } catch (const lmdb::error &e) { - qWarning() << "failed to retrieve room info from cache" << room_id_; + nhlog::db()->warn("failed to retrieve room info from cache: {}", + room_id_.toStdString()); } } @@ -415,17 +416,17 @@ RoomSettings::enableEncryption() room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) { if (err) { int status_code = static_cast(err->status_code); - log::net()->warn("failed to enable encryption in room ({}): {} {}", - room_id, - err->matrix_error.error, - status_code); + nhlog::net()->warn("failed to enable encryption in room ({}): {} {}", + room_id, + err->matrix_error.error, + status_code); emit enableEncryptionError( tr("Failed to enable encryption: %1") .arg(QString::fromStdString(err->matrix_error.error))); return; } - log::net()->info("enabled encryption on room ({})", room_id); + nhlog::net()->info("enabled encryption on room ({})", room_id); }); } diff --git a/src/main.cc b/src/main.cc index 0a127962..24524c2c 100644 --- a/src/main.cc +++ b/src/main.cc @@ -127,9 +127,9 @@ main(int argc, char *argv[]) createCacheDirectory(); try { - log::init(QString("%1/nheko.log") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .toStdString()); + nhlog::init(QString("%1/nheko.log") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .toStdString()); } catch (const spdlog::spdlog_ex &ex) { std::cout << "Log initialization failed: " << ex.what() << std::endl; std::exit(1); @@ -171,7 +171,7 @@ main(int argc, char *argv[]) } }); - log::main()->info("starting nheko {}", nheko::version); + nhlog::ui()->info("starting nheko {}", nheko::version); return app.exec(); } diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc index 83a0aaed..3505d347 100644 --- a/src/timeline/TimelineItem.cc +++ b/src/timeline/TimelineItem.cc @@ -23,6 +23,7 @@ #include "Avatar.h" #include "ChatPage.h" #include "Config.h" +#include "Logging.hpp" #include "timeline/TimelineItem.h" #include "timeline/widgets/AudioItem.h" @@ -653,3 +654,19 @@ TimelineItem::addAvatar() AvatarProvider::resolve( room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); } + +void +TimelineItem::sendReadReceipt() const +{ + if (!event_id_.isEmpty()) + http::v2::client()->read_event(room_id_.toStdString(), + event_id_.toStdString(), + [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to read_event ({}, {})", + room_id_.toStdString(), + event_id_.toStdString()); + } + }); +} diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 9276a7bc..8f3ad1a7 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -301,8 +301,8 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents & try { cache::client()->setEncryptedRoom(room_id_.toStdString()); } catch (const lmdb::error &e) { - log::db()->critical("failed to save room {} as encrypted", - room_id_.toStdString()); + nhlog::db()->critical("failed to save room {} as encrypted", + room_id_.toStdString()); } } @@ -324,10 +324,10 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEventinboundMegolmSessionExists(index)) { - log::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); // TODO: request megolm session_id & session_key from the sender. return dummy; } @@ -344,7 +344,7 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEventinfo("decrypted data: \n {}", body.dump(2)); + nhlog::crypto()->info("decrypted data: \n {}", body.dump(2)); json event_array = json::array(); event_array.push_back(body); @@ -523,10 +523,10 @@ TimelineView::getMessages() http::v2::client()->messages( opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { if (err) { - log::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); return; } @@ -607,7 +607,7 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction) void TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) { - log::main()->info("[{}] message was received by the server", txn_id); + nhlog::ui()->info("[{}] message was received by the server", txn_id); if (!pending_msgs_.isEmpty() && pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet auto msg = pending_msgs_.dequeue(); @@ -642,7 +642,7 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) try { message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); } catch (const lmdb::error &e) { - log::db()->critical("failed to check encryption status of room {}", e.what()); + nhlog::db()->critical("failed to check encryption status of room {}", e.what()); view_item->deleteLater(); // TODO: Send a notification to the user. @@ -676,11 +676,11 @@ TimelineView::sendNextPendingMessage() PendingMessage &m = pending_msgs_.head(); - log::main()->info("[{}] sending next queued message", m.txn_id); + nhlog::ui()->info("[{}] sending next queued message", m.txn_id); if (m.is_encrypted) { prepareEncryptedMessage(std::move(m)); - log::main()->info("[{}] sending encrypted event", m.txn_id); + nhlog::ui()->info("[{}] sending encrypted event", m.txn_id); return; } @@ -763,7 +763,7 @@ TimelineView::sendNextPendingMessage() break; } default: - log::main()->warn("cannot send unknown message type: {}", m.body.toStdString()); + nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); break; } } @@ -777,7 +777,7 @@ TimelineView::notifyForLastEvent() if (lastTimelineItem) emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); else - log::main()->warn("cast to TimelineView failed: {}", room_id_.toStdString()); + nhlog::ui()->warn("cast to TimelineView failed: {}", room_id_.toStdString()); } void @@ -817,7 +817,7 @@ TimelineView::removePendingMessage(const std::string &txn_id) if (pending_sent_msgs_.isEmpty()) sendNextPendingMessage(); - log::main()->info("[{}] removed message with sync", txn_id); + nhlog::ui()->info("[{}] removed message with sync", txn_id); } } for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { @@ -825,7 +825,7 @@ TimelineView::removePendingMessage(const std::string &txn_id) int index = std::distance(pending_msgs_.begin(), it); pending_msgs_.removeAt(index); - log::main()->info("[{}] removed message before sync", txn_id); + nhlog::ui()->info("[{}] removed message before sync", txn_id); return; } } @@ -861,7 +861,7 @@ TimelineView::readLastEvent() const eventId.toStdString(), [this, eventId](mtx::http::RequestErr err) { if (err) { - log::net()->warn( + nhlog::net()->warn( "failed to read event ({}, {})", room_id_.toStdString(), eventId.toStdString()); @@ -936,7 +936,7 @@ void TimelineView::removeEvent(const QString &event_id) { if (!eventIds_.contains(event_id)) { - log::main()->warn("cannot remove widget with unknown event_id: {}", + nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", event_id.toStdString()); return; } @@ -1062,10 +1062,10 @@ TimelineView::sendRoomMessageHandler(const std::string &txn_id, { if (err) { const int status_code = static_cast(err->status_code); - log::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); emit messageFailed(txn_id); return; } @@ -1200,7 +1200,7 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) return; } - log::main()->info("creating new outbound megolm session"); + nhlog::ui()->info("creating new outbound megolm session"); // Create a new outbound megolm session. auto outbound_session = olm::client()->init_outbound_group_session(); @@ -1223,7 +1223,7 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) room_id, session_data, std::move(outbound_session)); const auto members = cache::client()->roomMembers(room_id); - log::main()->info("retrieved {} members for {}", members.size(), room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); auto keeper = std::make_shared( [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { @@ -1243,8 +1243,8 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) std::placeholders::_2)); } catch (const lmdb::error &e) { - log::db()->critical("failed to save megolm outbound session: {}", - e.what()); + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); } }); @@ -1257,16 +1257,16 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) [keeper = std::move(keeper), megolm_payload](const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast(err->status_code)); + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); // TODO: Mark the event as failed. Communicate with the UI. return; } for (const auto &entry : res.device_keys) { for (const auto &dev : entry.second) { - log::net()->info("received device {}", dev.first); + nhlog::net()->info("received device {}", dev.first); const auto device_keys = dev.second.keys; const auto curveKey = "curve25519:" + dev.first; @@ -1274,7 +1274,7 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) if ((device_keys.find(curveKey) == device_keys.end()) || (device_keys.find(edKey) == device_keys.end())) { - log::net()->info( + nhlog::net()->info( "ignoring malformed keys for device {}", dev.first); continue; @@ -1286,7 +1286,7 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) // Validate signatures for (const auto &algo : dev.second.keys) { - log::net()->info( + nhlog::net()->info( "dev keys {} {}", algo.first, algo.second); } @@ -1308,22 +1308,22 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) const mtx::responses::ClaimKeys &res, mtx::http::RequestErr err) { if (err) { - log::net()->warn( + nhlog::net()->warn( "claim keys error: {}", err->matrix_error.error); return; } - log::net()->info("claimed keys for {} - {}", - user_id, - device_id); + nhlog::net()->info("claimed keys for {} - {}", + user_id, + device_id); auto retrieved_devices = res.one_time_keys.at(user_id); for (const auto &rd : retrieved_devices) { - log::net()->info("{} : \n {}", - rd.first, - rd.second.dump(2)); + nhlog::net()->info("{} : \n {}", + rd.first, + rd.second.dump(2)); // TODO: Verify signatures auto otk = rd.second.begin()->at("key"); @@ -1351,7 +1351,7 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) body, [keeper](mtx::http::RequestErr err) { if (err) { - log::net()->warn( + nhlog::net()->warn( "failed to send " "send_to_device " "message: {}", @@ -1366,7 +1366,7 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) }); } catch (const lmdb::error &e) { - log::db()->critical( + nhlog::db()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); return; } diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc index b6e7d50d..7ea1ee4a 100644 --- a/src/timeline/TimelineViewManager.cc +++ b/src/timeline/TimelineViewManager.cc @@ -18,10 +18,10 @@ #include #include -#include #include #include +#include "Logging.hpp" #include "timeline/TimelineView.h" #include "timeline/TimelineViewManager.h" #include "timeline/widgets/AudioItem.h" @@ -76,7 +76,7 @@ TimelineViewManager::queueImageMessage(const QString &roomid, uint64_t size) { if (!timelineViewExists(roomid)) { - qDebug() << "Cannot send m.image message to a non-managed view"; + nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); return; } @@ -93,7 +93,7 @@ TimelineViewManager::queueFileMessage(const QString &roomid, uint64_t size) { if (!timelineViewExists(roomid)) { - qDebug() << "Cannot send m.file message to a non-managed view"; + nhlog::ui()->warn("cannot send m.file message to a non-managed view"); return; } @@ -110,7 +110,7 @@ TimelineViewManager::queueAudioMessage(const QString &roomid, uint64_t size) { if (!timelineViewExists(roomid)) { - qDebug() << "Cannot send m.audio message to a non-managed view"; + nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); return; } @@ -127,7 +127,7 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, uint64_t size) { if (!timelineViewExists(roomid)) { - qDebug() << "Cannot send m.video message to a non-managed view"; + nhlog::ui()->warn("cannot send m.video message to a non-managed view"); return; } @@ -198,7 +198,8 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms) auto roomid = QString::fromStdString(room.first); if (!timelineViewExists(roomid)) { - qDebug() << "Ignoring event from unknown room" << roomid; + nhlog::ui()->warn("ignoring event from unknown room: {}", + roomid.toStdString()); continue; } @@ -212,7 +213,8 @@ void TimelineViewManager::setHistoryView(const QString &room_id) { if (!timelineViewExists(room_id)) { - qDebug() << "Room ID from RoomList is not present in ViewManager" << room_id; + nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", + room_id.toStdString()); return; } diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cc index 1ad47747..7cbbed28 100644 --- a/src/timeline/widgets/AudioItem.cc +++ b/src/timeline/widgets/AudioItem.cc @@ -16,13 +16,13 @@ */ #include -#include #include #include #include #include #include +#include "Logging.hpp" #include "MatrixClient.h" #include "Utils.h" @@ -127,7 +127,8 @@ AudioItem::mousePressEvent(QMouseEvent *event) const std::string &, mtx::http::RequestErr err) { if (err) { - qWarning() << "failed to retrieve m.audio content:" << url_; + nhlog::net()->info("failed to retrieve m.audio content: {}", + url_.toString().toStdString()); return; } @@ -147,8 +148,8 @@ AudioItem::fileDownloaded(const QByteArray &data) file.write(data); file.close(); - } catch (const std::exception &ex) { - qDebug() << "Error while saving file to:" << ex.what(); + } catch (const std::exception &e) { + nhlog::ui()->warn("error while saving file: {}", e.what()); } } diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cc index 43689243..4ce4d256 100644 --- a/src/timeline/widgets/FileItem.cc +++ b/src/timeline/widgets/FileItem.cc @@ -16,13 +16,13 @@ */ #include -#include #include #include #include #include #include +#include "Logging.hpp" #include "MatrixClient.h" #include "Utils.h" @@ -89,7 +89,7 @@ FileItem::openUrl() .arg(QString::fromStdString(mxc_parts.media_id)); if (!QDesktopServices::openUrl(urlToOpen)) - qWarning() << "Could not open url" << urlToOpen; + nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString()); } QSize @@ -121,7 +121,8 @@ FileItem::mousePressEvent(QMouseEvent *event) const std::string &, mtx::http::RequestErr err) { if (err) { - qWarning() << "failed to retrieve m.file content:" << url_; + nhlog::ui()->warn("failed to retrieve m.file content: {}", + url_.toString().toStdString()); return; } @@ -143,8 +144,8 @@ FileItem::fileDownloaded(const QByteArray &data) file.write(data); file.close(); - } catch (const std::exception &ex) { - qDebug() << "Error while saving file to:" << ex.what(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); } } diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cc index 6aa010a4..bf1c05d6 100644 --- a/src/timeline/widgets/ImageItem.cc +++ b/src/timeline/widgets/ImageItem.cc @@ -16,7 +16,6 @@ */ #include -#include #include #include #include @@ -25,6 +24,7 @@ #include #include "Config.h" +#include "Logging.hpp" #include "MatrixClient.h" #include "Utils.h" #include "dialogs/ImageOverlay.h" @@ -39,8 +39,11 @@ ImageItem::downloadMedia(const QUrl &url) const std::string &, mtx::http::RequestErr err) { if (err) { - qWarning() - << "failed to retrieve image:" << url; + nhlog::net()->warn( + "failed to retrieve image {}: {} {}", + url.toString().toStdString(), + err->matrix_error.error, + static_cast(err->status_code)); return; } @@ -61,8 +64,8 @@ ImageItem::saveImage(const QString &filename, const QByteArray &data) file.write(data); file.close(); - } catch (const std::exception &ex) { - qDebug() << "Error while saving file to:" << ex.what(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); } } @@ -111,7 +114,7 @@ ImageItem::openUrl() .arg(QString::fromStdString(mxc_parts.media_id)); if (!QDesktopServices::openUrl(urlToOpen)) - qWarning() << "Could not open url" << urlToOpen; + nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString()); } QSize @@ -239,14 +242,19 @@ ImageItem::saveAs() if (filename.isEmpty()) return; + const auto url = url_.toString().toStdString(); + http::v2::client()->download( - url_.toString().toStdString(), - [this, filename](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { + url, + [this, filename, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { if (err) { - qWarning() << "failed to retrieve image:" << url_; + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); return; } diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cc index c1f68847..34d963a9 100644 --- a/src/timeline/widgets/VideoItem.cc +++ b/src/timeline/widgets/VideoItem.cc @@ -15,7 +15,6 @@ * along with this program. If not, see . */ -#include #include #include From 7fc010fc4f96d6a4cdec04558a7b053604e0cb39 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Thu, 14 Jun 2018 09:36:41 +0300 Subject: [PATCH 10/18] Fix CI errors on macOS & Windows - Run the linter - Explicitly use cmake for installing olm --- deps/cmake/Olm.cmake | 7 ++++--- src/CommunitiesList.cc | 8 ++++---- src/RegisterPage.cc | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake index c476f71d..eb4f53e3 100644 --- a/deps/cmake/Olm.cmake +++ b/deps/cmake/Olm.cmake @@ -1,5 +1,3 @@ -set(OLM_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/patches/olm-CMake-Support.patch) - ExternalProject_Add( Olm @@ -20,6 +18,9 @@ ExternalProject_Add( ${DEPS_BUILD_DIR}/olm BUILD_COMMAND ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/olm - --config Release) + --config Release + INSTALL_COMMAND ${CMAKE_COMMAND} + --build ${DEPS_BUILD_DIR}/olm + --target install) list(APPEND THIRD_PARTY_DEPS Olm) diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc index df4d6361..39e9a7fe 100644 --- a/src/CommunitiesList.cc +++ b/src/CommunitiesList.cc @@ -1,5 +1,5 @@ -#include "Cache.h" #include "CommunitiesList.h" +#include "Cache.h" #include "Logging.hpp" #include "MatrixClient.h" @@ -137,9 +137,9 @@ CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUr opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to download avatar: {} - ({} {})", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); return; } diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc index f56cd663..db52e101 100644 --- a/src/RegisterPage.cc +++ b/src/RegisterPage.cc @@ -248,7 +248,7 @@ RegisterPage::onRegisterButtonClicked() } nhlog::net()->warn("failed to register: status_code ({})", - static_cast(err->status_code)); + static_cast(err->status_code)); emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); emit errorOccurred(); From 9102a141f3f169c39b4fe87839e646eb68fd4b55 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Fri, 15 Jun 2018 01:35:31 +0300 Subject: [PATCH 11/18] Handle OLM_MESSAGE type of messages properly --- deps/CMakeLists.txt | 2 +- include/Cache.h | 28 ++++--- include/Olm.hpp | 12 ++- src/Cache.cc | 82 ++++++++++++-------- src/Olm.cpp | 144 +++++++++++++++++++++++++---------- src/timeline/TimelineView.cc | 13 ++-- 6 files changed, 188 insertions(+), 93 deletions(-) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index c948a097..7ea44bbd 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs) set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de) set(MTXCLIENT_URL https://github.com/mujx/mtxclient) -set(MTXCLIENT_TAG 688d5b0fd1fd16319d7fcbdbf938109eaa850545) +set(MTXCLIENT_TAG c566fa0a254dce3282435723eb58590880be2b53) set(OLM_URL https://git.matrix.org/git/olm.git) set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) diff --git a/include/Cache.h b/include/Cache.h index b4dcdb90..f5a655cf 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -17,6 +17,8 @@ #pragma once +#include + #include #include @@ -209,13 +211,12 @@ struct MegolmSessionIndex struct OlmSessionStorage { - std::map outbound_sessions; + // Megolm sessions std::map group_inbound_sessions; std::map group_outbound_sessions; std::map group_outbound_session_data; - // Guards for accessing critical data. - std::mutex outbound_mtx; + // Guards for accessing megolm sessions. std::mutex group_outbound_mtx; std::mutex group_inbound_mtx; }; @@ -374,12 +375,12 @@ public: bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; // - // Outbound Olm Sessions + // Olm Sessions // - void saveOutboundOlmSession(const std::string &curve25519, - mtx::crypto::OlmSessionPtr session); - OlmSession *getOutboundOlmSession(const std::string &curve25519); - bool outboundOlmSessionsExists(const std::string &curve25519) noexcept; + void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); + std::vector getOlmSessions(const std::string &curve25519); + boost::optional getOlmSession(const std::string &curve25519, + const std::string &session_id); void saveOlmAccount(const std::string &pickled); std::string restoreOlmAccount(); @@ -560,6 +561,16 @@ private: return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); } + //! Retrieves or creates the database that stores the open OLM sessions between our device + //! and the given curve25519 key which represents another device. + //! + //! Each entry is a map from the session_id to the pickled representation of the session. + lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) + { + return lmdb::dbi::open( + txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); + } + QString getDisplayName(const mtx::events::StateEvent &event) { if (!event.content.display_name.empty()) @@ -584,7 +595,6 @@ private: lmdb::dbi inboundMegolmSessionDb_; lmdb::dbi outboundMegolmSessionDb_; - lmdb::dbi outboundOlmSessionDb_; QString localUserId_; QString cacheDirectory_; diff --git a/include/Olm.hpp b/include/Olm.hpp index 0839f01c..6f871628 100644 --- a/include/Olm.hpp +++ b/include/Olm.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -51,13 +53,17 @@ client(); void handle_to_device_messages(const std::vector &msgs); +boost::optional +try_olm_decryption(const std::string &sender_key, const OlmCipherContent &content); + void handle_olm_message(const OlmMessage &msg); +//! Establish a new inbound megolm session with the decrypted payload from olm. void -handle_olm_normal_message(const std::string &sender, - const std::string &sender_key, - const OlmCipherContent &content); +create_inbound_megolm_session(const std::string &sender, + const std::string &sender_key, + const nlohmann::json &payload); void handle_pre_key_olm_message(const std::string &sender, diff --git a/src/Cache.cc b/src/Cache.cc index 20572ece..41a759ab 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -62,11 +62,10 @@ constexpr auto DEVICE_KEYS_DB("device_keys"); //! room_ids that have encryption enabled. constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); -//! MegolmSessionIndex -> pickled OlmInboundGroupSession +//! room_id -> pickled OlmInboundGroupSession constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); //! MegolmSessionIndex -> pickled OlmOutboundGroupSession constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); -constexpr auto OUTBOUND_OLM_SESSIONS_DB("outbound_olm_sessions"); using CachedReceipts = std::multimap>; using Receipts = std::map>; @@ -110,7 +109,6 @@ Cache::Cache(const QString &userId, QObject *parent) , deviceKeysDb_{0} , inboundMegolmSessionDb_{0} , outboundMegolmSessionDb_{0} - , outboundOlmSessionDb_{0} , localUserId_{userId} { setup(); @@ -180,7 +178,6 @@ Cache::setup() // Session management inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); - outboundOlmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_OLM_SESSIONS_DB, MDB_CREATE); txn.commit(); } @@ -321,35 +318,66 @@ Cache::getOutboundMegolmSession(const std::string &room_id) session_storage.group_outbound_session_data[room_id]}; } +// +// OLM sessions. +// + void -Cache::saveOutboundOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) { using namespace mtx::crypto; - const auto pickled = pickle(session.get(), SECRET); auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, outboundOlmSessionDb_, lmdb::val(curve25519), lmdb::val(pickled)); + auto db = getOlmSessionsDb(txn, curve25519); + + const auto pickled = pickle(session.get(), SECRET); + const auto session_id = mtx::crypto::session_id(session.get()); + + lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(pickled)); + + txn.commit(); +} + +boost::optional +Cache::getOlmSession(const std::string &curve25519, const std::string &session_id) +{ + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); + + lmdb::val pickled; + bool found = lmdb::dbi_get(txn, db, lmdb::val(session_id), pickled); + txn.commit(); - { - std::unique_lock lock(session_storage.outbound_mtx); - session_storage.outbound_sessions[curve25519] = std::move(session); + if (found) { + auto data = std::string(pickled.data(), pickled.size()); + return unpickle(data, SECRET); } + + return boost::none; } -bool -Cache::outboundOlmSessionsExists(const std::string &curve25519) noexcept +std::vector +Cache::getOlmSessions(const std::string &curve25519) { - std::unique_lock lock(session_storage.outbound_mtx); - return session_storage.outbound_sessions.find(curve25519) != - session_storage.outbound_sessions.end(); -} + using namespace mtx::crypto; -OlmSession * -Cache::getOutboundOlmSession(const std::string &curve25519) -{ - std::unique_lock lock(session_storage.outbound_mtx); - return session_storage.outbound_sessions.at(curve25519).get(); + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); + + std::string session_id, unused; + std::vector res; + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(session_id, unused, MDB_NEXT)) + res.emplace_back(session_id); + cursor.close(); + + txn.commit(); + + return res; } void @@ -405,18 +433,6 @@ Cache::restoreSessions() cursor.close(); } - // - // Outbound Olm Sessions - // - { - auto cursor = lmdb::cursor::open(txn, outboundOlmSessionDb_); - while (cursor.get(key, value, MDB_NEXT)) { - auto session = unpickle(value, SECRET); - session_storage.outbound_sessions[key] = std::move(session); - } - cursor.close(); - } - txn.commit(); nhlog::db()->info("sessions restored"); diff --git a/src/Olm.cpp b/src/Olm.cpp index f39554f0..814fce18 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -55,10 +55,21 @@ handle_olm_message(const OlmMessage &msg) const auto type = cipher.second.type; nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); - if (type == OLM_MESSAGE_TYPE_PRE_KEY) - handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second); - else - handle_olm_normal_message(msg.sender, msg.sender_key, cipher.second); + auto payload = try_olm_decryption(msg.sender_key, cipher.second); + + if (payload) { + nhlog::crypto()->info("decrypted olm payload: {}", payload.value().dump(2)); + create_inbound_megolm_session(msg.sender, msg.sender_key, payload.value()); + return; + } + + // Not a PRE_KEY message + if (cipher.second.type != 0) { + // TODO: log that it should have matched something + return; + } + + handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second); } } @@ -72,6 +83,10 @@ handle_pre_key_olm_message(const std::string &sender, OlmSessionPtr inbound_session = nullptr; try { inbound_session = olm::client()->create_inbound_session(content.body); + + // We also remove the one time key used to establish that + // session so we'll have to update our copy of the account object. + cache::client()->saveOlmAccount(olm::client()->save("secret")); } catch (const olm_exception &e) { nhlog::crypto()->critical( "failed to create inbound session with {}: {}", sender, e.what()); @@ -86,8 +101,8 @@ handle_pre_key_olm_message(const std::string &sender, mtx::crypto::BinaryBuf output; try { - output = olm::client()->decrypt_message( - inbound_session.get(), OLM_MESSAGE_TYPE_PRE_KEY, content.body); + output = + olm::client()->decrypt_message(inbound_session.get(), content.type, content.body); } catch (const olm_exception &e) { nhlog::crypto()->critical( "failed to decrypt olm message {}: {}", content.body, e.what()); @@ -97,45 +112,14 @@ handle_pre_key_olm_message(const std::string &sender, auto plaintext = json::parse(std::string((char *)output.data(), output.size())); nhlog::crypto()->info("decrypted message: \n {}", plaintext.dump(2)); - std::string room_id, session_id, session_key; try { - room_id = plaintext.at("content").at("room_id"); - session_id = plaintext.at("content").at("session_id"); - session_key = plaintext.at("content").at("session_key"); - } catch (const nlohmann::json::exception &e) { - nhlog::crypto()->critical( - "failed to parse plaintext olm message: {} {}", e.what(), plaintext.dump(2)); - return; + cache::client()->saveOlmSession(sender_key, std::move(inbound_session)); + } catch (const lmdb::error &e) { + nhlog::db()->warn( + "failed to save inbound olm session from {}: {}", sender, e.what()); } - MegolmSessionIndex index; - index.room_id = room_id; - index.session_id = session_id; - index.sender_key = sender_key; - - if (!cache::client()->inboundMegolmSessionExists(index)) { - auto megolm_session = olm::client()->init_inbound_group_session(session_key); - - try { - cache::client()->saveInboundMegolmSession(index, std::move(megolm_session)); - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save inbound megolm session: {}", - e.what()); - return; - } - - nhlog::crypto()->info( - "established inbound megolm session ({}, {})", room_id, sender); - } else { - nhlog::crypto()->warn( - "inbound megolm session already exists ({}, {})", room_id, sender); - } -} - -void -handle_olm_normal_message(const std::string &, const std::string &, const OlmCipherContent &) -{ - nhlog::crypto()->warn("olm(1) not implemeted yet"); + create_inbound_megolm_session(sender, sender_key, plaintext); } mtx::events::msg::Encrypted @@ -165,4 +149,80 @@ encrypt_group_message(const std::string &room_id, return data; } +boost::optional +try_olm_decryption(const std::string &sender_key, const OlmCipherContent &msg) +{ + auto session_ids = cache::client()->getOlmSessions(sender_key); + + for (const auto &id : session_ids) { + auto session = cache::client()->getOlmSession(sender_key, id); + + if (!session) + continue; + + mtx::crypto::BinaryBuf text; + + try { + text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); + cache::client()->saveOlmSession(id, std::move(session.value())); + + } catch (const olm_exception &e) { + nhlog::crypto()->info("failed to decrypt olm message ({}, {}) with {}: {}", + msg.type, + sender_key, + id, + e.what()); + continue; + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save session: {}", e.what()); + return {}; + } + + try { + return json::parse(std::string((char *)text.data(), text.size())); + } catch (const json::exception &e) { + nhlog::crypto()->critical("failed to parse the decrypted session msg: {}", + e.what()); + } + } + + return {}; +} + +void +create_inbound_megolm_session(const std::string &sender, + const std::string &sender_key, + const nlohmann::json &payload) +{ + std::string room_id, session_id, session_key; + + try { + room_id = payload.at("content").at("room_id"); + session_id = payload.at("content").at("session_id"); + session_key = payload.at("content").at("session_key"); + } catch (const nlohmann::json::exception &e) { + nhlog::crypto()->critical( + "failed to parse plaintext olm message: {} {}", e.what(), payload.dump(2)); + return; + } + + MegolmSessionIndex index; + index.room_id = room_id; + index.session_id = session_id; + index.sender_key = sender_key; + + try { + auto megolm_session = olm::client()->init_inbound_group_session(session_key); + cache::client()->saveInboundMegolmSession(index, std::move(megolm_session)); + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); + return; + } catch (const olm_exception &e) { + nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what()); + return; + } + + nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender); +} + } // namespace olm diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 8f3ad1a7..5841ebce 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -1329,18 +1329,21 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) auto otk = rd.second.begin()->at("key"); auto id_key = pks.curve25519; - auto session = - olm::client() - ->create_outbound_session(id_key, - otk); + auto s = olm::client() + ->create_outbound_session( + id_key, otk); auto device_msg = olm::client() ->create_olm_encrypted_content( - session.get(), + s.get(), room_key, pks.curve25519); + // TODO: Handle exception + cache::client()->saveOlmSession( + id_key, std::move(s)); + json body{ {"messages", {{user_id, From b758cc4487837b93dacac9605a1668543a79a221 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Fri, 15 Jun 2018 02:25:09 +0300 Subject: [PATCH 12/18] Pass down toolchain file on Windows --- deps/cmake/MatrixClient.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/deps/cmake/MatrixClient.cmake b/deps/cmake/MatrixClient.cmake index 7377f710..d8dd48c7 100644 --- a/deps/cmake/MatrixClient.cmake +++ b/deps/cmake/MatrixClient.cmake @@ -22,6 +22,7 @@ ExternalProject_Add( -DCMAKE_BUILD_TYPE=Release -DBUILD_LIB_TESTS=OFF -DBUILD_LIB_EXAMPLES=OFF + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} ${PLATFORM_FLAGS} ${DEPS_BUILD_DIR}/mtxclient BUILD_COMMAND From 24e0285210e758ab4913fbad84daba632bc2a6d1 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Fri, 15 Jun 2018 16:45:39 +0300 Subject: [PATCH 13/18] Don't try to send megolm session keys to devices with no one-time keys --- include/timeline/TimelineView.h | 21 +++++ src/timeline/TimelineView.cc | 154 ++++++++++++++++---------------- 2 files changed, 96 insertions(+), 79 deletions(-) diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index 5b5c2292..a985f75b 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -33,6 +33,19 @@ #include "ScrollBar.h" #include "TimelineItem.h" +class StateKeeper +{ +public: + StateKeeper(std::function &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function fn_; +}; + class FloatingButton; struct DescInfo; @@ -181,6 +194,14 @@ private: TimelineEvent parseEncryptedEvent( const mtx::events::EncryptedEvent &e); + void handleClaimedKeys(std::shared_ptr keeper, + const std::string &room_key, + const DevicePublicKeys &pks, + const std::string &user_id, + const std::string &device_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + //! Callback for all message sending. void sendRoomMessageHandler(const std::string &txn_id, const mtx::responses::EventId &res, diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 5841ebce..5ee2d32b 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -34,19 +34,6 @@ #include "timeline/widgets/ImageItem.h" #include "timeline/widgets/VideoItem.h" -class StateKeeper -{ -public: - StateKeeper(std::function &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function fn_; -}; - using TimelineEvent = mtx::events::collections::TimelineEvents; DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) @@ -1254,8 +1241,8 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) http::v2::client()->query_keys( req, - [keeper = std::move(keeper), megolm_payload](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { + [keeper = std::move(keeper), megolm_payload, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {} {}", err->matrix_error.error, @@ -1300,70 +1287,15 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) http::v2::client()->claim_keys( dev.second.user_id, {dev.second.device_id}, - [keeper, - room_key, - pks, - user_id = dev.second.user_id, - device_id = dev.second.device_id]( - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "claim keys error: {}", - err->matrix_error.error); - return; - } - - nhlog::net()->info("claimed keys for {} - {}", - user_id, - device_id); - - auto retrieved_devices = - res.one_time_keys.at(user_id); - for (const auto &rd : retrieved_devices) { - nhlog::net()->info("{} : \n {}", - rd.first, - rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - auto id_key = pks.curve25519; - - auto s = olm::client() - ->create_outbound_session( - id_key, otk); - - auto device_msg = - olm::client() - ->create_olm_encrypted_content( - s.get(), - room_key, - pks.curve25519); - - // TODO: Handle exception - cache::client()->saveOlmSession( - id_key, std::move(s)); - - json body{ - {"messages", - {{user_id, - {{device_id, device_msg}}}}}}; - - http::v2::client()->send_to_device( - "m.room.encrypted", - body, - [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to send " - "send_to_device " - "message: {}", - err->matrix_error - .error); - } - }); - } - }); + std::bind(&TimelineView::handleClaimedKeys, + this, + keeper, + room_key, + pks, + dev.second.user_id, + dev.second.device_id, + std::placeholders::_1, + std::placeholders::_2)); } } }); @@ -1374,3 +1306,67 @@ TimelineView::prepareEncryptedMessage(const PendingMessage &msg) return; } } + +void +TimelineView::handleClaimedKeys(std::shared_ptr keeper, + const std::string &room_key, + const DevicePublicKeys &pks, + const std::string &user_id, + const std::string &device_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {}", err->matrix_error.error); + return; + } + + nhlog::net()->info("claimed keys for {} - {}", user_id, device_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->info("no one-time keys found for device_id: {}", device_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->info( + "no one-time keys found in device_id {} for the user {}", device_id, user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + for (const auto &rd : retrieved_devices) { + nhlog::net()->info("{} : \n {}", rd.first, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + auto id_key = pks.curve25519; + + auto s = olm::client()->create_outbound_session(id_key, otk); + + auto device_msg = + olm::client()->create_olm_encrypted_content(s.get(), room_key, pks.curve25519); + + try { + cache::client()->saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + json body{{"messages", {{user_id, {{device_id, device_msg}}}}}}; + + http::v2::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + }); + } +} From c0d32ef319f9fad0e2857f580c763cdb14d0cbb5 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sun, 17 Jun 2018 00:23:49 +0300 Subject: [PATCH 14/18] Update CI instructions --- .ci/script.sh | 1 - CMakeLists.txt | 21 +++++++++------------ Makefile | 15 ++++++++++----- appveyor.yml | 6 ------ deps/CMakeLists.txt | 2 +- deps/cmake/Olm.cmake | 8 ++++++++ deps/cmake/OlmConfig.cmake.in | 4 ++-- deps/cmake/SpdLog.cmake | 9 +++++++-- src/dialogs/PreviewUploadOverlay.cc | 6 +++--- 9 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.ci/script.sh b/.ci/script.sh index f5966349..377f23e1 100755 --- a/.ci/script.sh +++ b/.ci/script.sh @@ -11,7 +11,6 @@ if [ $TRAVIS_OS_NAME == osx ]; then fi # Build & install dependencies -mkdir -p .deps/usr/{lib,include}/ cmake -Hdeps -B.deps \ -DUSE_BUNDLED_BOOST=${USE_BUNDLED_BOOST} \ -DUSE_BUNDLED_SPDLOG=${USE_BUNDLED_SPDLOG} diff --git a/CMakeLists.txt b/CMakeLists.txt index ef20be39..e9bf05bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,12 +29,6 @@ set(IDENTIFIER "com.github.mujx.nheko") add_project_meta(META_FILES_TO_INCLUDE) -set(DEPS_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/.deps/usr" - CACHE PATH "Path prefix for finding dependencies") -list(INSERT CMAKE_PREFIX_PATH 0 ${DEPS_PREFIX}) - -include_directories(SYSTEM ${DEPS_PREFIX}/include) - if(APPLE) set(OPENSSL_ROOT_DIR /usr/local/opt/openssl) endif() @@ -208,8 +202,10 @@ set(EXTERNAL_PROJECT_DEPS "") find_package(ZLIB REQUIRED) find_package(OpenSSL REQUIRED) -find_package(MatrixStructs REQUIRED) -find_package(MatrixClient REQUIRED) +find_package(MatrixStructs 0.1.0 REQUIRED) +find_package(MatrixClient 0.1.0 REQUIRED) +find_package(Olm 2 REQUIRED) +find_package(spdlog 0.16.0 CONFIG REQUIRED) # # tweeny @@ -315,7 +311,8 @@ set(COMMON_LIBS MatrixClient::MatrixClient Qt5::Widgets Qt5::Svg - Qt5::Concurrent) + Qt5::Concurrent + Qt5::Multimedia) if(APPVEYOR_BUILD) set(NHEKO_LIBS ${COMMON_LIBS} lmdb) @@ -345,13 +342,13 @@ endif() if(APPLE) add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) - target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras Qt5::Multimedia) + target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras) elseif(WIN32) add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS}) - target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain Qt5::Multimedia) + target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain) else() add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) - target_link_libraries (nheko ${NHEKO_LIBS} Qt5::Multimedia) + target_link_libraries (nheko ${NHEKO_LIBS}) endif() if(EXTERNAL_PROJECT_DEPS) diff --git a/Makefile b/Makefile index 833d81a0..584aafa2 100644 --- a/Makefile +++ b/Makefile @@ -2,25 +2,30 @@ DEPS_BUILD_DIR=.deps DEPS_SOURCE_DIR=deps debug: - @cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 + @cmake -H. -GNinja \ + -Bbuild \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \ + -DCMAKE_INSTALL_PREFIX=${DEPS_BUILD_DIR}/usr @cmake --build build -third_party: - @mkdir -p ${DEPS_BUILD_DIR}/usr/{lib,include}/ +third-party: @cmake -GNinja -H${DEPS_SOURCE_DIR} -B${DEPS_BUILD_DIR} \ -DCMAKE_BUILD_TYPE=Release \ -DUSE_BUNDLED_BOOST=OFF @cmake --build ${DEPS_BUILD_DIR} ci: - mkdir -p ${DEPS_BUILD_DIR}/usr/{lib,include}/ cmake -H${DEPS_SOURCE_DIR} -B${DEPS_BUILD_DIR} -DCMAKE_BUILD_TYPE=Release cmake --build ${DEPS_BUILD_DIR} cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build release: - @cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo + @cmake -H. -GNinja \ + -Bbuild \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=${DEPS_BUILD_DIR}/usr @cmake --build build linux-install: diff --git a/appveyor.yml b/appveyor.yml index c9f3023e..dfd637ed 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,7 +15,6 @@ install: - set QT_DIR=C:\Qt\5.10.1\msvc2017_64 - set PATH=%PATH%;%QT_DIR%\bin;C:\MinGW\bin - set PATH=%PATH%;C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\bin - - mingw32-make.exe --version - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" - vcpkg install boost-asio:%PLATFORM%-windows @@ -28,7 +27,6 @@ install: libsodium:%PLATFORM%-windows lmdb:%PLATFORM%-windows openssl:%PLATFORM%-windows - spdlog:%PLATFORM%-windows zlib:%PLATFORM%-windows build_script: @@ -53,16 +51,12 @@ build_script: # Build & install the dependencies - cmake -G "Visual Studio 15 2017 Win64" -Hdeps -B.deps -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake - -DCMAKE_BUILD_TYPE=Release -DUSE_BUNDLED_BOOST=OFF - -DUSE_BUNDLED_SPDLOG=OFF - -DUSE_BUNDLED_GTEST=OFF - cmake --build .deps --config Release # Build nheko - cmake -G "Visual Studio 15 2017 Win64" -H. -Bbuild -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake - -DCMAKE_BUILD_TYPE=Release - cmake --build build --config Release after_build: diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 7ea44bbd..34bcea31 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs) set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de) set(MTXCLIENT_URL https://github.com/mujx/mtxclient) -set(MTXCLIENT_TAG c566fa0a254dce3282435723eb58590880be2b53) +set(MTXCLIENT_TAG e45b1c85fce52f8ed516fd98a9cbb6a110a20d09) set(OLM_URL https://git.matrix.org/git/olm.git) set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake index eb4f53e3..a5b8be76 100644 --- a/deps/cmake/Olm.cmake +++ b/deps/cmake/Olm.cmake @@ -1,3 +1,9 @@ +set(WINDOWS_FLAGS "") + +if(MSVC) + set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") +endif() + ExternalProject_Add( Olm @@ -16,11 +22,13 @@ ExternalProject_Add( -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} -DCMAKE_BUILD_TYPE=Release ${DEPS_BUILD_DIR}/olm + ${WINDOWS_FLAGS} BUILD_COMMAND ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/olm --config Release INSTALL_COMMAND ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/olm + --config Release --target install) list(APPEND THIRD_PARTY_DEPS Olm) diff --git a/deps/cmake/OlmConfig.cmake.in b/deps/cmake/OlmConfig.cmake.in index a7541f7a..b670fe85 100644 --- a/deps/cmake/OlmConfig.cmake.in +++ b/deps/cmake/OlmConfig.cmake.in @@ -4,8 +4,8 @@ include(CMakeFindDependencyMacro) list(APPEND CMAKE_MODULE_PATH ${Olm_CMAKE_DIR}) list(REMOVE_AT CMAKE_MODULE_PATH -1) -if(NOT TARGET Olm::olm) +if(NOT TARGET Olm::Olm) include("${Olm_CMAKE_DIR}/OlmTargets.cmake") endif() -set(Olm_LIBRARIES Olm::olm) +set(Olm_LIBRARIES Olm::Olm) diff --git a/deps/cmake/SpdLog.cmake b/deps/cmake/SpdLog.cmake index 1335725e..5a5f318c 100644 --- a/deps/cmake/SpdLog.cmake +++ b/deps/cmake/SpdLog.cmake @@ -1,3 +1,9 @@ +set(WINDOWS_FLAGS "") + +if(MSVC) + set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") +endif() + ExternalProject_Add( SpdLog @@ -10,8 +16,7 @@ ExternalProject_Add( -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} -DSPDLOG_BUILD_EXAMPLES=0 -DSPDLOG_BUILD_TESTING=0 - -DCMAKE_BUILD_TYPE=Release ${DEPS_BUILD_DIR}/spdlog -) + ${WINDOWS_FLAGS}) list(APPEND THIRD_PARTY_DEPS SpdLog) diff --git a/src/dialogs/PreviewUploadOverlay.cc b/src/dialogs/PreviewUploadOverlay.cc index db1e31f2..e01d2b17 100644 --- a/src/dialogs/PreviewUploadOverlay.cc +++ b/src/dialogs/PreviewUploadOverlay.cc @@ -31,8 +31,8 @@ using namespace dialogs; -static constexpr const char *DEFAULT = "Upload %1?"; -static constexpr const char *ERROR = "Failed to load image type '%1'. Continue upload?"; +constexpr const char *DEFAULT = "Upload %1?"; +constexpr const char *ERR_MSG = "Failed to load image type '%1'. Continue upload?"; PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent) : QWidget{parent} @@ -105,7 +105,7 @@ PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64 { if (mediaType_ == "image") { if (!image_.loadFromData(data_)) { - titleLabel_.setText(QString{tr(ERROR)}.arg(type)); + titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); } else { titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_)); } From 66249ed12628b9bb5cf0603a627858d921e8b41a Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sun, 17 Jun 2018 02:29:19 +0300 Subject: [PATCH 15/18] Correctly mark received messages whose response came after /sync --- include/timeline/TimelineItem.h | 5 +++++ src/timeline/TimelineItem.cc | 1 + src/timeline/TimelineView.cc | 19 +++++++++++++------ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h index 6623a82c..f055f217 100644 --- a/include/timeline/TimelineItem.h +++ b/include/timeline/TimelineItem.h @@ -193,6 +193,7 @@ public: QString eventId() const { return event_id_; } void setEventId(const QString &event_id) { event_id_ = event_id; } void markReceived(); + bool isReceived() { return isReceived_; }; void setRoomId(QString room_id) { room_id_ = room_id; } void sendReadReceipt() const; @@ -225,6 +226,10 @@ private: void setupAvatarLayout(const QString &userName); void setupSimpleLayout(); + //! Whether or not the event associated with the widget + //! has been acknowledged by the server. + bool isReceived_ = false; + QString replaceEmoji(const QString &body); QString event_id_; QString room_id_; diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc index 3505d347..c104801d 100644 --- a/src/timeline/TimelineItem.cc +++ b/src/timeline/TimelineItem.cc @@ -432,6 +432,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent void TimelineItem::markReceived() { + isReceived_ = true; checkmark_->setText(CHECKMARK); checkmark_->setAlignment(Qt::AlignTop); diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 5ee2d32b..e437439e 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -602,11 +602,18 @@ TimelineView::updatePendingMessage(const std::string &txn_id, const QString &eve if (msg.widget) { msg.widget->setEventId(event_id); - msg.widget->markReceived(); eventIds_[event_id] = msg.widget; - } - pending_sent_msgs_.append(msg); + // If the response comes after we have received the event from sync + // we've already marked the widget as received. + if (!msg.widget->isReceived()) { + msg.widget->markReceived(); + pending_sent_msgs_.append(msg); + } + } else { + nhlog::ui()->warn("[{}] received message response for invalid widget", + txn_id); + } } sendNextPendingMessage(); @@ -809,10 +816,10 @@ TimelineView::removePendingMessage(const std::string &txn_id) } for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { if (it->txn_id == txn_id) { - int index = std::distance(pending_msgs_.begin(), it); - pending_msgs_.removeAt(index); + if (it->widget) + it->widget->markReceived(); - nhlog::ui()->info("[{}] removed message before sync", txn_id); + nhlog::ui()->info("[{}] received sync before message response", txn_id); return; } } From d081fe9b9e248de9bd0452871e6bf4dbae9e603a Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sun, 17 Jun 2018 16:53:14 +0300 Subject: [PATCH 16/18] Setup BinTray --- .ci/bintray-release.json | 37 +++++++++++++++++++++++++++++++++++++ .ci/linux/deploy.sh | 11 +++++++++-- .ci/macos/deploy.sh | 4 ++++ .travis.yml | 28 +++++++++------------------- appveyor.yml | 29 ++++++++++++++++------------- src/ChatPage.cc | 16 ++++++++-------- 6 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 .ci/bintray-release.json diff --git a/.ci/bintray-release.json b/.ci/bintray-release.json new file mode 100644 index 00000000..7115c910 --- /dev/null +++ b/.ci/bintray-release.json @@ -0,0 +1,37 @@ +{ + "files": [ + { + "includePattern": "nheko-VERSION_NAME_VALUE.dmg", + "matrixParams": { + "override": 1 + }, + "uploadPattern": "VERSION_NAME_VALUE/nheko-VERSION_NAME_VALUE.dmg" + }, + { + "includePattern": "nheko-VERSION_NAME_VALUE-x86_64.AppImage", + "matrixParams": { + "override": 1 + }, + "uploadPattern": "VERSION_NAME_VALUE/nheko-VERSION_NAME_VALUE-x86_64.AppImage" + } + ], + "package": { + "desc": "Desktop client for the Matrix protocol", + "issue_tracker_url": "https://github.com/mujx/nheko/issues", + "licenses": [ + "GPL-3.0" + ], + "name": "nheko", + "public_download_numbers": true, + "public_stats": true, + "repo": "matrix", + "subject": "mujx", + "vcs_url": "https://github.com/mujx/nheko", + "website_url": "https://github.com/mujx/nheko" + }, + "publish": true, + "version": { + "name": "VERSION_NAME_VALUE", + "vcs_tag": "VERSION_NAME_VALUE" + } +} diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 77a4fa06..39fb0e30 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -7,7 +7,7 @@ DIR=${APP}.AppDir TAG=`git tag -l --points-at HEAD` # Set up AppImage structure. -mkdir -p ${DIR}/usr/{bin,share/pixmaps,share/applications} +mkdir -p ${DIR}/usr/{bin,lib,share/pixmaps,share/applications} # Copy resources. cp build/nheko ${DIR}/usr/bin @@ -30,9 +30,16 @@ unset QTDIR unset QT_PLUGIN_PATH unset LD_LIBRARY_PATH +cp -R .deps/usr/lib/* ${DIR}/usr/lib + +ldd ${DIR}/usr/bin/nheko + ./linuxdeployqt*.AppImage \ ${DIR}/usr/share/applications/nheko.desktop \ - -bundle-non-qt-libs\ -appimage chmod +x nheko-x86_64.AppImage + +if [ ! -z $TRAVIS_TAG ]; then + mv nheko-x86_64.AppImage nheko-${TRAVIS_TAG}-x86_64.AppImage +fi diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh index 133c7b0e..1de95a44 100755 --- a/.ci/macos/deploy.sh +++ b/.ci/macos/deploy.sh @@ -15,3 +15,7 @@ mv nheko.dmg .. popd dmgbuild -s ./.ci/macos/settings.json "Nheko" nheko.dmg + +if [ ! -z $TRAVIS_TAG ]; then + mv nheko.dmg nheko-${TRAVIS_TAG}.dmg +fi diff --git a/.travis.yml b/.travis.yml index 3e631f30..690f6690 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,28 +66,18 @@ install: script: - ./.ci/script.sh + - sed -i -e "s/VERSION_NAME_VALUE/${TRAVIS_TAG}/g" ./.ci/bintray-release.json || true + - cp ./.ci/bintray-release.json . deploy: - - skip_cleanup: true + - provider: bintray + user: "mujx" + key: + secure: "CAVzWZPxYSOTollo9bpD4tvEbfxXjqelc32aApV48GKyJrMQljQ+mvSe25BuUtnDehxnw8affgGX23AYXmvG8P7w4hM2d7//8Lgan1zCmusV8JE432jknev6X641B4cvrywqSe0Dj3l0kS9Xgirq4BGavlI0y2vUjeJfQEv0y8GYoI72LwgyH0i82v/1Qi92Fh8429IJIb0eKmC1wGWXCmo2kd8StZRL5mSlc4TmyWI0SHpA5GrLMiQwLAuD7DjDl5mpaK2yQx+H4vBcI2SUMvmlHGgVjXikJG5gURlHbnIaaBFvO67INc1/65KtMokWuMP12zxqJiaMPtsAskOpQv4FLAYDfnigH3NxufyOIGp2cxS5RhJDQhbNsxHEDnUo1kHcO23ZYNWCuC1yUdn0RXzKhWcUsz8mKF8KJs22Ty4VjfUMZ+vqK/AbHyq4rkl8DizVRZqKF1KjSWrSv/2sT4itnHk9pmcgxAYfGuALcjrJJveI4MTwDhzXB62CKnMOqLq3sAMqvE0+BdA0BykQr7qrKtptuyP2/OFx6RDbfHQl5Klkb6cSOjxm0oUzh/8iaxgsVdCrhfE67eqkhFZ+a8lJkB/rZ4zSK1Q2Cp4nLtnxenUCW+Ptk2l7zZN6kXM1/+tcgqVROChYJ6asMUpsjFOOAVQ8SZ4TcxX1rq+pxlA=" + skip_cleanup: true overwrite: true - provider: releases - api_key: - secure: oprXzESukFiXBeF2BXkXUlegsAQc95Ub4kc/OkoNFaYBvqpA+IGpWHmHCx5JPny/OT3Kc2Urpe2JUeGSWDHZ7UCKDjH+NzGP3uN82fHh/HiszG/Srw7+hWEHm1ve+gMK9GS8pr+yUsUrPP0UfVXlPoI4pBWa4zGi2Ndb/SqvjCgIHFLtGyoBo6CydKQ/AyWiXSMKDfJL+Dx4JLIPP4RTCBJy8ZrZ8m/a5Tgy4Ij6+djjrgYCZCEhGxSYw7xDIT/9SV8g9NkrbisqBDrILzAH8Yhe4XMRMXo88OAxV5+Vb9Rw1hrqczez6lpFDbJetv8MjofND+pSoAFwtjaL1wTFK9Ud6w4O9AuHlEQH9cGVdvsxFcosRwJVh58x51JM9ptoktqhx/HHJBTuCHCYYlHwtRwbwqnMYdLzKZG5FnujT8DG+9mcva1fL6tzW/XD505VPMWwXFC/2/pvolgAkTFFXYSALAwZlK3IgoXU8Gok/3B4iHofzQsFf6Yq3BI/88x7tVASUqiYhoKrO50+gb6pNIRCyWgGUiBEVXBp6Ziq3ORQPyQJg7i9HHUGTUu74yvGLHWLwjNQzZP/hxJZK3VlJxzyXntdOxiJc8iOzNrU+rPKBAlAaE6bQDOoniIysEgdD5BXHTLFzPvts4P1n2Ckor5/rNJ+qXR8GU+/y7e1GKU= - file_glob: true - file: - - nheko-x86_64.AppImage + file: "bintray-release.json" on: - condition: $TRAVIS_OS_NAME == linux && $DEPLOYMENT == 1 - repo: mujx/nheko - tags: true - - - skip_cleanup: true - overwrite: true - provider: releases - api_key: - secure: oprXzESukFiXBeF2BXkXUlegsAQc95Ub4kc/OkoNFaYBvqpA+IGpWHmHCx5JPny/OT3Kc2Urpe2JUeGSWDHZ7UCKDjH+NzGP3uN82fHh/HiszG/Srw7+hWEHm1ve+gMK9GS8pr+yUsUrPP0UfVXlPoI4pBWa4zGi2Ndb/SqvjCgIHFLtGyoBo6CydKQ/AyWiXSMKDfJL+Dx4JLIPP4RTCBJy8ZrZ8m/a5Tgy4Ij6+djjrgYCZCEhGxSYw7xDIT/9SV8g9NkrbisqBDrILzAH8Yhe4XMRMXo88OAxV5+Vb9Rw1hrqczez6lpFDbJetv8MjofND+pSoAFwtjaL1wTFK9Ud6w4O9AuHlEQH9cGVdvsxFcosRwJVh58x51JM9ptoktqhx/HHJBTuCHCYYlHwtRwbwqnMYdLzKZG5FnujT8DG+9mcva1fL6tzW/XD505VPMWwXFC/2/pvolgAkTFFXYSALAwZlK3IgoXU8Gok/3B4iHofzQsFf6Yq3BI/88x7tVASUqiYhoKrO50+gb6pNIRCyWgGUiBEVXBp6Ziq3ORQPyQJg7i9HHUGTUu74yvGLHWLwjNQzZP/hxJZK3VlJxzyXntdOxiJc8iOzNrU+rPKBAlAaE6bQDOoniIysEgdD5BXHTLFzPvts4P1n2Ckor5/rNJ+qXR8GU+/y7e1GKU= - file: nheko.dmg - on: - condition: $TRAVIS_OS_NAME == osx && $DEPLOYMENT == 1 + condition: $DEPLOYMENT == 1 repo: mujx/nheko tags: true diff --git a/appveyor.yml b/appveyor.yml index dfd637ed..48c7e5cc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -67,12 +67,9 @@ after_build: - copy build\Release\nheko.exe NhekoRelease\nheko.exe - windeployqt --qmldir %QT_DIR%\qml\ --release NhekoRelease\nheko.exe - - copy C:\Tools\vcpkg\installed\x64-windows\lib\lmdb.lib .\NhekoRelease\lmdb.lib - - copy C:\Tools\vcpkg\installed\x64-windows\bin\lmdb.dll .\NhekoRelease\lmdb.dll + - copy C:\Tools\vcpkg\installed\x64-windows\lib\*.lib .\NhekoRelease\ + - copy C:\Tools\vcpkg\installed\x64-windows\bin\*.dll .\NhekoRelease\ - - copy C:\OpenSSL-Win64\bin\ssleay32.dll .\NhekoRelease\ssleay32.dll - - copy C:\OpenSSL-Win64\bin\libeay32.dll .\NhekoRelease\libeay32.dll - - copy C:\OpenSSL-Win64\lib\libeay32.lib .\NhekoRelease\libeay32.lib - 7z a nheko_win_64.zip .\NhekoRelease\* - ls -lh build\Release\ - ls -lh NhekoRelease\ @@ -115,17 +112,23 @@ after_build: - set PATH=%BUILD%\tools\bin;%PATH% - binarycreator.exe -f -c installer\config\config.xml -p installer\packages nheko-installer.exe + - mv nheko-installer.exe nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe + deploy: - description: "Development builds" - provider: GitHub - auth_token: - secure: YqB7hcM+4482eSHhtVR7ZA7N7lE78y8BC897/7UDTBQd+NWdWFW/6S+oKDie9TT7 - artifact: nheko-installer.exe - force_update: true - prerelease: true + provider: BinTray + username: mujx + api_key: + secure: "hhhAH6csIrPEVH92NNQkiGCkuON6l6sfhbZk+pvzDAM3vHex7YbqFKW6v5UjAS8v" + subject: mujx + repo: matrix + package: nheko + version: $(APPVEYOR_REPO_TAG_NAME) + publish: true + override: true + artifact: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe on: appveyor_repo_tag: true artifacts: - path: nheko_win_64.zip - - path: nheko-installer.exe + - path: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe diff --git a/src/ChatPage.cc b/src/ChatPage.cc index cffb2a46..e543cdf9 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -1084,7 +1084,7 @@ ChatPage::trySync() if (!http::v2::is_logged_in()) return; - emit dropToLoginPageCb(msg); + emit tryDelayedSyncCb(); return; } @@ -1095,16 +1095,16 @@ ChatPage::trySync() emit trySyncCb(); return; } - case 401: - case 403: { + default: { if (!http::v2::is_logged_in()) return; - emit dropToLoginPageCb(msg); - return; - } - default: { - emit tryDelayedSyncCb(); + if (err->matrix_error.errcode == + mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) + emit dropToLoginPageCb(msg); + else + emit tryDelayedSyncCb(); + return; } } From 197b2dac748563f99de4a941a59e795dc759cca0 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sun, 17 Jun 2018 18:44:04 +0300 Subject: [PATCH 17/18] Fix regressions regarding invites & slowness during shutdown --- .ci/linux/deploy.sh | 9 +++------ deps/CMakeLists.txt | 2 +- include/timeline/TimelineView.h | 2 -- src/Cache.cc | 3 ++- src/main.cc | 3 ++- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 39fb0e30..7b5b8e4a 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -30,13 +30,10 @@ unset QTDIR unset QT_PLUGIN_PATH unset LD_LIBRARY_PATH -cp -R .deps/usr/lib/* ${DIR}/usr/lib +export ARCH=$(uname -m) -ldd ${DIR}/usr/bin/nheko - -./linuxdeployqt*.AppImage \ - ${DIR}/usr/share/applications/nheko.desktop \ - -appimage +./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -bundle-non-qt-libs +./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -appimage chmod +x nheko-x86_64.AppImage diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 34bcea31..06b1f7e1 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs) set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de) set(MTXCLIENT_URL https://github.com/mujx/mtxclient) -set(MTXCLIENT_TAG e45b1c85fce52f8ed516fd98a9cbb6a110a20d09) +set(MTXCLIENT_TAG 68188721e042ff5b47ea9a87aa97d3a9efbca989) set(OLM_URL https://git.matrix.org/git/olm.git) set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index a985f75b..a86c0286 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -362,8 +362,6 @@ TimelineView::addUserMessage(const QString &url, lastMessageDirection_ = TimelineDirection::Bottom; - QApplication::processEvents(); - // Keep track of the sender and the timestamp of the current message. saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); diff --git a/src/Cache.cc b/src/Cache.cc index 41a759ab..397dd05f 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -900,7 +900,8 @@ Cache::getRoomInfo(const std::vector &rooms) { std::map room_info; - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + // TODO This should be read only. + auto txn = lmdb::txn::begin(env_); for (const auto &room : rooms) { lmdb::val data; diff --git a/src/main.cc b/src/main.cc index 24524c2c..327ec587 100644 --- a/src/main.cc +++ b/src/main.cc @@ -166,8 +166,9 @@ main(int argc, char *argv[]) QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() { w.saveCurrentWindowSize(); if (http::v2::client() != nullptr) { + nhlog::net()->info("shutting down all I/O threads & open connections"); http::v2::client()->shutdown(); - http::v2::client()->close(); + http::v2::client()->close(true); } }); From 9884e02eb81498d383b19cc3c2c47e35ca6950da Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sun, 17 Jun 2018 19:10:48 +0300 Subject: [PATCH 18/18] Update build instructions --- README.md | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c71b5ed6..5e22f5fd 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IR Most of the features you would expect from a chat application are missing right now but we are getting close to a more feature complete client. Specifically there is support for: +- E2EE encryption. - User registration. - Creating, joining & leaving rooms. - Sending & receiving invites. @@ -29,20 +30,15 @@ Specifically there is support for: ## Installation -### Nightly releases -- Linux [AppImage](https://github.com/mujx/nheko/releases/download/nightly/nheko-x86_64.AppImage) -- Windows [x64 installer](https://github.com/mujx/nheko/releases/download/nightly/nheko-installer.exe) -- macOS [disk image](https://github.com/mujx/nheko/releases/download/nightly/nheko.dmg) +### Releases + +You can find releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer) on the [Bintray repo](https://bintray.com/mujx/matrix/nheko). ### Repositories #### Arch Linux ```bash -pacaur -S nheko-git - -# or - -pacaur -S nheko +pacaur -S nheko # nheko-git ``` #### Fedora @@ -69,7 +65,13 @@ sudo apk add nheko - Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with Freetype, which is essential to properly support emoji. - CMake 3.1 or greater. -- [LMDB](https://symas.com/lightning-memory-mapped-database/). +- [mtxclient](https://github.com/mujx/mtxclient) +- [matrix-structs](https://github.com/mujx/matrix-structs) +- [LMDB](https://symas.com/lightning-memory-mapped-database/) +- Boost 1.66 or greater. +- [libolm](https://git.matrix.org/git/olm) +- [libsodium](https://github.com/jedisct1/libsodium) +- [spdlog](https://github.com/gabime/spdlog) - A compiler that supports C++ 14: - Clang 5 (tested on Travis CI) - GCC 7 (tested on Travis CI) @@ -89,7 +91,16 @@ Debian as the build host in an attempt to work around this [issue](https://githu ##### Arch Linux ```bash -sudo pacman -S qt5-base qt5-tools qt5-multimedia qt5-svg cmake gcc fontconfig lmdb +sudo pacman -S qt5-base \ + qt5-tools \ + qt5-multimedia \ + qt5-svg \ + cmake \ + gcc \ + fontconfig \ + lmdb \ + boost \ + libsodium ``` ##### Gentoo Linux @@ -105,28 +116,31 @@ sudo add-apt-repository ppa:beineri/opt-qt592-trusty sudo add-apt-repository ppa:george-edison55/cmake-3.x sudo add-apt-repository ppa:ubuntu-toolchain-r-test sudo apt-get update -sudo apt-get install -y g++-7 qt59base qt59svg qt59tools qt59multimedia cmake liblmdb-dev +sudo apt-get install -y g++-7 qt59base qt59svg qt59tools qt59multimedia cmake liblmdb-dev libsodium-dev ``` ##### macOS (Xcode 8 or later) ```bash brew update -brew install qt5 lmdb cmake llvm +brew install qt5 lmdb cmake llvm libsodium spdlog boost ``` ### Building -Clone the repo and run +First we need to install the rest of the dependencies that are not available in our system ```bash -make release +cmake -Hdeps -B.deps \ + -DUSE_BUNDLED_BOOST=OFF # if we already have boost & spdlog installed. + -DUSE_BUNDLED_SPDLOG=OFF +cmake --build .deps ``` -which invokes cmake and translates to +We can now build nheko by pointing it to the path that we installed the dependencies. ```bash -cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=.deps/usr cmake --build build ``` @@ -143,7 +157,7 @@ You might need to pass `-DCMAKE_PREFIX_PATH` to cmake to point it at your qt5 in e.g on macOS ``` -cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_PREFIX_PATH=$(brew --prefix qt5) +cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt5) cmake --build build ```