View
@@ -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,14 +116,14 @@ 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
```
##### Windows
@@ -136,16 +147,19 @@ cd vcpkg
### 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
```
@@ -162,7 +176,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
```
View
@@ -14,8 +14,20 @@ 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
- 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
+ zlib:%PLATFORM%-windows
build_script:
# VERSION format: branch-master/branch-1.2
@@ -35,9 +47,16 @@ 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
+ -DUSE_BUNDLED_BOOST=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:
@@ -48,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\
@@ -96,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
View
@@ -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()
View
@@ -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 ""
-)
View
@@ -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 68188721e042ff5b47ea9a87aa97d3a9efbca989)
+
+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()
View
@@ -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)
View
@@ -0,0 +1,31 @@
+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
+ -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
+ ${PLATFORM_FLAGS}
+ ${DEPS_BUILD_DIR}/mtxclient
+ BUILD_COMMAND
+ ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/mtxclient --config Release)
+
+list(APPEND THIRD_PARTY_DEPS MatrixClient)
View
@@ -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)
View
@@ -0,0 +1,34 @@
+set(WINDOWS_FLAGS "")
+
+if(MSVC)
+ set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64")
+endif()
+
+ExternalProject_Add(
+ Olm
+
+ GIT_REPOSITORY ${OLM_URL}
+ GIT_TAG ${OLM_TAG}
+
+ BUILD_IN_SOURCE 1
+ SOURCE_DIR ${DEPS_BUILD_DIR}/olm
+ 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
+ ${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)
View
@@ -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
+ $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
+ $<INSTALL_INTERFACE:include>
+ 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)
View
@@ -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)
View
@@ -0,0 +1,22 @@
+set(WINDOWS_FLAGS "")
+
+if(MSVC)
+ set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64")
+endif()
+
+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}
+ -DSPDLOG_BUILD_EXAMPLES=0
+ -DSPDLOG_BUILD_TESTING=0
+ ${DEPS_BUILD_DIR}/spdlog
+ ${WINDOWS_FLAGS})
+
+list(APPEND THIRD_PARTY_DEPS SpdLog)
View
@@ -20,15 +20,17 @@
#include <QImage>
#include <functional>
-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<void(QImage)> callback);
+signals:
+ void avatarDownloaded(const QByteArray &data);
};
+
+using AvatarCallback = std::function<void(QImage)>;
+
+namespace AvatarProvider {
+void
+resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb);
+}
View
@@ -17,13 +17,18 @@
#pragma once
-#include <QDebug>
+#include <boost/optional.hpp>
+
#include <QDir>
#include <QImage>
+
#include <json.hpp>
#include <lmdb++.h>
#include <mtx/events/join_rules.hpp>
#include <mtx/responses.hpp>
+#include <mtxclient/crypto/client.hpp>
+#include <mutex>
+
using mtx::events::state::JoinRule;
struct RoomMember
@@ -140,6 +145,82 @@ 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
+{
+ // Megolm sessions
+ std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions;
+ std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions;
+ std::map<std::string, OutboundGroupSessionData> group_outbound_session_data;
+
+ // Guards for accessing megolm sessions.
+ std::mutex group_outbound_mtx;
+ std::mutex group_inbound_mtx;
+};
+
class Cache : public QObject
{
Q_OBJECT
@@ -192,7 +273,7 @@ class Cache : public QObject
void saveState(const mtx::responses::Sync &res);
bool isInitialized() const;
- QString nextBatchToken() const;
+ std::string nextBatchToken() const;
void deleteData();
@@ -206,6 +287,9 @@ class Cache : public QObject
bool isFormatValid();
void setCurrentFormat();
+ //! Retrieve all the user ids from a room.
+ std::vector<std::string> 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<mtx::events::EventType> &eventTypes,
@@ -237,6 +321,7 @@ class Cache : public QObject
{
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);
@@ -259,6 +344,51 @@ class Cache : public QObject
//! 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);
+ 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);
+
+ //! Save the device list for a user.
+ void setDeviceList(const std::string &user_id, const std::vector<std::string> &devices);
+ std::vector<std::string> getDeviceList(const std::string &user_id);
+
+ //
+ // Outbound Megolm Sessions
+ //
+ void saveOutboundMegolmSession(const std::string &room_id,
+ const OutboundGroupSessionData &data,
+ mtx::crypto::OutboundGroupSessionPtr session);
+ 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
+ //
+ void saveInboundMegolmSession(const MegolmSessionIndex &index,
+ mtx::crypto::InboundGroupSessionPtr session);
+ OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index);
+ bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept;
+
+ //
+ // Olm Sessions
+ //
+ void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session);
+ std::vector<std::string> getOlmSessions(const std::string &curve25519);
+ boost::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519,
+ const std::string &session_id);
+
+ void saveOlmAccount(const std::string &pickled);
+ std::string restoreOlmAccount();
+
+ void restoreSessions();
+
+ OlmSessionStorage session_storage;
+
private:
//! Save an invited room.
void saveInvite(lmdb::txn &txn,
@@ -431,6 +561,16 @@ class Cache : public QObject
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<mtx::events::state::Member> &event)
{
if (!event.content.display_name.empty())
@@ -450,6 +590,12 @@ class Cache : public QObject
lmdb::dbi readReceiptsDb_;
lmdb::dbi notificationsDb_;
+ lmdb::dbi devicesDb_;
+ lmdb::dbi deviceKeysDb_;
+
+ lmdb::dbi inboundMegolmSessionDb_;
+ lmdb::dbi outboundMegolmSessionDb_;
+
QString localUserId_;
QString cacheDirectory_;
};
View
@@ -17,6 +17,8 @@
#pragma once
+#include <atomic>
+
#include <QFrame>
#include <QHBoxLayout>
#include <QMap>
@@ -27,8 +29,7 @@
#include "Cache.h"
#include "CommunitiesList.h"
#include "Community.h"
-
-#include <mtx.hpp>
+#include "MatrixClient.h"
class OverlayModal;
class QuickSwitcher;
@@ -50,9 +51,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<std::string>)
-
class ChatPage : public QWidget
{
Q_OBJECT
@@ -71,7 +69,37 @@ class ChatPage : public QWidget
QSharedPointer<UserSettings> 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 +110,50 @@ class ChatPage : public QWidget
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 tryDelayedSyncCb();
+ void tryInitialSyncCb();
+ void leftRoom(const QString &room_id);
+
void initializeRoomList(QMap<QString, RoomInfo>);
void initializeViews(const mtx::responses::Rooms &rooms);
void initializeEmptyViews(const std::vector<std::string> &rooms);
void syncUI(const mtx::responses::Rooms &rooms);
- void continueSync(const QString &next_batch);
void syncRoomlist(const std::map<QString, RoomInfo> &updates);
void syncTopBar(const std::map<QString, RoomInfo> &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<QString> &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_;
+ //! 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<std::string, uint16_t> &counts);
+ void getProfileInfo();
+
//! Check if the given room is currently open.
bool isRoomActive(const QString &room_id)
{
@@ -161,8 +209,8 @@ private slots:
// 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_;
View
@@ -23,12 +23,14 @@ class CommunitiesList : public QWidget
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<Community>(new Community), "world"); }
//! Check whether or not a community id is currently managed.
View
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <memory>
+#include <spdlog/spdlog.h>
+
+namespace nhlog {
+void
+init(const std::string &file);
+
+std::shared_ptr<spdlog::logger>
+ui();
+
+std::shared_ptr<spdlog::logger>
+net();
+
+std::shared_ptr<spdlog::logger>
+db();
+
+std::shared_ptr<spdlog::logger>
+crypto();
+}
View
@@ -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 @@ class LoginPage : public QWidget
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_;
View
@@ -96,7 +96,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();
View
@@ -1,287 +1,28 @@
-/*
- * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
#pragma once
-#include <QFileInfo>
-#include <QJsonDocument>
-#include <QNetworkAccessManager>
-#include <QNetworkReply>
-#include <QNetworkRequest>
-#include <QUrl>
-#include <memory>
-#include <mtx.hpp>
-#include <mtx/errors.hpp>
-
-class DownloadMediaProxy : public QObject
-{
- Q_OBJECT
-
-signals:
- void imageDownloaded(const QPixmap &data);
- void fileDownloaded(const QByteArray &data);
- void avatarDownloaded(const QImage &img);
-};
+#include <QMetaType>
-class StateEventProxy : public QObject
-{
- Q_OBJECT
-
-signals:
- void stateEventSent();
- void stateEventError(const QString &msg);
-};
+#include <mtx/responses.hpp>
+#include <mtxclient/http/client.hpp>
+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)
+Q_DECLARE_METATYPE(std::string)
+Q_DECLARE_METATYPE(std::vector<std::string>)
-/*
- * 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<class EventBody, mtx::events::EventType EventT>
- std::shared_ptr<StateEventProxy> 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<DownloadMediaProxy> fetchUserAvatar(const QUrl &avatarUrl);
- void fetchCommunityAvatar(const QString &communityId, const QUrl &avatarUrl);
- void fetchCommunityProfile(const QString &communityId);
- void fetchCommunityRooms(const QString &communityId);
- QSharedPointer<DownloadMediaProxy> downloadImage(const QUrl &url);
- QSharedPointer<DownloadMediaProxy> 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<QIODevice> data);
- void uploadFile(const QString &roomid,
- const QString &filename,
- const QSharedPointer<QIODevice> data);
- void uploadAudio(const QString &roomid,
- const QString &filename,
- const QSharedPointer<QIODevice> data);
- void uploadVideo(const QString &roomid,
- const QString &filename,
- const QSharedPointer<QIODevice> 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<QString> &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 &notifications);
-
-private:
- QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> 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_;
+namespace http {
+namespace v2 {
+mtx::http::Client *
+client();
- //! 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_;
-};
+bool
+is_logged_in();
+}
-namespace http {
//! Initialize the http module
void
init();
-
-//! Retrieve the client instance.
-MatrixClient *
-client();
-}
-
-template<class EventBody, mtx::events::EventType EventT>
-std::shared_ptr<StateEventProxy>
-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<StateEventProxy>(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;
}
View
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <boost/optional.hpp>
+
+#include <memory>
+#include <mtx.hpp>
+#include <mtxclient/crypto/client.hpp>
+
+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<RecipientKey, OlmCipherContent> 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<std::map<std::string, OlmCipherContent>>();
+}
+
+mtx::crypto::OlmClient *
+client();
+
+void
+handle_to_device_messages(const std::vector<nlohmann::json> &msgs);
+
+boost::optional<json>
+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
+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,
+ 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
View
@@ -44,6 +44,11 @@ class RegisterPage : public QWidget
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();
View
@@ -60,6 +60,8 @@ class RoomList : public QWidget
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);
View
@@ -129,6 +129,16 @@ class TextInputWidget : public QWidget
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();
View
@@ -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;
View
@@ -5,16 +5,17 @@
#include "Cache.h"
+class Avatar;
class FlatButton;
-class TextField;
+class QComboBox;
class QHBoxLayout;
-class Avatar;
-class QPixmap;
-class QLayout;
class QLabel;
-class QComboBox;
-class TextField;
class QLabel;
+class QLayout;
+class QPixmap;
+class TextField;
+class TextField;
+class Toggle;
template<class T>
class QSharedPointer;
@@ -30,6 +31,9 @@ class EditModal : public QWidget
signals:
void nameChanged(const QString &roomName);
+ void nameEventSentCb(const QString &newName);
+ void topicEventSentCb();
+ void stateEventErrorCb(const QString &msg);
private:
QString roomId_;
@@ -81,6 +85,7 @@ class RoomSettings : public QFrame
signals:
void closing();
+ void enableEncryptionError(const QString &msg);
protected:
void paintEvent(QPaintEvent *event) override;
@@ -95,13 +100,15 @@ private slots:
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_;
@@ -113,6 +120,7 @@ private slots:
TopSection *topSection_;
QComboBox *accessCombo;
+ Toggle *encryptionToggle_;
};
} // dialogs
View
@@ -193,16 +193,17 @@ class TimelineItem : public QWidget
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
- {
- if (!event_id_.isEmpty())
- http::client()->readEvent(room_id_, event_id_);
- }
+ void sendReadReceipt() const;
//! 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;
@@ -225,6 +226,10 @@ class TimelineItem : public QWidget
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_;
View
@@ -18,7 +18,6 @@
#pragma once
#include <QApplication>
-#include <QDebug>
#include <QLayout>
#include <QList>
#include <QQueue>
@@ -34,6 +33,19 @@
#include "ScrollBar.h"
#include "TimelineItem.h"
+class StateKeeper
+{
+public:
+ StateKeeper(std::function<void()> &&fn)
+ : fn_(std::move(fn))
+ {}
+
+ ~StateKeeper() { fn_(); }
+
+private:
+ std::function<void()> fn_;
+};
+
class FloatingButton;
struct DescInfo;
@@ -42,33 +54,44 @@ 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)
- {}
+ bool is_encrypted = false;
};
+template<class MessageT>
+MessageT
+toRoomMessage(const PendingMessage &) = delete;
+
+template<>
+mtx::events::msg::Audio
+toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m);
+
+template<>
+mtx::events::msg::Emote
+toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m);
+
+template<>
+mtx::events::msg::File
+toRoomMessage<mtx::events::msg::File>(const PendingMessage &);
+
+template<>
+mtx::events::msg::Image
+toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m);
+
+template<>
+mtx::events::msg::Text
+toRoomMessage<mtx::events::msg::Text>(const PendingMessage &);
+
+template<>
+mtx::events::msg::Video
+toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m);
+
// In which place new TimelineItems should be inserted.
enum class TimelineDirection
{
@@ -129,7 +152,7 @@ class TimelineView : public QWidget
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 +165,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 +191,25 @@ private slots:
QWidget *relativeWidget(TimelineItem *item, int dt) const;
+ TimelineEvent parseEncryptedEvent(
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
+
+ void handleClaimedKeys(std::shared_ptr<StateKeeper> 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,
+ mtx::http::RequestErr err);
+ void prepareEncryptedMessage(const PendingMessage &msg);
+
+ //! 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 +275,10 @@ private slots:
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); }
@@ -315,14 +362,18 @@ 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());
- int txn_id = http::client()->incrementTransactionId();
+ PendingMessage message;
+ message.ty = MsgType;
+ message.txn_id = http::v2::client()->generate_txn_id();
+ 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 +402,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 +427,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;
}
View
@@ -56,6 +56,8 @@ class TimelineViewManager : public QStackedWidget
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(); }
View
@@ -69,9 +69,14 @@ class AudioItem : public QWidget
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
{
View
@@ -52,15 +52,20 @@ class FileItem : public QWidget
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_;
View
@@ -40,13 +40,17 @@ class ImageItem : public QWidget
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 @@ public slots:
bool isInteractive_ = true;
private:
+ void init();
void openUrl();
+ void downloadMedia(const QUrl &url);
int max_width_ = 500;
int max_height_ = 300;
View
@@ -16,17 +16,17 @@
*/
#include <QBuffer>
-#include <QtConcurrent>
+#include <memory>
#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<void(QImage)> 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<AvatarProxy>();
+ 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) {
+ nhlog::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);
+ });
+}
}
View

Large diffs are not rendered by default.

Oops, something went wrong.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -1,4 +1,6 @@
#include "CommunitiesList.h"
+#include "Cache.h"
+#include "Logging.hpp"
#include "MatrixClient.h"
#include <QLabel>
@@ -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::map<QString, QSharedPointer<Community
for (const auto &community : communities) {
addCommunity(community.second, community.first);
- http::client()->fetchCommunityProfile(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> community, const QString
communities_.emplace(community_id, QSharedPointer<CommunitiesListItem>(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,40 @@ 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;
+ }
+
+ if (avatarUrl.isEmpty())
+ 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) {
+ nhlog::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);
+ });
+}
View
@@ -0,0 +1,59 @@
+#include "Logging.hpp"
+
+#include <iostream>
+#include <spdlog/sinks/file_sinks.h>
+
+namespace {
+std::shared_ptr<spdlog::logger> db_logger = nullptr;
+std::shared_ptr<spdlog::logger> net_logger = nullptr;
+std::shared_ptr<spdlog::logger> crypto_logger = nullptr;
+std::shared_ptr<spdlog::logger> ui_logger = nullptr;
+
+constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
+constexpr auto MAX_LOG_FILES = 3;
+}
+
+namespace nhlog {
+void
+init(const std::string &file_path)
+{
+ auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
+ file_path, MAX_FILE_SIZE, MAX_LOG_FILES);
+
+ auto console_sink = std::make_shared<spdlog::sinks::stdout_sink_mt>();
+
+ std::vector<spdlog::sink_ptr> sinks;
+ sinks.push_back(file_sink);
+ sinks.push_back(console_sink);
+
+ net_logger = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks));
+ ui_logger = std::make_shared<spdlog::logger>("ui", std::begin(sinks), std::end(sinks));
+ db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
+ crypto_logger =
+ std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
+}
+
+std::shared_ptr<spdlog::logger>
+ui()
+{
+ return ui_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+net()
+{
+ return net_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+db()
+{
+ return db_logger;
+}
+
+std::shared_ptr<spdlog::logger>
+crypto()
+{
+ return crypto_logger;
+}
+}
View
@@ -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();
}
View
@@ -17,7 +17,6 @@
#include <QApplication>
#include <QLayout>
-#include <QNetworkReply>
#include <QSettings>
#include <QShortcut>
@@ -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"
@@ -54,9 +54,6 @@ MainWindow::MainWindow(QWidget *parent)
setWindowTitle("nheko");
setObjectName("MainWindow");
- // Initialize the http client.
- http::init();
-
restoreWindowSize();
QFont font("Open Sans");
@@ -124,21 +121,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);
@@ -156,8 +145,21 @@ 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;
+ http::v2::client()->set_user(parse<User>(user_id.toStdString()));
+ } catch (const std::invalid_argument &e) {
+ nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
+ user_id.toStdString());
+ }
- showChatPage(user_id, home_server, token);
+ showChatPage();
}
}
@@ -216,12 +218,19 @@ 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 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());
+
QSettings settings;
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();
@@ -317,7 +326,7 @@ MainWindow::openLeaveRoomDialog(const QString &room_id)
leaveRoomModal_->hide();
if (leaving)
- http::client()->leaveRoom(roomToLeave);
+ chat_page_->leaveRoom(roomToLeave);
});
leaveRoomModal_ =
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -0,0 +1,228 @@
+#include "Olm.hpp"
+
+#include "Cache.h"
+#include "Logging.hpp"
+
+using namespace mtx::crypto;
+
+namespace {
+auto client_ = std::make_unique<mtx::crypto::OlmClient>();
+}
+
+namespace olm {
+
+mtx::crypto::OlmClient *
+client()
+{
+ return client_.get();
+}
+
+void
+handle_to_device_messages(const std::vector<nlohmann::json> &msgs)
+{
+ if (msgs.empty())
+ return;
+
+ 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) {
+ nhlog::crypto()->warn(
+ "parsing error for olm message: {} {}", e.what(), msg.dump(2));
+ } catch (const std::invalid_argument &e) {
+ nhlog::crypto()->warn(
+ "validation error for olm message: {} {}", e.what(), msg.dump(2));
+ }
+ }
+}
+
+void
+handle_olm_message(const OlmMessage &msg)
+{
+ nhlog::crypto()->info("sender : {}", msg.sender);
+ nhlog::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;
+ nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
+
+ 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);
+ }
+}
+
+void
+handle_pre_key_olm_message(const std::string &sender,
+ const std::string &sender_key,
+ const OlmCipherContent &content)
+{
+ nhlog::crypto()->info("opening olm session with {}", 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());
+ return;
+ }
+
+ if (!matches_inbound_session_from(inbound_session.get(), sender_key, content.body)) {
+ nhlog::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(), content.type, content.body);
+ } catch (const olm_exception &e) {
+ nhlog::crypto()->critical(
+ "failed to decrypt olm message {}: {}", content.body, e.what());
+ return;
+ }
+
+ auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
+ nhlog::crypto()->info("decrypted message: \n {}", plaintext.dump(2));
+
+ try {
+ 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());
+ }
+
+ create_inbound_megolm_session(sender, sender_key, plaintext);
+}
+
+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);
+ 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);
+
+ return data;
+}
+
+boost::optional<json>
+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
View
@@ -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();
-
- if (!captchaDialog_) {
- captchaDialog_ =
- std::make_shared<dialogs::ReCaptcha>(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);
- });
- }
-
- QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); });
- });
+ 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<dialogs::ReCaptcha>(
+ QString::fromStdString(session), this);
+ connect(
+ captchaDialog_.get(),
+ &dialogs::ReCaptcha::closing,
+ this,
+ [this, user, pass, session]() {
+ captchaDialog_->close();
+ emit registering();
+
+ http::v2::client()->flow_response(
+ user,
+ pass,
+ session,
+ "m.login.recaptcha",
+ [this](const mtx::responses::Register &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::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) {
+ nhlog::net()->warn(
+ "failed to retrieve registration flows: ({}) "
+ "{}",
+ static_cast<int>(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;
+ }
+
+ nhlog::net()->warn("failed to register: status_code ({})",
+ static_cast<int>(err->status_code));
+
+ emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
+ emit errorOccurred();
+ });
- http::client()->registerUser(username, password, server);
emit registering();
}
}
Oops, something went wrong.