mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 03:00:46 +03:00
Merge branch 'e2ee'
- Support for e2ee rooms - Implement categories & file logging - Let the user know when the app can't reach the server (#93) fixes #13 fixes #326
This commit is contained in:
commit
8704265978
64 changed files with 3767 additions and 2672 deletions
37
.ci/bintray-release.json
Normal file
37
.ci/bintray-release.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,8 @@ set -ex
|
||||||
|
|
||||||
if [ $TRAVIS_OS_NAME == osx ]; then
|
if [ $TRAVIS_OS_NAME == osx ]; then
|
||||||
brew update
|
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
|
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||||
sudo python get-pip.py
|
sudo python get-pip.py
|
||||||
|
@ -23,6 +24,7 @@ if [ $TRAVIS_OS_NAME == linux ]; then
|
||||||
QT_PKG="59"
|
QT_PKG="59"
|
||||||
fi
|
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:beineri/opt-qt${QT_VERSION}-trusty
|
||||||
sudo add-apt-repository -y ppa:george-edison55/cmake-3.x
|
sudo add-apt-repository -y ppa:george-edison55/cmake-3.x
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
|
@ -32,5 +34,6 @@ if [ $TRAVIS_OS_NAME == linux ]; then
|
||||||
qt${QT_PKG}svg \
|
qt${QT_PKG}svg \
|
||||||
qt${QT_PKG}multimedia \
|
qt${QT_PKG}multimedia \
|
||||||
cmake \
|
cmake \
|
||||||
liblmdb-dev
|
liblmdb-dev \
|
||||||
|
libsodium-dev
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -7,7 +7,7 @@ DIR=${APP}.AppDir
|
||||||
TAG=`git tag -l --points-at HEAD`
|
TAG=`git tag -l --points-at HEAD`
|
||||||
|
|
||||||
# Set up AppImage structure.
|
# 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.
|
# Copy resources.
|
||||||
cp build/nheko ${DIR}/usr/bin
|
cp build/nheko ${DIR}/usr/bin
|
||||||
|
@ -30,9 +30,13 @@ unset QTDIR
|
||||||
unset QT_PLUGIN_PATH
|
unset QT_PLUGIN_PATH
|
||||||
unset LD_LIBRARY_PATH
|
unset LD_LIBRARY_PATH
|
||||||
|
|
||||||
./linuxdeployqt*.AppImage \
|
export ARCH=$(uname -m)
|
||||||
${DIR}/usr/share/applications/nheko.desktop \
|
|
||||||
-bundle-non-qt-libs\
|
./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -bundle-non-qt-libs
|
||||||
-appimage
|
./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -appimage
|
||||||
|
|
||||||
chmod +x nheko-x86_64.AppImage
|
chmod +x nheko-x86_64.AppImage
|
||||||
|
|
||||||
|
if [ ! -z $TRAVIS_TAG ]; then
|
||||||
|
mv nheko-x86_64.AppImage nheko-${TRAVIS_TAG}-x86_64.AppImage
|
||||||
|
fi
|
||||||
|
|
|
@ -15,3 +15,7 @@ mv nheko.dmg ..
|
||||||
popd
|
popd
|
||||||
|
|
||||||
dmgbuild -s ./.ci/macos/settings.json "Nheko" nheko.dmg
|
dmgbuild -s ./.ci/macos/settings.json "Nheko" nheko.dmg
|
||||||
|
|
||||||
|
if [ ! -z $TRAVIS_TAG ]; then
|
||||||
|
mv nheko.dmg nheko-${TRAVIS_TAG}.dmg
|
||||||
|
fi
|
||||||
|
|
|
@ -10,7 +10,15 @@ if [ $TRAVIS_OS_NAME == osx ]; then
|
||||||
export CMAKE_PREFIX_PATH=/usr/local/opt/qt5
|
export CMAKE_PREFIX_PATH=/usr/local/opt/qt5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
make ci
|
# Build & install dependencies
|
||||||
|
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
|
if [ $TRAVIS_OS_NAME == osx ]; then
|
||||||
make lint;
|
make lint;
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -99,3 +99,4 @@ package.dir
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
.third-party
|
.third-party
|
||||||
|
.deps
|
||||||
|
|
36
.travis.yml
36
.travis.yml
|
@ -14,6 +14,8 @@ matrix:
|
||||||
compiler: clang
|
compiler: clang
|
||||||
env:
|
env:
|
||||||
- DEPLOYMENT=1
|
- DEPLOYMENT=1
|
||||||
|
- USE_BUNDLED_BOOST=0
|
||||||
|
- USE_BUNDLED_SPDLOG=0
|
||||||
- os: linux
|
- os: linux
|
||||||
compiler: gcc
|
compiler: gcc
|
||||||
env:
|
env:
|
||||||
|
@ -22,6 +24,8 @@ matrix:
|
||||||
- QT_VERSION="-5.10.1"
|
- QT_VERSION="-5.10.1"
|
||||||
- QT_PKG=510
|
- QT_PKG=510
|
||||||
- DEPLOYMENT=1
|
- DEPLOYMENT=1
|
||||||
|
- USE_BUNDLED_BOOST=1
|
||||||
|
- USE_BUNDLED_SPDLOG=1
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources: ["ubuntu-toolchain-r-test"]
|
sources: ["ubuntu-toolchain-r-test"]
|
||||||
|
@ -33,6 +37,8 @@ matrix:
|
||||||
- C_COMPILER=gcc-7
|
- C_COMPILER=gcc-7
|
||||||
- QT_VERSION=571
|
- QT_VERSION=571
|
||||||
- QT_PKG=57
|
- QT_PKG=57
|
||||||
|
- USE_BUNDLED_BOOST=1
|
||||||
|
- USE_BUNDLED_SPDLOG=1
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources: ["ubuntu-toolchain-r-test"]
|
sources: ["ubuntu-toolchain-r-test"]
|
||||||
|
@ -44,6 +50,8 @@ matrix:
|
||||||
- C_COMPILER=clang-5.0
|
- C_COMPILER=clang-5.0
|
||||||
- QT_VERSION=592
|
- QT_VERSION=592
|
||||||
- QT_PKG=59
|
- QT_PKG=59
|
||||||
|
- USE_BUNDLED_BOOST=1
|
||||||
|
- USE_BUNDLED_SPDLOG=1
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources: ["ubuntu-toolchain-r-test", "llvm-toolchain-trusty-5.0"]
|
sources: ["ubuntu-toolchain-r-test", "llvm-toolchain-trusty-5.0"]
|
||||||
|
@ -58,28 +66,18 @@ install:
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./.ci/script.sh
|
- ./.ci/script.sh
|
||||||
|
- sed -i -e "s/VERSION_NAME_VALUE/${TRAVIS_TAG}/g" ./.ci/bintray-release.json || true
|
||||||
|
- cp ./.ci/bintray-release.json .
|
||||||
|
|
||||||
deploy:
|
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
|
overwrite: true
|
||||||
provider: releases
|
file: "bintray-release.json"
|
||||||
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
|
|
||||||
on:
|
on:
|
||||||
condition: $TRAVIS_OS_NAME == linux && $DEPLOYMENT == 1
|
condition: $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
|
|
||||||
repo: mujx/nheko
|
repo: mujx/nheko
|
||||||
tags: true
|
tags: true
|
||||||
|
|
|
@ -29,6 +29,15 @@ set(IDENTIFIER "com.github.mujx.nheko")
|
||||||
|
|
||||||
add_project_meta(META_FILES_TO_INCLUDE)
|
add_project_meta(META_FILES_TO_INCLUDE)
|
||||||
|
|
||||||
|
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
|
# LMDB
|
||||||
#
|
#
|
||||||
|
@ -37,7 +46,7 @@ include(LMDB)
|
||||||
#
|
#
|
||||||
# Discover Qt dependencies.
|
# Discover Qt dependencies.
|
||||||
#
|
#
|
||||||
find_package(Qt5 COMPONENTS Core Widgets Network LinguistTools Concurrent Svg Multimedia REQUIRED)
|
find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED)
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
find_package(Qt5MacExtras REQUIRED)
|
find_package(Qt5MacExtras REQUIRED)
|
||||||
|
@ -60,7 +69,20 @@ endif(NOT MSVC)
|
||||||
set(CMAKE_CXX_STANDARD 14)
|
set(CMAKE_CXX_STANDARD 14)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR 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 OR CMAKE_CONFIGURATION_TYPES))
|
if(NOT (CMAKE_BUILD_TYPE OR CMAKE_CONFIGURATION_TYPES))
|
||||||
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING
|
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING
|
||||||
|
@ -151,9 +173,11 @@ set(SRC_FILES
|
||||||
src/Community.cc
|
src/Community.cc
|
||||||
src/InviteeItem.cc
|
src/InviteeItem.cc
|
||||||
src/LoginPage.cc
|
src/LoginPage.cc
|
||||||
|
src/Logging.cpp
|
||||||
src/MainWindow.cc
|
src/MainWindow.cc
|
||||||
src/MatrixClient.cc
|
src/MatrixClient.cc
|
||||||
src/QuickSwitcher.cc
|
src/QuickSwitcher.cc
|
||||||
|
src/Olm.cpp
|
||||||
src/RegisterPage.cc
|
src/RegisterPage.cc
|
||||||
src/RoomInfoListItem.cc
|
src/RoomInfoListItem.cc
|
||||||
src/RoomList.cc
|
src/RoomList.cc
|
||||||
|
@ -175,20 +199,12 @@ set(SRC_FILES
|
||||||
# ExternalProject dependencies
|
# ExternalProject dependencies
|
||||||
set(EXTERNAL_PROJECT_DEPS "")
|
set(EXTERNAL_PROJECT_DEPS "")
|
||||||
|
|
||||||
#
|
find_package(ZLIB REQUIRED)
|
||||||
# matrix-structs
|
find_package(OpenSSL REQUIRED)
|
||||||
#
|
find_package(MatrixStructs 0.1.0 REQUIRED)
|
||||||
find_library(MATRIX_STRUCTS_LIBRARY
|
find_package(MatrixClient 0.1.0 REQUIRED)
|
||||||
NAMES matrix_structs
|
find_package(Olm 2 REQUIRED)
|
||||||
PATHS ${MATRIX_STRUCTS_ROOT}
|
find_package(spdlog 0.16.0 CONFIG REQUIRED)
|
||||||
${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})
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# tweeny
|
# tweeny
|
||||||
|
@ -267,7 +283,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||||
include/LoginPage.h
|
include/LoginPage.h
|
||||||
include/MainWindow.h
|
include/MainWindow.h
|
||||||
include/InviteeItem.h
|
include/InviteeItem.h
|
||||||
include/MatrixClient.h
|
|
||||||
include/QuickSwitcher.h
|
include/QuickSwitcher.h
|
||||||
include/RegisterPage.h
|
include/RegisterPage.h
|
||||||
include/RoomInfoListItem.h
|
include/RoomInfoListItem.h
|
||||||
|
@ -291,11 +306,12 @@ include(Translations)
|
||||||
set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
|
set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
|
||||||
|
|
||||||
set(COMMON_LIBS
|
set(COMMON_LIBS
|
||||||
${MATRIX_STRUCTS_LIBRARY}
|
MatrixStructs::MatrixStructs
|
||||||
|
MatrixClient::MatrixClient
|
||||||
Qt5::Widgets
|
Qt5::Widgets
|
||||||
Qt5::Network
|
|
||||||
Qt5::Svg
|
Qt5::Svg
|
||||||
Qt5::Concurrent)
|
Qt5::Concurrent
|
||||||
|
Qt5::Multimedia)
|
||||||
|
|
||||||
if(APPVEYOR_BUILD)
|
if(APPVEYOR_BUILD)
|
||||||
set(NHEKO_LIBS ${COMMON_LIBS} lmdb)
|
set(NHEKO_LIBS ${COMMON_LIBS} lmdb)
|
||||||
|
@ -325,13 +341,13 @@ endif()
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
|
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)
|
elseif(WIN32)
|
||||||
add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS})
|
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()
|
else()
|
||||||
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
|
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
|
||||||
target_link_libraries (nheko ${NHEKO_LIBS} Qt5::Multimedia)
|
target_link_libraries (nheko ${NHEKO_LIBS})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(EXTERNAL_PROJECT_DEPS)
|
if(EXTERNAL_PROJECT_DEPS)
|
||||||
|
|
25
Makefile
25
Makefile
|
@ -1,14 +1,31 @@
|
||||||
|
DEPS_BUILD_DIR=.deps
|
||||||
|
DEPS_SOURCE_DIR=deps
|
||||||
|
|
||||||
debug:
|
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
|
@cmake --build build
|
||||||
|
|
||||||
|
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:
|
ci:
|
||||||
@cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
cmake -H${DEPS_SOURCE_DIR} -B${DEPS_BUILD_DIR} -DCMAKE_BUILD_TYPE=Release
|
||||||
@cmake --build build
|
cmake --build ${DEPS_BUILD_DIR}
|
||||||
|
cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||||
|
cmake --build build
|
||||||
|
|
||||||
release:
|
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
|
@cmake --build build
|
||||||
|
|
||||||
linux-install:
|
linux-install:
|
||||||
|
|
50
README.md
50
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
|
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.
|
but we are getting close to a more feature complete client.
|
||||||
Specifically there is support for:
|
Specifically there is support for:
|
||||||
|
- E2EE encryption.
|
||||||
- User registration.
|
- User registration.
|
||||||
- Creating, joining & leaving rooms.
|
- Creating, joining & leaving rooms.
|
||||||
- Sending & receiving invites.
|
- Sending & receiving invites.
|
||||||
|
@ -29,20 +30,15 @@ Specifically there is support for:
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Nightly releases
|
### 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)
|
You can find releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer) on the [Bintray repo](https://bintray.com/mujx/matrix/nheko).
|
||||||
- macOS [disk image](https://github.com/mujx/nheko/releases/download/nightly/nheko.dmg)
|
|
||||||
|
|
||||||
### Repositories
|
### Repositories
|
||||||
|
|
||||||
#### Arch Linux
|
#### Arch Linux
|
||||||
```bash
|
```bash
|
||||||
pacaur -S nheko-git
|
pacaur -S nheko # nheko-git
|
||||||
|
|
||||||
# or
|
|
||||||
|
|
||||||
pacaur -S nheko
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Fedora
|
#### Fedora
|
||||||
|
@ -69,7 +65,13 @@ sudo apk add nheko
|
||||||
- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with
|
- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with
|
||||||
Freetype, which is essential to properly support emoji.
|
Freetype, which is essential to properly support emoji.
|
||||||
- CMake 3.1 or greater.
|
- 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:
|
- A compiler that supports C++ 14:
|
||||||
- Clang 5 (tested on Travis CI)
|
- Clang 5 (tested on Travis CI)
|
||||||
- GCC 7 (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
|
##### Arch Linux
|
||||||
|
|
||||||
```bash
|
```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
|
##### 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:george-edison55/cmake-3.x
|
||||||
sudo add-apt-repository ppa:ubuntu-toolchain-r-test
|
sudo add-apt-repository ppa:ubuntu-toolchain-r-test
|
||||||
sudo apt-get update
|
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)
|
##### macOS (Xcode 8 or later)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew update
|
brew update
|
||||||
brew install qt5 lmdb cmake llvm
|
brew install qt5 lmdb cmake llvm libsodium spdlog boost
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Windows
|
##### Windows
|
||||||
|
@ -136,16 +147,19 @@ cd vcpkg
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
Clone the repo and run
|
First we need to install the rest of the dependencies that are not available in our system
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=.deps/usr
|
||||||
cmake --build build
|
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
|
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
|
cmake --build build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
52
appveyor.yml
52
appveyor.yml
|
@ -14,8 +14,20 @@ build:
|
||||||
install:
|
install:
|
||||||
- set QT_DIR=C:\Qt\5.10.1\msvc2017_64
|
- set QT_DIR=C:\Qt\5.10.1\msvc2017_64
|
||||||
- set PATH=%PATH%;%QT_DIR%\bin;C:\MinGW\bin
|
- 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"
|
- 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:
|
build_script:
|
||||||
# VERSION format: branch-master/branch-1.2
|
# VERSION format: branch-master/branch-1.2
|
||||||
|
@ -35,9 +47,16 @@ build_script:
|
||||||
- echo %VERSION%
|
- echo %VERSION%
|
||||||
- echo %INSTVERSION%
|
- echo %INSTVERSION%
|
||||||
- echo %DATE%
|
- 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
|
- cmake -G "Visual Studio 15 2017 Win64" -H. -Bbuild
|
||||||
-DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake
|
-DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake
|
||||||
-DCMAKE_BUILD_TYPE=Release
|
|
||||||
- cmake --build build --config Release
|
- cmake --build build --config Release
|
||||||
|
|
||||||
after_build:
|
after_build:
|
||||||
|
@ -48,12 +67,9 @@ after_build:
|
||||||
- copy build\Release\nheko.exe NhekoRelease\nheko.exe
|
- copy build\Release\nheko.exe NhekoRelease\nheko.exe
|
||||||
- windeployqt --qmldir %QT_DIR%\qml\ --release 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\lib\*.lib .\NhekoRelease\
|
||||||
- copy C:\Tools\vcpkg\installed\x64-windows\bin\lmdb.dll .\NhekoRelease\lmdb.dll
|
- 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\*
|
- 7z a nheko_win_64.zip .\NhekoRelease\*
|
||||||
- ls -lh build\Release\
|
- ls -lh build\Release\
|
||||||
- ls -lh NhekoRelease\
|
- ls -lh NhekoRelease\
|
||||||
|
@ -96,17 +112,23 @@ after_build:
|
||||||
- set PATH=%BUILD%\tools\bin;%PATH%
|
- set PATH=%BUILD%\tools\bin;%PATH%
|
||||||
- binarycreator.exe -f -c installer\config\config.xml -p installer\packages nheko-installer.exe
|
- 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:
|
deploy:
|
||||||
description: "Development builds"
|
provider: BinTray
|
||||||
provider: GitHub
|
username: mujx
|
||||||
auth_token:
|
api_key:
|
||||||
secure: YqB7hcM+4482eSHhtVR7ZA7N7lE78y8BC897/7UDTBQd+NWdWFW/6S+oKDie9TT7
|
secure: "hhhAH6csIrPEVH92NNQkiGCkuON6l6sfhbZk+pvzDAM3vHex7YbqFKW6v5UjAS8v"
|
||||||
artifact: nheko-installer.exe
|
subject: mujx
|
||||||
force_update: true
|
repo: matrix
|
||||||
prerelease: true
|
package: nheko
|
||||||
|
version: $(APPVEYOR_REPO_TAG_NAME)
|
||||||
|
publish: true
|
||||||
|
override: true
|
||||||
|
artifact: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe
|
||||||
on:
|
on:
|
||||||
appveyor_repo_tag: true
|
appveyor_repo_tag: true
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
- path: nheko_win_64.zip
|
- path: nheko_win_64.zip
|
||||||
- path: nheko-installer.exe
|
- path: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe
|
||||||
|
|
|
@ -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()
|
|
|
@ -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 ""
|
|
||||||
)
|
|
82
deps/CMakeLists.txt
vendored
Normal file
82
deps/CMakeLists.txt
vendored
Normal file
|
@ -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()
|
23
deps/cmake/Boost.cmake
vendored
Normal file
23
deps/cmake/Boost.cmake
vendored
Normal file
|
@ -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)
|
31
deps/cmake/MatrixClient.cmake
vendored
Normal file
31
deps/cmake/MatrixClient.cmake
vendored
Normal file
|
@ -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)
|
25
deps/cmake/MatrixStructs.cmake
vendored
Normal file
25
deps/cmake/MatrixStructs.cmake
vendored
Normal file
|
@ -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)
|
34
deps/cmake/Olm.cmake
vendored
Normal file
34
deps/cmake/Olm.cmake
vendored
Normal file
|
@ -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)
|
107
deps/cmake/OlmCMakeLists.txt
vendored
Normal file
107
deps/cmake/OlmCMakeLists.txt
vendored
Normal file
|
@ -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)
|
11
deps/cmake/OlmConfig.cmake.in
vendored
Normal file
11
deps/cmake/OlmConfig.cmake.in
vendored
Normal file
|
@ -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)
|
22
deps/cmake/SpdLog.cmake
vendored
Normal file
22
deps/cmake/SpdLog.cmake
vendored
Normal file
|
@ -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)
|
|
@ -20,15 +20,17 @@
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
class AvatarProvider : public QObject
|
class AvatarProxy : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
signals:
|
||||||
//! The callback is called with the downloaded avatar for the given user
|
void avatarDownloaded(const QByteArray &data);
|
||||||
//! 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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using AvatarCallback = std::function<void(QImage)>;
|
||||||
|
|
||||||
|
namespace AvatarProvider {
|
||||||
|
void
|
||||||
|
resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb);
|
||||||
|
}
|
||||||
|
|
150
include/Cache.h
150
include/Cache.h
|
@ -17,13 +17,18 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QDebug>
|
#include <boost/optional.hpp>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
|
||||||
#include <json.hpp>
|
#include <json.hpp>
|
||||||
#include <lmdb++.h>
|
#include <lmdb++.h>
|
||||||
#include <mtx/events/join_rules.hpp>
|
#include <mtx/events/join_rules.hpp>
|
||||||
#include <mtx/responses.hpp>
|
#include <mtx/responses.hpp>
|
||||||
|
#include <mtxclient/crypto/client.hpp>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
using mtx::events::state::JoinRule;
|
using mtx::events::state::JoinRule;
|
||||||
|
|
||||||
struct RoomMember
|
struct RoomMember
|
||||||
|
@ -140,6 +145,82 @@ struct RoomSearchResult
|
||||||
Q_DECLARE_METATYPE(RoomSearchResult)
|
Q_DECLARE_METATYPE(RoomSearchResult)
|
||||||
Q_DECLARE_METATYPE(RoomInfo)
|
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
|
class Cache : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -192,7 +273,7 @@ public:
|
||||||
void saveState(const mtx::responses::Sync &res);
|
void saveState(const mtx::responses::Sync &res);
|
||||||
bool isInitialized() const;
|
bool isInitialized() const;
|
||||||
|
|
||||||
QString nextBatchToken() const;
|
std::string nextBatchToken() const;
|
||||||
|
|
||||||
void deleteData();
|
void deleteData();
|
||||||
|
|
||||||
|
@ -206,6 +287,9 @@ public:
|
||||||
bool isFormatValid();
|
bool isFormatValid();
|
||||||
void setCurrentFormat();
|
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
|
//! Check if the given user has power leve greater than than
|
||||||
//! lowest power level of the given events.
|
//! lowest power level of the given events.
|
||||||
bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
|
bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
|
||||||
|
@ -237,6 +321,7 @@ public:
|
||||||
{
|
{
|
||||||
return image(QString::fromStdString(url));
|
return image(QString::fromStdString(url));
|
||||||
}
|
}
|
||||||
|
void saveImage(const std::string &url, const std::string &data);
|
||||||
void saveImage(const QString &url, const QByteArray &data);
|
void saveImage(const QString &url, const QByteArray &data);
|
||||||
|
|
||||||
RoomInfo singleRoomInfo(const std::string &room_id);
|
RoomInfo singleRoomInfo(const std::string &room_id);
|
||||||
|
@ -259,6 +344,51 @@ public:
|
||||||
//! Check if we have sent a desktop notification for the given event id.
|
//! Check if we have sent a desktop notification for the given event id.
|
||||||
bool isNotificationSent(const std::string &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:
|
private:
|
||||||
//! Save an invited room.
|
//! Save an invited room.
|
||||||
void saveInvite(lmdb::txn &txn,
|
void saveInvite(lmdb::txn &txn,
|
||||||
|
@ -431,6 +561,16 @@ private:
|
||||||
return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
|
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)
|
QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event)
|
||||||
{
|
{
|
||||||
if (!event.content.display_name.empty())
|
if (!event.content.display_name.empty())
|
||||||
|
@ -450,6 +590,12 @@ private:
|
||||||
lmdb::dbi readReceiptsDb_;
|
lmdb::dbi readReceiptsDb_;
|
||||||
lmdb::dbi notificationsDb_;
|
lmdb::dbi notificationsDb_;
|
||||||
|
|
||||||
|
lmdb::dbi devicesDb_;
|
||||||
|
lmdb::dbi deviceKeysDb_;
|
||||||
|
|
||||||
|
lmdb::dbi inboundMegolmSessionDb_;
|
||||||
|
lmdb::dbi outboundMegolmSessionDb_;
|
||||||
|
|
||||||
QString localUserId_;
|
QString localUserId_;
|
||||||
QString cacheDirectory_;
|
QString cacheDirectory_;
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
@ -27,8 +29,7 @@
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "CommunitiesList.h"
|
#include "CommunitiesList.h"
|
||||||
#include "Community.h"
|
#include "Community.h"
|
||||||
|
#include "MatrixClient.h"
|
||||||
#include <mtx.hpp>
|
|
||||||
|
|
||||||
class OverlayModal;
|
class OverlayModal;
|
||||||
class QuickSwitcher;
|
class QuickSwitcher;
|
||||||
|
@ -50,9 +51,6 @@ constexpr int CONSENSUS_TIMEOUT = 1000;
|
||||||
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
|
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
|
||||||
constexpr int TYPING_REFRESH_TIMEOUT = 10000;
|
constexpr int TYPING_REFRESH_TIMEOUT = 10000;
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(mtx::responses::Rooms)
|
|
||||||
Q_DECLARE_METATYPE(std::vector<std::string>)
|
|
||||||
|
|
||||||
class ChatPage : public QWidget
|
class ChatPage : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -71,7 +69,37 @@ public:
|
||||||
QSharedPointer<UserSettings> userSettings() { return userSettings_; }
|
QSharedPointer<UserSettings> userSettings() { return userSettings_; }
|
||||||
void deleteConfigs();
|
void deleteConfigs();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void leaveRoom(const QString &room_id);
|
||||||
|
|
||||||
signals:
|
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 contentLoaded();
|
||||||
void closing();
|
void closing();
|
||||||
void changeWindowTitle(const QString &msg);
|
void changeWindowTitle(const QString &msg);
|
||||||
|
@ -82,30 +110,50 @@ signals:
|
||||||
void showOverlayProgressBar();
|
void showOverlayProgressBar();
|
||||||
void startConsesusTimer();
|
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 initializeRoomList(QMap<QString, RoomInfo>);
|
||||||
void initializeViews(const mtx::responses::Rooms &rooms);
|
void initializeViews(const mtx::responses::Rooms &rooms);
|
||||||
void initializeEmptyViews(const std::vector<std::string> &rooms);
|
void initializeEmptyViews(const std::vector<std::string> &rooms);
|
||||||
void syncUI(const mtx::responses::Rooms &rooms);
|
void syncUI(const mtx::responses::Rooms &rooms);
|
||||||
void continueSync(const QString &next_batch);
|
|
||||||
void syncRoomlist(const std::map<QString, RoomInfo> &updates);
|
void syncRoomlist(const std::map<QString, RoomInfo> &updates);
|
||||||
void syncTopBar(const std::map<QString, RoomInfo> &updates);
|
void syncTopBar(const std::map<QString, RoomInfo> &updates);
|
||||||
|
void dropToLoginPageCb(const QString &msg);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void showUnreadMessageNotification(int count);
|
void showUnreadMessageNotification(int count);
|
||||||
void updateTopBarAvatar(const QString &roomid, const QPixmap &img);
|
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 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 changeTopRoomInfo(const QString &room_id);
|
||||||
void logout();
|
void logout();
|
||||||
void removeRoom(const QString &room_id);
|
void removeRoom(const QString &room_id);
|
||||||
//! Handles initial sync failures.
|
void dropToLoginPage(const QString &msg);
|
||||||
void retryInitialSync(int status_code = -1);
|
|
||||||
|
void joinRoom(const QString &room);
|
||||||
|
void createRoom(const mtx::requests::CreateRoom &req);
|
||||||
|
void sendTypingNotifications();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static ChatPage *instance_;
|
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.
|
//! Check if the given room is currently open.
|
||||||
bool isRoomActive(const QString &room_id)
|
bool isRoomActive(const QString &room_id)
|
||||||
{
|
{
|
||||||
|
@ -161,8 +209,8 @@ private:
|
||||||
// Safety net if consensus is not possible or too slow.
|
// Safety net if consensus is not possible or too slow.
|
||||||
QTimer *showContentTimer_;
|
QTimer *showContentTimer_;
|
||||||
QTimer *consensusTimer_;
|
QTimer *consensusTimer_;
|
||||||
QTimer *syncTimeoutTimer_;
|
QTimer connectivityTimer_;
|
||||||
QTimer *initialSyncTimer_;
|
std::atomic_bool isConnected_;
|
||||||
|
|
||||||
QString current_room_;
|
QString current_room_;
|
||||||
QString current_community_;
|
QString current_community_;
|
||||||
|
|
|
@ -23,12 +23,14 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void communityChanged(const QString &id);
|
void communityChanged(const QString &id);
|
||||||
|
void avatarRetrieved(const QString &id, const QPixmap &img);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void updateCommunityAvatar(const QString &id, const QPixmap &img);
|
void updateCommunityAvatar(const QString &id, const QPixmap &img);
|
||||||
void highlightSelectedCommunity(const QString &id);
|
void highlightSelectedCommunity(const QString &id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void fetchCommunityAvatar(const QString &id, const QString &avatarUrl);
|
||||||
void addGlobalItem() { addCommunity(QSharedPointer<Community>(new Community), "world"); }
|
void addGlobalItem() { addCommunity(QSharedPointer<Community>(new Community), "world"); }
|
||||||
|
|
||||||
//! Check whether or not a community id is currently managed.
|
//! Check whether or not a community id is currently managed.
|
||||||
|
|
21
include/Logging.hpp
Normal file
21
include/Logging.hpp
Normal file
|
@ -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();
|
||||||
|
}
|
|
@ -28,6 +28,12 @@ class OverlayModal;
|
||||||
class RaisedButton;
|
class RaisedButton;
|
||||||
class TextField;
|
class TextField;
|
||||||
|
|
||||||
|
namespace mtx {
|
||||||
|
namespace responses {
|
||||||
|
struct Login;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LoginPage : public QWidget
|
class LoginPage : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -42,12 +48,19 @@ signals:
|
||||||
void loggingIn();
|
void loggingIn();
|
||||||
void errorOccurred();
|
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:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
// Displays errors produced during the login.
|
// 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:
|
private slots:
|
||||||
// Callback for the back button.
|
// Callback for the back button.
|
||||||
|
@ -63,13 +76,25 @@ private slots:
|
||||||
void onServerAddressEntered();
|
void onServerAddressEntered();
|
||||||
|
|
||||||
// Callback for errors produced during server probing
|
// Callback for errors produced during server probing
|
||||||
void versionError(QString error_message);
|
void versionError(const QString &error_message);
|
||||||
|
|
||||||
// Callback for successful server probing
|
// Callback for successful server probing
|
||||||
void versionSuccess();
|
void versionOk();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isMatrixIdValid();
|
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_;
|
QVBoxLayout *top_layout_;
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ private slots:
|
||||||
void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); }
|
void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); }
|
||||||
|
|
||||||
//! Show the chat page and start communicating with the given access token.
|
//! 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 showOverlayProgressBar();
|
||||||
void removeOverlayProgressBar();
|
void removeOverlayProgressBar();
|
||||||
|
|
|
@ -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
|
#pragma once
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QMetaType>
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QNetworkRequest>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <memory>
|
|
||||||
#include <mtx.hpp>
|
|
||||||
#include <mtx/errors.hpp>
|
|
||||||
|
|
||||||
class DownloadMediaProxy : public QObject
|
#include <mtx/responses.hpp>
|
||||||
{
|
#include <mtxclient/http/client.hpp>
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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(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 ¬ifications);
|
|
||||||
|
|
||||||
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_;
|
|
||||||
|
|
||||||
//! 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_;
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace http {
|
namespace http {
|
||||||
|
namespace v2 {
|
||||||
|
mtx::http::Client *
|
||||||
|
client();
|
||||||
|
|
||||||
|
bool
|
||||||
|
is_logged_in();
|
||||||
|
}
|
||||||
|
|
||||||
//! Initialize the http module
|
//! Initialize the http module
|
||||||
void
|
void
|
||||||
init();
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
78
include/Olm.hpp
Normal file
78
include/Olm.hpp
Normal file
|
@ -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
|
|
@ -44,6 +44,11 @@ signals:
|
||||||
void backButtonClicked();
|
void backButtonClicked();
|
||||||
void errorOccurred();
|
void errorOccurred();
|
||||||
void registering();
|
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:
|
private slots:
|
||||||
void onBackButtonClicked();
|
void onBackButtonClicked();
|
||||||
|
|
|
@ -60,6 +60,8 @@ signals:
|
||||||
void acceptInvite(const QString &room_id);
|
void acceptInvite(const QString &room_id);
|
||||||
void declineInvite(const QString &room_id);
|
void declineInvite(const QString &room_id);
|
||||||
void roomAvatarChanged(const QString &room_id, const QPixmap &img);
|
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:
|
public slots:
|
||||||
void updateRoomAvatar(const QString &roomid, const QPixmap &img);
|
void updateRoomAvatar(const QString &roomid, const QPixmap &img);
|
||||||
|
|
|
@ -129,6 +129,16 @@ public:
|
||||||
|
|
||||||
QColor borderColor() const { return borderColor_; }
|
QColor borderColor() const { return borderColor_; }
|
||||||
void setBorderColor(QColor &color) { borderColor_ = color; }
|
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:
|
public slots:
|
||||||
void openFileSelection();
|
void openFileSelection();
|
||||||
|
|
|
@ -12,7 +12,7 @@ class ReCaptcha : public QWidget
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ReCaptcha(const QString &server, const QString &session, QWidget *parent = nullptr);
|
ReCaptcha(const QString &session, QWidget *parent = nullptr);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
|
@ -5,16 +5,17 @@
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
|
|
||||||
class FlatButton;
|
|
||||||
class TextField;
|
|
||||||
class QHBoxLayout;
|
|
||||||
class Avatar;
|
class Avatar;
|
||||||
class QPixmap;
|
class FlatButton;
|
||||||
class QLayout;
|
|
||||||
class QLabel;
|
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class TextField;
|
class QHBoxLayout;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
class QLabel;
|
||||||
|
class QLayout;
|
||||||
|
class QPixmap;
|
||||||
|
class TextField;
|
||||||
|
class TextField;
|
||||||
|
class Toggle;
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
class QSharedPointer;
|
class QSharedPointer;
|
||||||
|
@ -30,6 +31,9 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void nameChanged(const QString &roomName);
|
void nameChanged(const QString &roomName);
|
||||||
|
void nameEventSentCb(const QString &newName);
|
||||||
|
void topicEventSentCb();
|
||||||
|
void stateEventErrorCb(const QString &msg);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString roomId_;
|
QString roomId_;
|
||||||
|
@ -81,6 +85,7 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void closing();
|
void closing();
|
||||||
|
void enableEncryptionError(const QString &msg);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
@ -95,13 +100,15 @@ private:
|
||||||
void setupEditButton();
|
void setupEditButton();
|
||||||
//! Retrieve the current room information from cache.
|
//! Retrieve the current room information from cache.
|
||||||
void retrieveRoomInfo();
|
void retrieveRoomInfo();
|
||||||
|
void enableEncryption();
|
||||||
|
|
||||||
//! Whether the user would be able to change the name or the topic of the room.
|
//! 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_;
|
QHBoxLayout *editLayout_;
|
||||||
|
|
||||||
// Button section
|
// Button section
|
||||||
FlatButton *saveBtn_;
|
FlatButton *okBtn_;
|
||||||
FlatButton *cancelBtn_;
|
FlatButton *cancelBtn_;
|
||||||
|
|
||||||
FlatButton *editFieldsBtn_;
|
FlatButton *editFieldsBtn_;
|
||||||
|
@ -113,6 +120,7 @@ private:
|
||||||
TopSection *topSection_;
|
TopSection *topSection_;
|
||||||
|
|
||||||
QComboBox *accessCombo;
|
QComboBox *accessCombo;
|
||||||
|
Toggle *encryptionToggle_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // dialogs
|
} // dialogs
|
||||||
|
|
|
@ -193,16 +193,17 @@ public:
|
||||||
QString eventId() const { return event_id_; }
|
QString eventId() const { return event_id_; }
|
||||||
void setEventId(const QString &event_id) { event_id_ = event_id; }
|
void setEventId(const QString &event_id) { event_id_ = event_id; }
|
||||||
void markReceived();
|
void markReceived();
|
||||||
|
bool isReceived() { return isReceived_; };
|
||||||
void setRoomId(QString room_id) { room_id_ = room_id; }
|
void setRoomId(QString room_id) { room_id_ = room_id; }
|
||||||
void sendReadReceipt() const
|
void sendReadReceipt() const;
|
||||||
{
|
|
||||||
if (!event_id_.isEmpty())
|
|
||||||
http::client()->readEvent(room_id_, event_id_);
|
|
||||||
}
|
|
||||||
|
|
||||||
//! Add a user avatar for this event.
|
//! Add a user avatar for this event.
|
||||||
void addAvatar();
|
void addAvatar();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void eventRedacted(const QString &event_id);
|
||||||
|
void redactionFailed(const QString &msg);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||||
|
@ -225,6 +226,10 @@ private:
|
||||||
void setupAvatarLayout(const QString &userName);
|
void setupAvatarLayout(const QString &userName);
|
||||||
void setupSimpleLayout();
|
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 replaceEmoji(const QString &body);
|
||||||
QString event_id_;
|
QString event_id_;
|
||||||
QString room_id_;
|
QString room_id_;
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDebug>
|
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QQueue>
|
#include <QQueue>
|
||||||
|
@ -34,6 +33,19 @@
|
||||||
#include "ScrollBar.h"
|
#include "ScrollBar.h"
|
||||||
#include "TimelineItem.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;
|
class FloatingButton;
|
||||||
struct DescInfo;
|
struct DescInfo;
|
||||||
|
|
||||||
|
@ -42,33 +54,44 @@ struct DescInfo;
|
||||||
struct PendingMessage
|
struct PendingMessage
|
||||||
{
|
{
|
||||||
mtx::events::MessageType ty;
|
mtx::events::MessageType ty;
|
||||||
int txn_id;
|
std::string txn_id;
|
||||||
QString body;
|
QString body;
|
||||||
QString filename;
|
QString filename;
|
||||||
QString mime;
|
QString mime;
|
||||||
uint64_t media_size;
|
uint64_t media_size;
|
||||||
QString event_id;
|
QString event_id;
|
||||||
TimelineItem *widget;
|
TimelineItem *widget;
|
||||||
|
bool is_encrypted = false;
|
||||||
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)
|
|
||||||
{}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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.
|
// In which place new TimelineItems should be inserted.
|
||||||
enum class TimelineDirection
|
enum class TimelineDirection
|
||||||
{
|
{
|
||||||
|
@ -129,7 +152,7 @@ public:
|
||||||
const QString &filename,
|
const QString &filename,
|
||||||
const QString &mime,
|
const QString &mime,
|
||||||
uint64_t size);
|
uint64_t size);
|
||||||
void updatePendingMessage(int txn_id, QString event_id);
|
void updatePendingMessage(const std::string &txn_id, const QString &event_id);
|
||||||
void scrollDown();
|
void scrollDown();
|
||||||
QLabel *createDateSeparator(QDateTime datetime);
|
QLabel *createDateSeparator(QDateTime datetime);
|
||||||
|
|
||||||
|
@ -142,18 +165,21 @@ public slots:
|
||||||
void fetchHistory();
|
void fetchHistory();
|
||||||
|
|
||||||
// Add old events at the top of the timeline.
|
// 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.
|
// Whether or not the initial batch has been loaded.
|
||||||
bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; }
|
bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; }
|
||||||
|
|
||||||
void handleFailedMessage(int txnid);
|
void handleFailedMessage(const std::string &txn_id);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void sendNextPendingMessage();
|
void sendNextPendingMessage();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void updateLastTimelineMessage(const QString &user, const DescInfo &info);
|
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:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
@ -165,6 +191,25 @@ private:
|
||||||
|
|
||||||
QWidget *relativeWidget(TimelineItem *item, int dt) const;
|
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
|
//! HACK: Fixing layout flickering when adding to the bottom
|
||||||
//! of the timeline.
|
//! of the timeline.
|
||||||
void pushTimelineItem(TimelineItem *item)
|
void pushTimelineItem(TimelineItem *item)
|
||||||
|
@ -230,8 +275,10 @@ private:
|
||||||
uint64_t origin_server_ts,
|
uint64_t origin_server_ts,
|
||||||
TimelineDirection direction);
|
TimelineDirection direction);
|
||||||
|
|
||||||
bool isPendingMessage(const QString &txnid, const QString &sender, const QString &userid);
|
bool isPendingMessage(const std::string &txn_id,
|
||||||
void removePendingMessage(const QString &txnid);
|
const QString &sender,
|
||||||
|
const QString &userid);
|
||||||
|
void removePendingMessage(const std::string &txn_id);
|
||||||
|
|
||||||
bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); }
|
bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); }
|
||||||
|
|
||||||
|
@ -315,14 +362,18 @@ TimelineView::addUserMessage(const QString &url,
|
||||||
|
|
||||||
lastMessageDirection_ = TimelineDirection::Bottom;
|
lastMessageDirection_ = TimelineDirection::Bottom;
|
||||||
|
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
// Keep track of the sender and the timestamp of the current message.
|
// Keep track of the sender and the timestamp of the current message.
|
||||||
saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
|
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);
|
handleNewUserMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,10 +402,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
|
||||||
const auto event_id = QString::fromStdString(event.event_id);
|
const auto event_id = QString::fromStdString(event.event_id);
|
||||||
const auto sender = QString::fromStdString(event.sender);
|
const auto sender = QString::fromStdString(event.sender);
|
||||||
|
|
||||||
const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
|
const auto txn_id = event.unsigned_data.transaction_id;
|
||||||
if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
|
if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
|
||||||
isDuplicate(event_id)) {
|
isDuplicate(event_id)) {
|
||||||
removePendingMessage(txnid);
|
removePendingMessage(txn_id);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,10 +427,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
|
||||||
const auto event_id = QString::fromStdString(event.event_id);
|
const auto event_id = QString::fromStdString(event.event_id);
|
||||||
const auto sender = QString::fromStdString(event.sender);
|
const auto sender = QString::fromStdString(event.sender);
|
||||||
|
|
||||||
const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
|
const auto txn_id = event.unsigned_data.transaction_id;
|
||||||
if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
|
if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
|
||||||
isDuplicate(event_id)) {
|
isDuplicate(event_id)) {
|
||||||
removePendingMessage(txnid);
|
removePendingMessage(txn_id);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,8 @@ signals:
|
||||||
void updateRoomsLastMessage(const QString &user, const DescInfo &info);
|
void updateRoomsLastMessage(const QString &user, const DescInfo &info);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
void removeTimelineEvent(const QString &room_id, const QString &event_id);
|
||||||
|
|
||||||
void setHistoryView(const QString &room_id);
|
void setHistoryView(const QString &room_id);
|
||||||
void queueTextMessage(const QString &msg);
|
void queueTextMessage(const QString &msg);
|
||||||
void queueEmoteMessage(const QString &msg);
|
void queueEmoteMessage(const QString &msg);
|
||||||
|
@ -80,10 +82,6 @@ public slots:
|
||||||
const QString &mime,
|
const QString &mime,
|
||||||
uint64_t dsize);
|
uint64_t dsize);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void messageSent(const QString &eventid, const QString &roomid, int txnid);
|
|
||||||
void messageSendFailed(const QString &roomid, int txnid);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
//! Check if the given room id is managed by a TimelineView.
|
//! Check if the given room id is managed by a TimelineView.
|
||||||
bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }
|
bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }
|
||||||
|
|
|
@ -69,9 +69,14 @@ protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileDownloadedCb(const QByteArray &data);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void fileDownloaded(const QByteArray &data);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void init();
|
void init();
|
||||||
void fileDownloaded(const QByteArray &data);
|
|
||||||
|
|
||||||
enum class AudioState
|
enum class AudioState
|
||||||
{
|
{
|
||||||
|
|
|
@ -52,15 +52,20 @@ public:
|
||||||
QColor iconColor() const { return iconColor_; }
|
QColor iconColor() const { return iconColor_; }
|
||||||
QColor backgroundColor() const { return backgroundColor_; }
|
QColor backgroundColor() const { return backgroundColor_; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileDownloadedCb(const QByteArray &data);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void fileDownloaded(const QByteArray &data);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void openUrl();
|
void openUrl();
|
||||||
void init();
|
void init();
|
||||||
void fileDownloaded(const QByteArray &data);
|
|
||||||
|
|
||||||
QUrl url_;
|
QUrl url_;
|
||||||
QString text_;
|
QString text_;
|
||||||
|
|
|
@ -40,13 +40,17 @@ public:
|
||||||
uint64_t size,
|
uint64_t size,
|
||||||
QWidget *parent = nullptr);
|
QWidget *parent = nullptr);
|
||||||
|
|
||||||
void setImage(const QPixmap &image);
|
|
||||||
|
|
||||||
QSize sizeHint() const override;
|
QSize sizeHint() const override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
//! Show a save as dialog for the image.
|
//! Show a save as dialog for the image.
|
||||||
void saveAs();
|
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:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
@ -57,7 +61,9 @@ protected:
|
||||||
bool isInteractive_ = true;
|
bool isInteractive_ = true;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void init();
|
||||||
void openUrl();
|
void openUrl();
|
||||||
|
void downloadMedia(const QUrl &url);
|
||||||
|
|
||||||
int max_width_ = 500;
|
int max_width_ = 500;
|
||||||
int max_height_ = 300;
|
int max_height_ = 300;
|
||||||
|
|
|
@ -16,17 +16,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QtConcurrent>
|
#include <memory>
|
||||||
|
|
||||||
#include "AvatarProvider.h"
|
#include "AvatarProvider.h"
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
|
|
||||||
|
namespace AvatarProvider {
|
||||||
|
|
||||||
void
|
void
|
||||||
AvatarProvider::resolve(const QString &room_id,
|
resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback)
|
||||||
const QString &user_id,
|
|
||||||
QObject *receiver,
|
|
||||||
std::function<void(QImage)> callback)
|
|
||||||
{
|
{
|
||||||
const auto key = QString("%1 %2").arg(room_id).arg(user_id);
|
const auto key = QString("%1 %2").arg(room_id).arg(user_id);
|
||||||
const auto avatarUrl = Cache::avatarUrl(room_id, user_id);
|
const auto avatarUrl = Cache::avatarUrl(room_id, user_id);
|
||||||
|
@ -43,24 +43,30 @@ AvatarProvider::resolve(const QString &room_id,
|
||||||
return;
|
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())
|
mtx::http::ThumbOpts opts;
|
||||||
return;
|
opts.mxc_url = avatarUrl.toStdString();
|
||||||
|
|
||||||
connect(proxy.data(),
|
http::v2::client()->get_thumbnail(
|
||||||
&DownloadMediaProxy::avatarDownloaded,
|
opts,
|
||||||
receiver,
|
[opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) {
|
||||||
[user_id, proxy, callback, avatarUrl](const QImage &img) {
|
if (err) {
|
||||||
proxy->deleteLater();
|
nhlog::net()->warn("failed to download avatar: {} - ({} {})",
|
||||||
QtConcurrent::run([img, avatarUrl]() {
|
opts.mxc_url,
|
||||||
QByteArray data;
|
mtx::errors::to_string(err->matrix_error.errcode),
|
||||||
QBuffer buffer(&data);
|
err->matrix_error.error);
|
||||||
buffer.open(QIODevice::WriteOnly);
|
return;
|
||||||
img.save(&buffer, "PNG");
|
}
|
||||||
|
|
||||||
cache::client()->saveImage(avatarUrl, data);
|
cache::client()->saveImage(opts.mxc_url, res);
|
||||||
});
|
|
||||||
callback(img);
|
auto data = QByteArray(res.data(), res.size());
|
||||||
});
|
emit proxy->avatarDownloaded(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
469
src/Cache.cc
469
src/Cache.cc
|
@ -19,7 +19,6 @@
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QDebug>
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
@ -27,29 +26,46 @@
|
||||||
#include <variant.hpp>
|
#include <variant.hpp>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
//! Should be changed when a breaking change occurs in the cache format.
|
//! Should be changed when a breaking change occurs in the cache format.
|
||||||
//! This will reset client's data.
|
//! 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 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");
|
static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version");
|
||||||
|
|
||||||
//! Cache databases and their format.
|
//! Cache databases and their format.
|
||||||
//!
|
//!
|
||||||
//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc).
|
//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc).
|
||||||
//! Format: room_id -> RoomInfo
|
//! Format: room_id -> RoomInfo
|
||||||
static constexpr const char *ROOMS_DB = "rooms";
|
constexpr auto ROOMS_DB("rooms");
|
||||||
static constexpr const char *INVITES_DB = "invites";
|
constexpr auto INVITES_DB("invites");
|
||||||
//! Keeps already downloaded media for reuse.
|
//! Keeps already downloaded media for reuse.
|
||||||
//! Format: matrix_url -> binary data.
|
//! 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.
|
//! 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.
|
//! Read receipts per room/event.
|
||||||
static constexpr const char *READ_RECEIPTS_DB = "read_receipts";
|
constexpr auto READ_RECEIPTS_DB("read_receipts");
|
||||||
static constexpr const char *NOTIFICATIONS_DB = "sent_notifications";
|
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");
|
||||||
|
|
||||||
|
//! 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");
|
||||||
|
|
||||||
using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
|
using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
|
||||||
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
|
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
|
||||||
|
@ -62,8 +78,15 @@ namespace cache {
|
||||||
void
|
void
|
||||||
init(const QString &user_id)
|
init(const QString &user_id)
|
||||||
{
|
{
|
||||||
if (!instance_)
|
qRegisterMetaType<SearchResult>();
|
||||||
instance_ = std::make_unique<Cache>(user_id);
|
qRegisterMetaType<QVector<SearchResult>>();
|
||||||
|
qRegisterMetaType<RoomMember>();
|
||||||
|
qRegisterMetaType<RoomSearchResult>();
|
||||||
|
qRegisterMetaType<RoomInfo>();
|
||||||
|
qRegisterMetaType<QMap<QString, RoomInfo>>();
|
||||||
|
qRegisterMetaType<std::map<QString, RoomInfo>>();
|
||||||
|
|
||||||
|
instance_ = std::make_unique<Cache>(user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache *
|
Cache *
|
||||||
|
@ -71,7 +94,7 @@ client()
|
||||||
{
|
{
|
||||||
return instance_.get();
|
return instance_.get();
|
||||||
}
|
}
|
||||||
}
|
} // namespace cache
|
||||||
|
|
||||||
Cache::Cache(const QString &userId, QObject *parent)
|
Cache::Cache(const QString &userId, QObject *parent)
|
||||||
: QObject{parent}
|
: QObject{parent}
|
||||||
|
@ -82,15 +105,21 @@ Cache::Cache(const QString &userId, QObject *parent)
|
||||||
, mediaDb_{0}
|
, mediaDb_{0}
|
||||||
, readReceiptsDb_{0}
|
, readReceiptsDb_{0}
|
||||||
, notificationsDb_{0}
|
, notificationsDb_{0}
|
||||||
|
, devicesDb_{0}
|
||||||
|
, deviceKeysDb_{0}
|
||||||
|
, inboundMegolmSessionDb_{0}
|
||||||
|
, outboundMegolmSessionDb_{0}
|
||||||
, localUserId_{userId}
|
, localUserId_{userId}
|
||||||
{}
|
{
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Cache::setup()
|
Cache::setup()
|
||||||
{
|
{
|
||||||
qDebug() << "Setting up cache";
|
nhlog::db()->debug("setting up cache");
|
||||||
|
|
||||||
auto statePath = QString("%1/%2/state")
|
auto statePath = QString("%1/%2")
|
||||||
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
|
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
|
||||||
.arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
|
.arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
|
||||||
|
|
||||||
|
@ -105,7 +134,7 @@ Cache::setup()
|
||||||
env_.set_max_dbs(1024UL);
|
env_.set_max_dbs(1024UL);
|
||||||
|
|
||||||
if (isInitial) {
|
if (isInitial) {
|
||||||
qDebug() << "First time initializing LMDB";
|
nhlog::db()->info("initializing LMDB");
|
||||||
|
|
||||||
if (!QDir().mkpath(statePath)) {
|
if (!QDir().mkpath(statePath)) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
|
@ -121,7 +150,7 @@ Cache::setup()
|
||||||
std::string(e.what()));
|
std::string(e.what()));
|
||||||
}
|
}
|
||||||
|
|
||||||
qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what();
|
nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
|
||||||
|
|
||||||
QDir stateDir(statePath);
|
QDir stateDir(statePath);
|
||||||
|
|
||||||
|
@ -141,30 +170,315 @@ Cache::setup()
|
||||||
mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
|
mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
|
||||||
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
|
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
|
||||||
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
|
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
|
||||||
txn.commit();
|
|
||||||
|
|
||||||
qRegisterMetaType<RoomInfo>();
|
// 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);
|
||||||
|
|
||||||
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Cache::saveImage(const QString &url, const QByteArray &image)
|
Cache::setEncryptedRoom(const std::string &room_id)
|
||||||
{
|
{
|
||||||
auto key = url.toUtf8();
|
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);
|
||||||
|
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
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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<InboundSessionObject>(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<std::mutex> lock(session_storage.group_inbound_mtx);
|
||||||
|
session_storage.group_inbound_sessions[key] = std::move(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OlmInboundGroupSession *
|
||||||
|
Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> 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<std::mutex> lock(session_storage.group_inbound_mtx);
|
||||||
|
return session_storage.group_inbound_sessions.find(index.to_hash()) !=
|
||||||
|
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<std::mutex> 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<OutboundSessionObject>(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,
|
||||||
|
mtx::crypto::OutboundGroupSessionPtr session)
|
||||||
|
{
|
||||||
|
using namespace mtx::crypto;
|
||||||
|
const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET);
|
||||||
|
|
||||||
|
json j;
|
||||||
|
j["data"] = data;
|
||||||
|
j["session"] = pickled;
|
||||||
|
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump()));
|
||||||
|
txn.commit();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
|
||||||
|
session_storage.group_outbound_session_data[room_id] = data;
|
||||||
|
session_storage.group_outbound_sessions[room_id] = std::move(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
|
||||||
|
return (session_storage.group_outbound_sessions.find(room_id) !=
|
||||||
|
session_storage.group_outbound_sessions.end()) &&
|
||||||
|
(session_storage.group_outbound_session_data.find(room_id) !=
|
||||||
|
session_storage.group_outbound_session_data.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
OutboundGroupSessionDataRef
|
||||||
|
Cache::getOutboundMegolmSession(const std::string &room_id)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
|
||||||
|
return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(),
|
||||||
|
session_storage.group_outbound_session_data[room_id]};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// OLM sessions.
|
||||||
|
//
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session)
|
||||||
|
{
|
||||||
|
using namespace mtx::crypto;
|
||||||
|
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
auto db = getOlmSessionsDb(txn, curve25519);
|
||||||
|
|
||||||
|
const auto pickled = pickle<SessionObject>(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<mtx::crypto::OlmSessionPtr>
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
auto data = std::string(pickled.data(), pickled.size());
|
||||||
|
return unpickle<SessionObject>(data, SECRET);
|
||||||
|
}
|
||||||
|
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
Cache::getOlmSessions(const std::string &curve25519)
|
||||||
|
{
|
||||||
|
using namespace mtx::crypto;
|
||||||
|
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
auto db = getOlmSessionsDb(txn, curve25519);
|
||||||
|
|
||||||
|
std::string session_id, unused;
|
||||||
|
std::vector<std::string> 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
|
||||||
|
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<InboundSessionObject>(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<OutboundGroupSessionData>();
|
||||||
|
|
||||||
|
auto session =
|
||||||
|
unpickle<OutboundSessionObject>(obj.at("session"), SECRET);
|
||||||
|
session_storage.group_outbound_sessions[key] = std::move(session);
|
||||||
|
} catch (const nlohmann::json::exception &e) {
|
||||||
|
nhlog::db()->critical(
|
||||||
|
"failed to parse outbound megolm session data: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.commit();
|
||||||
|
|
||||||
|
nhlog::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)
|
||||||
|
{
|
||||||
|
if (url.empty() || img_data.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto txn = lmdb::txn::begin(env_);
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
|
||||||
lmdb::dbi_put(txn,
|
lmdb::dbi_put(txn,
|
||||||
mediaDb_,
|
mediaDb_,
|
||||||
lmdb::val(key.data(), key.size()),
|
lmdb::val(url.data(), url.size()),
|
||||||
lmdb::val(image.data(), image.size()));
|
lmdb::val(img_data.data(), img_data.size()));
|
||||||
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
qCritical() << "saveImage:" << e.what();
|
nhlog::db()->critical("saveImage: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::saveImage(const QString &url, const QByteArray &image)
|
||||||
|
{
|
||||||
|
saveImage(url.toStdString(), std::string(image.constData(), image.length()));
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
Cache::image(lmdb::txn &txn, const std::string &url) const
|
Cache::image(lmdb::txn &txn, const std::string &url) const
|
||||||
{
|
{
|
||||||
|
@ -180,7 +494,7 @@ Cache::image(lmdb::txn &txn, const std::string &url) const
|
||||||
|
|
||||||
return QByteArray(image.data(), image.size());
|
return QByteArray(image.data(), image.size());
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
qCritical() << "image:" << e.what() << QString::fromStdString(url);
|
nhlog::db()->critical("image: {}, {}", e.what(), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QByteArray();
|
return QByteArray();
|
||||||
|
@ -208,7 +522,7 @@ Cache::image(const QString &url) const
|
||||||
|
|
||||||
return QByteArray(image.data(), image.size());
|
return QByteArray(image.data(), image.size());
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
qCritical() << "image:" << e.what() << url;
|
nhlog::db()->critical("image: {} {}", e.what(), url.toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return QByteArray();
|
return QByteArray();
|
||||||
|
@ -271,7 +585,7 @@ Cache::isInitialized() const
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString
|
std::string
|
||||||
Cache::nextBatchToken() const
|
Cache::nextBatchToken() const
|
||||||
{
|
{
|
||||||
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
||||||
|
@ -281,16 +595,17 @@ Cache::nextBatchToken() const
|
||||||
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
|
|
||||||
return QString::fromUtf8(token.data(), token.size());
|
return std::string(token.data(), token.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Cache::deleteData()
|
Cache::deleteData()
|
||||||
{
|
{
|
||||||
qInfo() << "Deleting cache data";
|
// TODO: We need to remove the env_ while not accepting new requests.
|
||||||
|
if (!cacheDirectory_.isEmpty()) {
|
||||||
if (!cacheDirectory_.isEmpty())
|
|
||||||
QDir(cacheDirectory_).removeRecursively();
|
QDir(cacheDirectory_).removeRecursively();
|
||||||
|
nhlog::db()->info("deleted cache files from disk");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -304,13 +619,14 @@ Cache::isFormatValid()
|
||||||
txn.commit();
|
txn.commit();
|
||||||
|
|
||||||
if (!res)
|
if (!res)
|
||||||
return false;
|
return true;
|
||||||
|
|
||||||
std::string stored_version(current_version.data(), current_version.size());
|
std::string stored_version(current_version.data(), current_version.size());
|
||||||
|
|
||||||
if (stored_version != CURRENT_CACHE_FORMAT_VERSION) {
|
if (stored_version != CURRENT_CACHE_FORMAT_VERSION) {
|
||||||
qWarning() << "Stored format version" << QString::fromStdString(stored_version);
|
nhlog::db()->warn("breaking changes in the cache format. stored: {}, current: {}",
|
||||||
qWarning() << "There are breaking changes in the cache format.";
|
stored_version,
|
||||||
|
CURRENT_CACHE_FORMAT_VERSION);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,7 +676,7 @@ Cache::readReceipts(const QString &event_id, const QString &room_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
qCritical() << "readReceipts:" << e.what();
|
nhlog::db()->critical("readReceipts: {}", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
return receipts;
|
return receipts;
|
||||||
|
@ -410,7 +726,7 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei
|
||||||
lmdb::val(merged_receipts.data(), merged_receipts.size()));
|
lmdb::val(merged_receipts.data(), merged_receipts.size()));
|
||||||
|
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
qCritical() << "updateReadReceipts:" << e.what();
|
nhlog::db()->critical("updateReadReceipts: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -568,9 +884,9 @@ Cache::singleRoomInfo(const std::string &room_id)
|
||||||
|
|
||||||
return tmp;
|
return tmp;
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning()
|
nhlog::db()->warn("failed to parse room info: room_id ({}), {}",
|
||||||
<< "failed to parse room info:" << QString::fromStdString(room_id)
|
room_id,
|
||||||
<< QString::fromStdString(std::string(data.data(), data.size()));
|
std::string(data.data(), data.size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -584,7 +900,8 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
|
||||||
{
|
{
|
||||||
std::map<QString, RoomInfo> room_info;
|
std::map<QString, RoomInfo> 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) {
|
for (const auto &room : rooms) {
|
||||||
lmdb::val data;
|
lmdb::val data;
|
||||||
|
@ -600,9 +917,9 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
|
||||||
|
|
||||||
room_info.emplace(QString::fromStdString(room), std::move(tmp));
|
room_info.emplace(QString::fromStdString(room), std::move(tmp));
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning()
|
nhlog::db()->warn("failed to parse room info: room_id ({}), {}",
|
||||||
<< "failed to parse room info:" << QString::fromStdString(room)
|
room,
|
||||||
<< QString::fromStdString(std::string(data.data(), data.size()));
|
std::string(data.data(), data.size()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if the room is an invite.
|
// Check if the room is an invite.
|
||||||
|
@ -615,10 +932,10 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
|
||||||
room_info.emplace(QString::fromStdString(room),
|
room_info.emplace(QString::fromStdString(room),
|
||||||
std::move(tmp));
|
std::move(tmp));
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << "failed to parse room info for invite:"
|
nhlog::db()->warn(
|
||||||
<< QString::fromStdString(room)
|
"failed to parse room info for invite: room_id ({}), {}",
|
||||||
<< QString::fromStdString(
|
room,
|
||||||
std::string(data.data(), data.size()));
|
std::string(data.data(), data.size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -703,7 +1020,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
|
||||||
|
|
||||||
return QString::fromStdString(msg.content.url);
|
return QString::fromStdString(msg.content.url);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -726,7 +1043,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
|
||||||
cursor.close();
|
cursor.close();
|
||||||
return QString::fromStdString(m.avatar_url);
|
return QString::fromStdString(m.avatar_url);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse member info: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -753,7 +1070,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
|
||||||
if (!msg.content.name.empty())
|
if (!msg.content.name.empty())
|
||||||
return QString::fromStdString(msg.content.name);
|
return QString::fromStdString(msg.content.name);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,7 +1085,8 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
|
||||||
if (!msg.content.alias.empty())
|
if (!msg.content.alias.empty())
|
||||||
return QString::fromStdString(msg.content.alias);
|
return QString::fromStdString(msg.content.alias);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
|
||||||
|
e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -784,7 +1102,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
|
||||||
try {
|
try {
|
||||||
members.emplace(user_id, json::parse(member_data));
|
members.emplace(user_id, json::parse(member_data));
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse member info: {}", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
ii++;
|
ii++;
|
||||||
|
@ -828,7 +1146,7 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
|
||||||
json::parse(std::string(event.data(), event.size()));
|
json::parse(std::string(event.data(), event.size()));
|
||||||
return msg.content.join_rule;
|
return msg.content.join_rule;
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << e.what();
|
nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return JoinRule::Knock;
|
return JoinRule::Knock;
|
||||||
|
@ -850,7 +1168,8 @@ Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
|
||||||
json::parse(std::string(event.data(), event.size()));
|
json::parse(std::string(event.data(), event.size()));
|
||||||
return msg.content.guest_access == AccessState::CanJoin;
|
return msg.content.guest_access == AccessState::CanJoin;
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << e.what();
|
nhlog::db()->warn("failed to parse m.room.guest_access event: {}",
|
||||||
|
e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -874,7 +1193,7 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
|
||||||
if (!msg.content.topic.empty())
|
if (!msg.content.topic.empty())
|
||||||
return QString::fromStdString(msg.content.topic);
|
return QString::fromStdString(msg.content.topic);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -897,7 +1216,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
|
||||||
json::parse(std::string(event.data(), event.size()));
|
json::parse(std::string(event.data(), event.size()));
|
||||||
return QString::fromStdString(msg.content.name);
|
return QString::fromStdString(msg.content.name);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -914,7 +1233,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
|
||||||
|
|
||||||
return QString::fromStdString(tmp.name);
|
return QString::fromStdString(tmp.name);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse member info: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -939,7 +1258,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
|
||||||
json::parse(std::string(event.data(), event.size()));
|
json::parse(std::string(event.data(), event.size()));
|
||||||
return QString::fromStdString(msg.content.url);
|
return QString::fromStdString(msg.content.url);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -956,7 +1275,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
|
||||||
|
|
||||||
return QString::fromStdString(tmp.avatar_url);
|
return QString::fromStdString(tmp.avatar_url);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse member info: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -981,7 +1300,7 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
|
||||||
json::parse(std::string(event.data(), event.size()));
|
json::parse(std::string(event.data(), event.size()));
|
||||||
return QString::fromStdString(msg.content.topic);
|
return QString::fromStdString(msg.content.topic);
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << QString::fromStdString(e.what());
|
nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1017,8 +1336,9 @@ Cache::getRoomAvatar(const std::string &room_id)
|
||||||
return QImage();
|
return QImage();
|
||||||
}
|
}
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << "failed to parse room info" << e.what()
|
nhlog::db()->warn("failed to parse room info: {}, {}",
|
||||||
<< QString::fromStdString(std::string(response.data(), response.size()));
|
e.what(),
|
||||||
|
std::string(response.data(), response.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) {
|
if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) {
|
||||||
|
@ -1054,7 +1374,7 @@ void
|
||||||
Cache::populateMembers()
|
Cache::populateMembers()
|
||||||
{
|
{
|
||||||
auto rooms = joinedRooms();
|
auto rooms = joinedRooms();
|
||||||
qDebug() << "loading" << rooms.size() << "rooms";
|
nhlog::db()->info("loading {} rooms", rooms.size());
|
||||||
|
|
||||||
auto txn = lmdb::txn::begin(env_);
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
|
||||||
|
@ -1182,7 +1502,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_
|
||||||
QString::fromStdString(tmp.name),
|
QString::fromStdString(tmp.name),
|
||||||
QImage::fromData(image(txn, tmp.avatar_url))});
|
QImage::fromData(image(txn, tmp.avatar_url))});
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << e.what();
|
nhlog::db()->warn("{}", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
currentIndex += 1;
|
currentIndex += 1;
|
||||||
|
@ -1253,7 +1573,8 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
|
||||||
std::min(min_event_level,
|
std::min(min_event_level,
|
||||||
(uint16_t)msg.content.state_level(to_string(ty)));
|
(uint16_t)msg.content.state_level(to_string(ty)));
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
qWarning() << "hasEnoughPowerLevel: " << e.what();
|
nhlog::db()->warn("failed to parse m.room.power_levels event: {}",
|
||||||
|
e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1262,6 +1583,26 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
|
||||||
return user_level >= min_event_level;
|
return user_level >= min_event_level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
Cache::roomMembers(const std::string &room_id)
|
||||||
|
{
|
||||||
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
||||||
|
|
||||||
|
std::vector<std::string> 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<QString, QString> Cache::DisplayNames;
|
QHash<QString, QString> Cache::DisplayNames;
|
||||||
QHash<QString, QString> Cache::AvatarUrls;
|
QHash<QString, QString> Cache::AvatarUrls;
|
||||||
|
|
||||||
|
|
1034
src/ChatPage.cc
1034
src/ChatPage.cc
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,6 @@
|
||||||
#include "CommunitiesList.h"
|
#include "CommunitiesList.h"
|
||||||
|
#include "Cache.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
@ -38,17 +40,14 @@ CommunitiesList::CommunitiesList(QWidget *parent)
|
||||||
scrollArea_->setWidget(scrollAreaContents_);
|
scrollArea_->setWidget(scrollAreaContents_);
|
||||||
topLayout_->addWidget(scrollArea_);
|
topLayout_->addWidget(scrollArea_);
|
||||||
|
|
||||||
connect(http::client(),
|
// connect(http::client(),
|
||||||
&MatrixClient::communityProfileRetrieved,
|
// &MatrixClient::communityProfileRetrieved,
|
||||||
this,
|
// this,
|
||||||
[](QString communityId, QJsonObject profile) {
|
// [this](QString communityId, QJsonObject profile) {
|
||||||
http::client()->fetchCommunityAvatar(
|
// fetchCommunityAvatar(communityId, profile["avatar_url"].toString());
|
||||||
communityId, QUrl(profile["avatar_url"].toString()));
|
// });
|
||||||
});
|
connect(
|
||||||
connect(http::client(),
|
this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar);
|
||||||
SIGNAL(communityAvatarRetrieved(const QString &, const QPixmap &)),
|
|
||||||
this,
|
|
||||||
SLOT(updateCommunityAvatar(const QString &, const QPixmap &)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -61,8 +60,8 @@ CommunitiesList::setCommunities(const std::map<QString, QSharedPointer<Community
|
||||||
for (const auto &community : communities) {
|
for (const auto &community : communities) {
|
||||||
addCommunity(community.second, community.first);
|
addCommunity(community.second, community.first);
|
||||||
|
|
||||||
http::client()->fetchCommunityProfile(community.first);
|
// http::client()->fetchCommunityProfile(community.first);
|
||||||
http::client()->fetchCommunityRooms(community.first);
|
// http::client()->fetchCommunityRooms(community.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
communities_["world"]->setPressedState(true);
|
communities_["world"]->setPressedState(true);
|
||||||
|
@ -77,7 +76,7 @@ CommunitiesList::addCommunity(QSharedPointer<Community> community, const QString
|
||||||
|
|
||||||
communities_.emplace(community_id, QSharedPointer<CommunitiesListItem>(list_item));
|
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);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
59
src/Logging.cpp
Normal file
59
src/Logging.cpp
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -137,16 +137,16 @@ LoginPage::LoginPage(QWidget *parent)
|
||||||
|
|
||||||
setLayout(top_layout_);
|
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(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
|
||||||
connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
|
connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
|
||||||
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
||||||
connect(password_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(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(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()));
|
connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,17 +180,47 @@ LoginPage::onMatrixIdEntered()
|
||||||
|
|
||||||
inferredServerAddress_ = homeServer;
|
inferredServerAddress_ = homeServer;
|
||||||
serverInput_->setText(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
|
void
|
||||||
LoginPage::onServerAddressEntered()
|
LoginPage::onServerAddressEntered()
|
||||||
{
|
{
|
||||||
error_label_->setText("");
|
error_label_->setText("");
|
||||||
http::client()->setServer(serverInput_->text());
|
http::v2::client()->set_server(serverInput_->text().toStdString());
|
||||||
http::client()->versions();
|
checkHomeserverVersion();
|
||||||
|
|
||||||
serverLayout_->removeWidget(errorIcon_);
|
serverLayout_->removeWidget(errorIcon_);
|
||||||
errorIcon_->hide();
|
errorIcon_->hide();
|
||||||
|
@ -199,11 +229,8 @@ LoginPage::onServerAddressEntered()
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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);
|
error_label_->setText(error);
|
||||||
serverInput_->show();
|
serverInput_->show();
|
||||||
|
|
||||||
|
@ -215,7 +242,7 @@ LoginPage::versionError(QString error)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
LoginPage::versionSuccess()
|
LoginPage::versionOk()
|
||||||
{
|
{
|
||||||
serverLayout_->removeWidget(spinner_);
|
serverLayout_->removeWidget(spinner_);
|
||||||
matrixidLayout_->removeWidget(spinner_);
|
matrixidLayout_->removeWidget(spinner_);
|
||||||
|
@ -241,8 +268,20 @@ LoginPage::onLoginButtonClicked()
|
||||||
if (password_input_->text().isEmpty())
|
if (password_input_->text().isEmpty())
|
||||||
return loginError(tr("Empty password"));
|
return loginError(tr("Empty password"));
|
||||||
|
|
||||||
http::client()->setServer(serverInput_->text());
|
http::v2::client()->set_server(serverInput_->text().toStdString());
|
||||||
http::client()->login(QString::fromStdString(user.localpart()), password_input_->text());
|
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();
|
emit loggingIn();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
|
|
||||||
|
@ -26,6 +25,7 @@
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "LoadingIndicator.h"
|
#include "LoadingIndicator.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "LoginPage.h"
|
#include "LoginPage.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
|
@ -54,9 +54,6 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
setWindowTitle("nheko");
|
setWindowTitle("nheko");
|
||||||
setObjectName("MainWindow");
|
setObjectName("MainWindow");
|
||||||
|
|
||||||
// Initialize the http client.
|
|
||||||
http::init();
|
|
||||||
|
|
||||||
restoreWindowSize();
|
restoreWindowSize();
|
||||||
|
|
||||||
QFont font("Open Sans");
|
QFont font("Open Sans");
|
||||||
|
@ -124,21 +121,13 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
connect(
|
connect(
|
||||||
chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
|
chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
|
||||||
|
|
||||||
connect(http::client(),
|
connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
|
||||||
SIGNAL(loginSuccess(QString, QString, QString)),
|
http::v2::client()->set_user(res.user_id);
|
||||||
this,
|
showChatPage();
|
||||||
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(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
|
||||||
|
|
||||||
QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
|
QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
|
||||||
connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
|
connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
|
||||||
|
|
||||||
|
@ -156,8 +145,21 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
QString token = settings.value("auth/access_token").toString();
|
QString token = settings.value("auth/access_token").toString();
|
||||||
QString home_server = settings.value("auth/home_server").toString();
|
QString home_server = settings.value("auth/home_server").toString();
|
||||||
QString user_id = settings.value("auth/user_id").toString();
|
QString user_id = settings.value("auth/user_id").toString();
|
||||||
|
QString device_id = settings.value("auth/device_id").toString();
|
||||||
|
|
||||||
showChatPage(user_id, home_server, token);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,12 +218,19 @@ MainWindow::removeOverlayProgressBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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;
|
QSettings settings;
|
||||||
settings.setValue("auth/access_token", token);
|
settings.setValue("auth/access_token", token);
|
||||||
settings.setValue("auth/home_server", homeserver);
|
settings.setValue("auth/home_server", homeserver);
|
||||||
settings.setValue("auth/user_id", userid);
|
settings.setValue("auth/user_id", userid);
|
||||||
|
settings.setValue("auth/device_id", device_id);
|
||||||
|
|
||||||
showOverlayProgressBar();
|
showOverlayProgressBar();
|
||||||
|
|
||||||
|
@ -317,7 +326,7 @@ MainWindow::openLeaveRoomDialog(const QString &room_id)
|
||||||
leaveRoomModal_->hide();
|
leaveRoomModal_->hide();
|
||||||
|
|
||||||
if (leaving)
|
if (leaving)
|
||||||
http::client()->leaveRoom(roomToLeave);
|
chat_page_->leaveRoom(roomToLeave);
|
||||||
});
|
});
|
||||||
|
|
||||||
leaveRoomModal_ =
|
leaveRoomModal_ =
|
||||||
|
|
1380
src/MatrixClient.cc
1380
src/MatrixClient.cc
File diff suppressed because it is too large
Load diff
228
src/Olm.cpp
Normal file
228
src/Olm.cpp
Normal file
|
@ -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
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "FlatButton.h"
|
#include "FlatButton.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "RaisedButton.h"
|
#include "RaisedButton.h"
|
||||||
|
@ -125,35 +126,53 @@ RegisterPage::RegisterPage(QWidget *parent)
|
||||||
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||||
connect(password_confirmation_, 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(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
|
||||||
connect(http::client(),
|
connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError);
|
||||||
SIGNAL(registerError(const QString &)),
|
connect(
|
||||||
this,
|
this,
|
||||||
SLOT(registerError(const QString &)));
|
&RegisterPage::registrationFlow,
|
||||||
connect(http::client(),
|
this,
|
||||||
&MatrixClient::registrationFlow,
|
[this](const std::string &user, const std::string &pass, const std::string &session) {
|
||||||
this,
|
emit errorOccurred();
|
||||||
[this](const QString &user,
|
|
||||||
const QString &pass,
|
|
||||||
const QString &server,
|
|
||||||
const QString &session) {
|
|
||||||
emit errorOccurred();
|
|
||||||
|
|
||||||
if (!captchaDialog_) {
|
if (!captchaDialog_) {
|
||||||
captchaDialog_ =
|
captchaDialog_ = std::make_shared<dialogs::ReCaptcha>(
|
||||||
std::make_shared<dialogs::ReCaptcha>(server, session, this);
|
QString::fromStdString(session), this);
|
||||||
connect(captchaDialog_.get(),
|
connect(
|
||||||
&dialogs::ReCaptcha::closing,
|
captchaDialog_.get(),
|
||||||
this,
|
&dialogs::ReCaptcha::closing,
|
||||||
[this, user, pass, server, session]() {
|
this,
|
||||||
captchaDialog_->close();
|
[this, user, pass, session]() {
|
||||||
emit registering();
|
captchaDialog_->close();
|
||||||
http::client()->registerUser(
|
emit registering();
|
||||||
user, pass, server, session);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
|
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_);
|
setLayout(top_layout_);
|
||||||
}
|
}
|
||||||
|
@ -185,11 +204,56 @@ RegisterPage::onRegisterButtonClicked()
|
||||||
} else if (!server_input_->hasAcceptableInput()) {
|
} else if (!server_input_->hasAcceptableInput()) {
|
||||||
registerError(tr("Invalid server name"));
|
registerError(tr("Invalid server name"));
|
||||||
} else {
|
} else {
|
||||||
QString username = username_input_->text();
|
auto username = username_input_->text().toStdString();
|
||||||
QString password = password_input_->text();
|
auto password = password_input_->text().toStdString();
|
||||||
QString server = server_input_->text();
|
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();
|
emit registering();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QDebug>
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "OverlayModal.h"
|
#include "OverlayModal.h"
|
||||||
|
@ -55,18 +55,7 @@ RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
||||||
scrollArea_->setWidget(scrollAreaContents_);
|
scrollArea_->setWidget(scrollAreaContents_);
|
||||||
topLayout_->addWidget(scrollArea_);
|
topLayout_->addWidget(scrollArea_);
|
||||||
|
|
||||||
connect(http::client(),
|
connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar);
|
||||||
&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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -101,7 +90,28 @@ RoomList::updateAvatar(const QString &room_id, const QString &url)
|
||||||
savedImgData = cache::client()->image(url);
|
savedImgData = cache::client()->image(url);
|
||||||
|
|
||||||
if (savedImgData.isEmpty()) {
|
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) {
|
||||||
|
nhlog::net()->warn(
|
||||||
|
"failed to download room avatar: {} {} {}",
|
||||||
|
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 {
|
} else {
|
||||||
QPixmap img;
|
QPixmap img;
|
||||||
img.loadFromData(savedImgData);
|
img.loadFromData(savedImgData);
|
||||||
|
@ -131,7 +141,8 @@ void
|
||||||
RoomList::updateUnreadMessageCount(const QString &roomid, int count)
|
RoomList::updateUnreadMessageCount(const QString &roomid, int count)
|
||||||
{
|
{
|
||||||
if (!roomExists(roomid)) {
|
if (!roomExists(roomid)) {
|
||||||
qWarning() << "UpdateUnreadMessageCount: Unknown roomid";
|
nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}",
|
||||||
|
roomid.toStdString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +167,7 @@ RoomList::calculateUnreadMessageCount()
|
||||||
void
|
void
|
||||||
RoomList::initialize(const QMap<QString, RoomInfo> &info)
|
RoomList::initialize(const QMap<QString, RoomInfo> &info)
|
||||||
{
|
{
|
||||||
qDebug() << "initialize room list";
|
nhlog::ui()->info("initialize room list");
|
||||||
|
|
||||||
rooms_.clear();
|
rooms_.clear();
|
||||||
|
|
||||||
|
@ -209,7 +220,7 @@ RoomList::highlightSelectedRoom(const QString &room_id)
|
||||||
emit roomChanged(room_id);
|
emit roomChanged(room_id);
|
||||||
|
|
||||||
if (!roomExists(room_id)) {
|
if (!roomExists(room_id)) {
|
||||||
qDebug() << "RoomList: clicked unknown roomid";
|
nhlog::ui()->warn("roomlist: clicked unknown room_id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +243,8 @@ void
|
||||||
RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img)
|
RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img)
|
||||||
{
|
{
|
||||||
if (!roomExists(roomid)) {
|
if (!roomExists(roomid)) {
|
||||||
qWarning() << "Avatar update on non existent room" << roomid;
|
nhlog::ui()->warn("avatar update on non-existent room_id: {}",
|
||||||
|
roomid.toStdString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,7 +258,9 @@ void
|
||||||
RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
|
RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
|
||||||
{
|
{
|
||||||
if (!roomExists(roomid)) {
|
if (!roomExists(roomid)) {
|
||||||
qWarning() << "Description update on non existent room" << roomid << info.body;
|
nhlog::ui()->warn("description update on non-existent room_id: {}, {}",
|
||||||
|
roomid.toStdString(),
|
||||||
|
info.body.toStdString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,7 +328,7 @@ RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias)
|
||||||
joinRoomModal_->hide();
|
joinRoomModal_->hide();
|
||||||
|
|
||||||
if (isJoining)
|
if (isJoining)
|
||||||
http::client()->joinRoom(roomAlias);
|
emit joinRoom(roomAlias);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
@ -71,8 +71,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
|
||||||
this,
|
this,
|
||||||
&FilteredTextEdit::uploadData);
|
&FilteredTextEdit::uploadData);
|
||||||
|
|
||||||
qRegisterMetaType<SearchResult>();
|
|
||||||
qRegisterMetaType<QVector<SearchResult>>();
|
|
||||||
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
|
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
|
||||||
connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
|
connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
|
||||||
popup_.hide();
|
popup_.hide();
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QDebug>
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
@ -25,14 +24,15 @@
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
#include "dialogs/PreviewUploadOverlay.h"
|
#include "dialogs/PreviewUploadOverlay.h"
|
||||||
|
|
||||||
using namespace dialogs;
|
using namespace dialogs;
|
||||||
|
|
||||||
static constexpr const char *DEFAULT = "Upload %1?";
|
constexpr const char *DEFAULT = "Upload %1?";
|
||||||
static constexpr const char *ERROR = "Failed to load image type '%1'. Continue upload?";
|
constexpr const char *ERR_MSG = "Failed to load image type '%1'. Continue upload?";
|
||||||
|
|
||||||
PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
|
PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
|
||||||
: QWidget{parent}
|
: QWidget{parent}
|
||||||
|
@ -105,7 +105,7 @@ PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64
|
||||||
{
|
{
|
||||||
if (mediaType_ == "image") {
|
if (mediaType_ == "image") {
|
||||||
if (!image_.loadFromData(data_)) {
|
if (!image_.loadFromData(data_)) {
|
||||||
titleLabel_.setText(QString{tr(ERROR)}.arg(type));
|
titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
|
||||||
} else {
|
} else {
|
||||||
titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_));
|
titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_));
|
||||||
}
|
}
|
||||||
|
@ -142,8 +142,9 @@ PreviewUploadOverlay::setPreview(const QString &path)
|
||||||
QFile file{path};
|
QFile file{path};
|
||||||
|
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
qWarning() << "Failed to open file from:" << path;
|
nhlog::ui()->warn("Failed to open file ({}): {}",
|
||||||
qWarning() << "Reason:" << file.errorString();
|
path.toStdString(),
|
||||||
|
file.errorString().toStdString());
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -152,7 +153,7 @@ PreviewUploadOverlay::setPreview(const QString &path)
|
||||||
auto mime = db.mimeTypeForFileNameAndData(path, &file);
|
auto mime = db.mimeTypeForFileNameAndData(path, &file);
|
||||||
|
|
||||||
if ((data_ = file.readAll()).isEmpty()) {
|
if ((data_ = file.readAll()).isEmpty()) {
|
||||||
qWarning() << "Failed to read media:" << file.errorString();
|
nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString());
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "FlatButton.h"
|
#include "FlatButton.h"
|
||||||
|
#include "MatrixClient.h"
|
||||||
#include "RaisedButton.h"
|
#include "RaisedButton.h"
|
||||||
#include "Theme.h"
|
#include "Theme.h"
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
|
|
||||||
using namespace dialogs;
|
using namespace dialogs;
|
||||||
|
|
||||||
ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *parent)
|
ReCaptcha::ReCaptcha(const QString &session, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
|
@ -51,12 +52,12 @@ ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *par
|
||||||
layout->addWidget(label);
|
layout->addWidget(label);
|
||||||
layout->addLayout(buttonLayout);
|
layout->addLayout(buttonLayout);
|
||||||
|
|
||||||
connect(openCaptchaBtn_, &QPushButton::clicked, [server, session]() {
|
connect(openCaptchaBtn_, &QPushButton::clicked, [session]() {
|
||||||
const auto url =
|
const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/"
|
||||||
QString(
|
"fallback/web?session=%3")
|
||||||
"https://%1/_matrix/client/r0/auth/m.login.recaptcha/fallback/web?session=%2")
|
.arg(QString::fromStdString(http::v2::client()->server()))
|
||||||
.arg(server)
|
.arg(http::v2::client()->port())
|
||||||
.arg(session);
|
.arg(session);
|
||||||
|
|
||||||
QDesktopServices::openUrl(url);
|
QDesktopServices::openUrl(url);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
#include "Avatar.h"
|
#include "Avatar.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "FlatButton.h"
|
#include "FlatButton.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Painter.h"
|
#include "Painter.h"
|
||||||
#include "TextField.h"
|
#include "TextField.h"
|
||||||
#include "Theme.h"
|
#include "Theme.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "dialogs/RoomSettings.hpp"
|
#include "dialogs/RoomSettings.hpp"
|
||||||
|
#include "ui/ToggleButton.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
@ -67,6 +71,20 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
|
||||||
labelLayout->addWidget(errorField_);
|
labelLayout->addWidget(errorField_);
|
||||||
layout->addLayout(labelLayout);
|
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]() {
|
connect(applyBtn_, &QPushButton::clicked, [this]() {
|
||||||
// Check if the values are changed from the originals.
|
// Check if the values are changed from the originals.
|
||||||
auto newName = nameInput_->text().trimmed();
|
auto newName = nameInput_->text().trimmed();
|
||||||
|
@ -85,53 +103,37 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
|
||||||
state::Name body;
|
state::Name body;
|
||||||
body.name = newName.toStdString();
|
body.name = newName.toStdString();
|
||||||
|
|
||||||
auto proxy =
|
http::v2::client()->send_state_event<state::Name, EventType::RoomName>(
|
||||||
http::client()->sendStateEvent<state::Name, EventType::RoomName>(body,
|
roomId_.toStdString(),
|
||||||
roomId_);
|
body,
|
||||||
connect(proxy.get(),
|
[this, newName](const mtx::responses::EventId &,
|
||||||
&StateEventProxy::stateEventSent,
|
mtx::http::RequestErr err) {
|
||||||
this,
|
if (err) {
|
||||||
[this, proxy, newName]() {
|
emit stateEventErrorCb(
|
||||||
Q_UNUSED(proxy);
|
QString::fromStdString(err->matrix_error.error));
|
||||||
errorField_->hide();
|
return;
|
||||||
emit nameChanged(newName);
|
}
|
||||||
close();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(proxy.get(),
|
emit nameEventSentCb(newName);
|
||||||
&StateEventProxy::stateEventError,
|
});
|
||||||
this,
|
|
||||||
[this, proxy, newName](const QString &msg) {
|
|
||||||
Q_UNUSED(proxy);
|
|
||||||
errorField_->setText(msg);
|
|
||||||
errorField_->show();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
|
if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
|
||||||
state::Topic body;
|
state::Topic body;
|
||||||
body.topic = newTopic.toStdString();
|
body.topic = newTopic.toStdString();
|
||||||
|
|
||||||
auto proxy =
|
http::v2::client()->send_state_event<state::Topic, EventType::RoomTopic>(
|
||||||
http::client()->sendStateEvent<state::Topic, EventType::RoomTopic>(
|
roomId_.toStdString(),
|
||||||
body, roomId_);
|
body,
|
||||||
connect(proxy.get(),
|
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
||||||
&StateEventProxy::stateEventSent,
|
if (err) {
|
||||||
this,
|
emit stateEventErrorCb(
|
||||||
[this, proxy, newTopic]() {
|
QString::fromStdString(err->matrix_error.error));
|
||||||
Q_UNUSED(proxy);
|
return;
|
||||||
errorField_->hide();
|
}
|
||||||
close();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(proxy.get(),
|
emit topicEventSentCb();
|
||||||
&StateEventProxy::stateEventError,
|
});
|
||||||
this,
|
|
||||||
[this, proxy, newTopic](const QString &msg) {
|
|
||||||
Q_UNUSED(proxy);
|
|
||||||
errorField_->setText(msg);
|
|
||||||
errorField_->show();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
|
connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
|
||||||
|
@ -190,8 +192,8 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
|
||||||
layout->setSpacing(15);
|
layout->setSpacing(15);
|
||||||
layout->setMargin(20);
|
layout->setMargin(20);
|
||||||
|
|
||||||
saveBtn_ = new FlatButton("SAVE", this);
|
okBtn_ = new FlatButton(tr("OK"), this);
|
||||||
saveBtn_->setFontSize(conf::btn::fontSize);
|
okBtn_->setFontSize(conf::btn::fontSize);
|
||||||
cancelBtn_ = new FlatButton(tr("CANCEL"), this);
|
cancelBtn_ = new FlatButton(tr("CANCEL"), this);
|
||||||
cancelBtn_->setFontSize(conf::btn::fontSize);
|
cancelBtn_->setFontSize(conf::btn::fontSize);
|
||||||
|
|
||||||
|
@ -199,7 +201,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
|
||||||
btnLayout->setSpacing(0);
|
btnLayout->setSpacing(0);
|
||||||
btnLayout->setMargin(0);
|
btnLayout->setMargin(0);
|
||||||
btnLayout->addStretch(1);
|
btnLayout->addStretch(1);
|
||||||
btnLayout->addWidget(saveBtn_);
|
btnLayout->addWidget(okBtn_);
|
||||||
btnLayout->addWidget(cancelBtn_);
|
btnLayout->addWidget(cancelBtn_);
|
||||||
|
|
||||||
auto notifOptionLayout_ = new QHBoxLayout;
|
auto notifOptionLayout_ = new QHBoxLayout;
|
||||||
|
@ -238,6 +240,61 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
|
||||||
accessOptionLayout->addWidget(accessLabel);
|
accessOptionLayout->addWidget(accessLabel);
|
||||||
accessOptionLayout->addWidget(accessCombo);
|
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. <br>"
|
||||||
|
"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;
|
QFont font;
|
||||||
font.setPixelSize(18);
|
font.setPixelSize(18);
|
||||||
font.setWeight(70);
|
font.setWeight(70);
|
||||||
|
@ -257,10 +314,18 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
|
||||||
layout->addLayout(editLayout_);
|
layout->addLayout(editLayout_);
|
||||||
layout->addLayout(notifOptionLayout_);
|
layout->addLayout(notifOptionLayout_);
|
||||||
layout->addLayout(accessOptionLayout);
|
layout->addLayout(accessOptionLayout);
|
||||||
|
layout->addLayout(encryptionOptionLayout);
|
||||||
layout->addLayout(btnLayout);
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
connect(cancelBtn_, &QPushButton::clicked, this, &RoomSettings::closing);
|
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
|
void
|
||||||
|
@ -273,7 +338,7 @@ RoomSettings::setupEditButton()
|
||||||
hasEditRights_ = cache::client()->hasEnoughPowerLevel(
|
hasEditRights_ = cache::client()->hasEnoughPowerLevel(
|
||||||
{EventType::RoomName, EventType::RoomTopic}, room_id_.toStdString(), userId);
|
{EventType::RoomName, EventType::RoomTopic}, room_id_.toStdString(), userId);
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
qWarning() << "lmdb error" << e.what();
|
nhlog::db()->warn("lmdb error: {}", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr int buttonSize = 36;
|
constexpr int buttonSize = 36;
|
||||||
|
@ -310,10 +375,12 @@ void
|
||||||
RoomSettings::retrieveRoomInfo()
|
RoomSettings::retrieveRoomInfo()
|
||||||
{
|
{
|
||||||
try {
|
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)));
|
setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url)));
|
||||||
} catch (const lmdb::error &e) {
|
} 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +408,28 @@ RoomSettings::saveSettings()
|
||||||
closing();
|
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<int>(err->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
nhlog::net()->info("enabled encryption on room ({})", room_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
RoomSettings::paintEvent(QPaintEvent *)
|
RoomSettings::paintEvent(QPaintEvent *)
|
||||||
{
|
{
|
||||||
|
|
57
src/main.cc
57
src/main.cc
|
@ -17,20 +17,23 @@
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDesktopWidget>
|
#include <QDesktopWidget>
|
||||||
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFontDatabase>
|
#include <QFontDatabase>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
#include <QLibraryInfo>
|
#include <QLibraryInfo>
|
||||||
#include <QNetworkProxy>
|
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
|
#include "MatrixClient.h"
|
||||||
#include "RaisedButton.h"
|
#include "RaisedButton.h"
|
||||||
#include "RunGuard.h"
|
#include "RunGuard.h"
|
||||||
#include "version.hpp"
|
#include "version.hpp"
|
||||||
|
@ -47,28 +50,13 @@ screenCenter(int width, int height)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
setupProxy()
|
createCacheDirectory()
|
||||||
{
|
{
|
||||||
QSettings settings;
|
auto dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||||
|
|
||||||
/**
|
if (!QDir().mkpath(dir)) {
|
||||||
To set up a SOCKS proxy:
|
throw std::runtime_error(
|
||||||
[user]
|
("Unable to create state directory:" + dir).toStdString().c_str());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +121,19 @@ main(int argc, char *argv[])
|
||||||
QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf");
|
QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf");
|
||||||
|
|
||||||
app.setWindowIcon(QIcon(":/logos/nheko.png"));
|
app.setWindowIcon(QIcon(":/logos/nheko.png"));
|
||||||
qSetMessagePattern("%{time process}: [%{type}] - %{message}");
|
|
||||||
|
http::init();
|
||||||
|
|
||||||
|
createCacheDirectory();
|
||||||
|
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
|
@ -154,8 +154,6 @@ main(int argc, char *argv[])
|
||||||
appTranslator.load("nheko_" + lang, ":/translations");
|
appTranslator.load("nheko_" + lang, ":/translations");
|
||||||
app.installTranslator(&appTranslator);
|
app.installTranslator(&appTranslator);
|
||||||
|
|
||||||
setupProxy();
|
|
||||||
|
|
||||||
MainWindow w;
|
MainWindow w;
|
||||||
|
|
||||||
// Move the MainWindow to the center
|
// Move the MainWindow to the center
|
||||||
|
@ -165,7 +163,16 @@ main(int argc, char *argv[])
|
||||||
!settings.value("user/window/tray", true).toBool())
|
!settings.value("user/window/tray", true).toBool())
|
||||||
w.show();
|
w.show();
|
||||||
|
|
||||||
QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize);
|
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(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nhlog::ui()->info("starting nheko {}", nheko::version);
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
#include "Avatar.h"
|
#include "Avatar.h"
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
|
|
||||||
#include "timeline/TimelineItem.h"
|
#include "timeline/TimelineItem.h"
|
||||||
#include "timeline/widgets/AudioItem.h"
|
#include "timeline/widgets/AudioItem.h"
|
||||||
|
@ -62,9 +63,27 @@ TimelineItem::init()
|
||||||
ChatPage::instance()->showReadReceipts(event_id_);
|
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]() {
|
connect(redactMsg_, &QAction::triggered, this, [this]() {
|
||||||
if (!event_id_.isEmpty())
|
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(); });
|
connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); });
|
||||||
|
@ -413,6 +432,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
|
||||||
void
|
void
|
||||||
TimelineItem::markReceived()
|
TimelineItem::markReceived()
|
||||||
{
|
{
|
||||||
|
isReceived_ = true;
|
||||||
checkmark_->setText(CHECKMARK);
|
checkmark_->setText(CHECKMARK);
|
||||||
checkmark_->setAlignment(Qt::AlignTop);
|
checkmark_->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
@ -635,3 +655,19 @@ TimelineItem::addAvatar()
|
||||||
AvatarProvider::resolve(
|
AvatarProvider::resolve(
|
||||||
room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); });
|
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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "FloatingButton.h"
|
#include "FloatingButton.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
|
#include "Olm.hpp"
|
||||||
#include "UserSettingsPage.h"
|
#include "UserSettingsPage.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
|
@ -100,7 +102,7 @@ TimelineView::TimelineView(const QString &room_id, QWidget *parent)
|
||||||
, room_id_{room_id}
|
, room_id_{room_id}
|
||||||
{
|
{
|
||||||
init();
|
init();
|
||||||
http::client()->messages(room_id_, "");
|
getMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -140,7 +142,7 @@ TimelineView::fetchHistory()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
isPaginationInProgress_ = true;
|
isPaginationInProgress_ = true;
|
||||||
http::client()->messages(room_id_, prev_batch_token_);
|
getMessages();
|
||||||
paginationTimer_->start(5000);
|
paginationTimer_->start(5000);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -189,18 +191,13 @@ TimelineView::sliderMoved(int position)
|
||||||
|
|
||||||
isPaginationInProgress_ = true;
|
isPaginationInProgress_ = true;
|
||||||
|
|
||||||
// FIXME: Maybe move this to TimelineViewManager to remove the
|
getMessages();
|
||||||
// extra calls?
|
|
||||||
http::client()->messages(room_id_, prev_batch_token_);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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.
|
// We've reached the start of the timline and there're no more messages.
|
||||||
if ((msgs.end == msgs.start) && msgs.chunk.size() == 0) {
|
if ((msgs.end == msgs.start) && msgs.chunk.size() == 0) {
|
||||||
isTimelineFinished = true;
|
isTimelineFinished = true;
|
||||||
|
@ -239,19 +236,19 @@ TimelineItem *
|
||||||
TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
|
TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
|
||||||
TimelineDirection direction)
|
TimelineDirection direction)
|
||||||
{
|
{
|
||||||
namespace msg = mtx::events::msg;
|
using namespace mtx::events;
|
||||||
using AudioEvent = mtx::events::RoomEvent<msg::Audio>;
|
|
||||||
using EmoteEvent = mtx::events::RoomEvent<msg::Emote>;
|
|
||||||
using FileEvent = mtx::events::RoomEvent<msg::File>;
|
|
||||||
using ImageEvent = mtx::events::RoomEvent<msg::Image>;
|
|
||||||
using NoticeEvent = mtx::events::RoomEvent<msg::Notice>;
|
|
||||||
using TextEvent = mtx::events::RoomEvent<msg::Text>;
|
|
||||||
using VideoEvent = mtx::events::RoomEvent<msg::Video>;
|
|
||||||
|
|
||||||
if (mpark::holds_alternative<mtx::events::RedactionEvent<msg::Redaction>>(event)) {
|
using AudioEvent = RoomEvent<msg::Audio>;
|
||||||
auto redaction_event =
|
using EmoteEvent = RoomEvent<msg::Emote>;
|
||||||
mpark::get<mtx::events::RedactionEvent<msg::Redaction>>(event);
|
using FileEvent = RoomEvent<msg::File>;
|
||||||
const auto event_id = QString::fromStdString(redaction_event.redacts);
|
using ImageEvent = RoomEvent<msg::Image>;
|
||||||
|
using NoticeEvent = RoomEvent<msg::Notice>;
|
||||||
|
using TextEvent = RoomEvent<msg::Text>;
|
||||||
|
using VideoEvent = RoomEvent<msg::Video>;
|
||||||
|
|
||||||
|
if (mpark::holds_alternative<RedactionEvent<msg::Redaction>>(event)) {
|
||||||
|
auto redaction_event = mpark::get<RedactionEvent<msg::Redaction>>(event);
|
||||||
|
const auto event_id = QString::fromStdString(redaction_event.redacts);
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [event_id, this]() {
|
QTimer::singleShot(0, this, [event_id, this]() {
|
||||||
if (eventIds_.contains(event_id))
|
if (eventIds_.contains(event_id))
|
||||||
|
@ -259,35 +256,96 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &
|
||||||
});
|
});
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
} else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Audio>>(event)) {
|
} else if (mpark::holds_alternative<RoomEvent<msg::Audio>>(event)) {
|
||||||
auto audio = mpark::get<mtx::events::RoomEvent<msg::Audio>>(event);
|
auto audio = mpark::get<RoomEvent<msg::Audio>>(event);
|
||||||
return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
|
return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
|
||||||
} else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Emote>>(event)) {
|
} else if (mpark::holds_alternative<RoomEvent<msg::Emote>>(event)) {
|
||||||
auto emote = mpark::get<mtx::events::RoomEvent<msg::Emote>>(event);
|
auto emote = mpark::get<RoomEvent<msg::Emote>>(event);
|
||||||
return processMessageEvent<EmoteEvent>(emote, direction);
|
return processMessageEvent<EmoteEvent>(emote, direction);
|
||||||
} else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::File>>(event)) {
|
} else if (mpark::holds_alternative<RoomEvent<msg::File>>(event)) {
|
||||||
auto file = mpark::get<mtx::events::RoomEvent<msg::File>>(event);
|
auto file = mpark::get<RoomEvent<msg::File>>(event);
|
||||||
return processMessageEvent<FileEvent, FileItem>(file, direction);
|
return processMessageEvent<FileEvent, FileItem>(file, direction);
|
||||||
} else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Image>>(event)) {
|
} else if (mpark::holds_alternative<RoomEvent<msg::Image>>(event)) {
|
||||||
auto image = mpark::get<mtx::events::RoomEvent<msg::Image>>(event);
|
auto image = mpark::get<RoomEvent<msg::Image>>(event);
|
||||||
return processMessageEvent<ImageEvent, ImageItem>(image, direction);
|
return processMessageEvent<ImageEvent, ImageItem>(image, direction);
|
||||||
} else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Notice>>(event)) {
|
} else if (mpark::holds_alternative<RoomEvent<msg::Notice>>(event)) {
|
||||||
auto notice = mpark::get<mtx::events::RoomEvent<msg::Notice>>(event);
|
auto notice = mpark::get<RoomEvent<msg::Notice>>(event);
|
||||||
return processMessageEvent<NoticeEvent>(notice, direction);
|
return processMessageEvent<NoticeEvent>(notice, direction);
|
||||||
} else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Text>>(event)) {
|
} else if (mpark::holds_alternative<RoomEvent<msg::Text>>(event)) {
|
||||||
auto text = mpark::get<mtx::events::RoomEvent<msg::Text>>(event);
|
auto text = mpark::get<RoomEvent<msg::Text>>(event);
|
||||||
return processMessageEvent<TextEvent>(text, direction);
|
return processMessageEvent<TextEvent>(text, direction);
|
||||||
} else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Video>>(event)) {
|
} else if (mpark::holds_alternative<RoomEvent<msg::Video>>(event)) {
|
||||||
auto video = mpark::get<mtx::events::RoomEvent<msg::Video>>(event);
|
auto video = mpark::get<RoomEvent<msg::Video>>(event);
|
||||||
return processMessageEvent<VideoEvent, VideoItem>(video, direction);
|
return processMessageEvent<VideoEvent, VideoItem>(video, direction);
|
||||||
} else if (mpark::holds_alternative<mtx::events::Sticker>(event)) {
|
} else if (mpark::holds_alternative<Sticker>(event)) {
|
||||||
return processMessageEvent<mtx::events::Sticker, StickerItem>(
|
return processMessageEvent<Sticker, StickerItem>(mpark::get<Sticker>(event),
|
||||||
mpark::get<mtx::events::Sticker>(event), direction);
|
direction);
|
||||||
|
} else if (mpark::holds_alternative<EncryptedEvent<msg::Encrypted>>(event)) {
|
||||||
|
auto decrypted =
|
||||||
|
parseEncryptedEvent(mpark::get<EncryptedEvent<msg::Encrypted>>(event));
|
||||||
|
return parseMessageEvent(decrypted, direction);
|
||||||
|
} else if (mpark::holds_alternative<StateEvent<state::Encryption>>(event)) {
|
||||||
|
try {
|
||||||
|
cache::client()->setEncryptedRoom(room_id_.toStdString());
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
nhlog::db()->critical("failed to save room {} as encrypted",
|
||||||
|
room_id_.toStdString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TimelineEvent
|
||||||
|
TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &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<mtx::events::msg::Text> 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)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
body["unsigned"] = e.unsigned_data;
|
||||||
|
|
||||||
|
nhlog::crypto()->info("decrypted data: \n {}", body.dump(2));
|
||||||
|
|
||||||
|
json event_array = json::array();
|
||||||
|
event_array.push_back(body);
|
||||||
|
|
||||||
|
std::vector<TimelineEvent> 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
|
void
|
||||||
TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
|
TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
|
||||||
{
|
{
|
||||||
|
@ -427,10 +485,10 @@ TimelineView::init()
|
||||||
paginationTimer_ = new QTimer(this);
|
paginationTimer_ = new QTimer(this);
|
||||||
connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
|
connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
|
||||||
|
|
||||||
connect(http::client(),
|
connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents);
|
||||||
&MatrixClient::messagesRetrieved,
|
|
||||||
this,
|
connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
|
||||||
&TimelineView::addBackwardsEvents);
|
connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage);
|
||||||
|
|
||||||
connect(scroll_area_->verticalScrollBar(),
|
connect(scroll_area_->verticalScrollBar(),
|
||||||
SIGNAL(valueChanged(int)),
|
SIGNAL(valueChanged(int)),
|
||||||
|
@ -442,6 +500,27 @@ TimelineView::init()
|
||||||
SLOT(sliderRangeChanged(int, int)));
|
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) {
|
||||||
|
nhlog::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
|
void
|
||||||
TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
|
TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
|
||||||
{
|
{
|
||||||
|
@ -513,8 +592,9 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineView::updatePendingMessage(int txn_id, QString event_id)
|
TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id)
|
||||||
{
|
{
|
||||||
|
nhlog::ui()->info("[{}] message was received by the server", txn_id);
|
||||||
if (!pending_msgs_.isEmpty() &&
|
if (!pending_msgs_.isEmpty() &&
|
||||||
pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
|
pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
|
||||||
auto msg = pending_msgs_.dequeue();
|
auto msg = pending_msgs_.dequeue();
|
||||||
|
@ -522,11 +602,18 @@ TimelineView::updatePendingMessage(int txn_id, QString event_id)
|
||||||
|
|
||||||
if (msg.widget) {
|
if (msg.widget) {
|
||||||
msg.widget->setEventId(event_id);
|
msg.widget->setEventId(event_id);
|
||||||
msg.widget->markReceived();
|
|
||||||
eventIds_[event_id] = msg.widget;
|
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();
|
sendNextPendingMessage();
|
||||||
|
@ -540,16 +627,28 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
|
||||||
TimelineItem *view_item =
|
TimelineItem *view_item =
|
||||||
new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_);
|
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) {
|
||||||
|
nhlog::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);
|
addTimelineItem(view_item);
|
||||||
|
|
||||||
lastMessageDirection_ = TimelineDirection::Bottom;
|
lastMessageDirection_ = TimelineDirection::Bottom;
|
||||||
|
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
|
saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
|
||||||
|
|
||||||
int txn_id = http::client()->incrementTransactionId();
|
|
||||||
PendingMessage message(ty, txn_id, body, "", "", -1, "", view_item);
|
|
||||||
handleNewUserMessage(message);
|
handleNewUserMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,19 +666,98 @@ TimelineView::sendNextPendingMessage()
|
||||||
if (pending_msgs_.size() == 0)
|
if (pending_msgs_.size() == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
using namespace mtx::events;
|
||||||
|
|
||||||
PendingMessage &m = pending_msgs_.head();
|
PendingMessage &m = pending_msgs_.head();
|
||||||
|
|
||||||
|
nhlog::ui()->info("[{}] sending next queued message", m.txn_id);
|
||||||
|
|
||||||
|
if (m.is_encrypted) {
|
||||||
|
prepareEncryptedMessage(std::move(m));
|
||||||
|
nhlog::ui()->info("[{}] sending encrypted event", m.txn_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (m.ty) {
|
switch (m.ty) {
|
||||||
case mtx::events::MessageType::Audio:
|
case mtx::events::MessageType::Audio: {
|
||||||
case mtx::events::MessageType::Image:
|
http::v2::client()->send_room_message<msg::Audio, EventType::RoomMessage>(
|
||||||
case mtx::events::MessageType::Video:
|
room_id_.toStdString(),
|
||||||
case mtx::events::MessageType::File:
|
m.txn_id,
|
||||||
// FIXME: Improve the API
|
toRoomMessage<msg::Audio>(m),
|
||||||
http::client()->sendRoomMessage(
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
m.ty, m.txn_id, room_id_, m.filename, m.mime, m.media_size, m.body);
|
this,
|
||||||
|
m.txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case mtx::events::MessageType::Image: {
|
||||||
|
http::v2::client()->send_room_message<msg::Image, EventType::RoomMessage>(
|
||||||
|
room_id_.toStdString(),
|
||||||
|
m.txn_id,
|
||||||
|
toRoomMessage<msg::Image>(m),
|
||||||
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
|
this,
|
||||||
|
m.txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case mtx::events::MessageType::Video: {
|
||||||
|
http::v2::client()->send_room_message<msg::Video, EventType::RoomMessage>(
|
||||||
|
room_id_.toStdString(),
|
||||||
|
m.txn_id,
|
||||||
|
toRoomMessage<msg::Video>(m),
|
||||||
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
|
this,
|
||||||
|
m.txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case mtx::events::MessageType::File: {
|
||||||
|
http::v2::client()->send_room_message<msg::File, EventType::RoomMessage>(
|
||||||
|
room_id_.toStdString(),
|
||||||
|
m.txn_id,
|
||||||
|
toRoomMessage<msg::File>(m),
|
||||||
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
|
this,
|
||||||
|
m.txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case mtx::events::MessageType::Text: {
|
||||||
|
http::v2::client()->send_room_message<msg::Text, EventType::RoomMessage>(
|
||||||
|
room_id_.toStdString(),
|
||||||
|
m.txn_id,
|
||||||
|
toRoomMessage<msg::Text>(m),
|
||||||
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
|
this,
|
||||||
|
m.txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case mtx::events::MessageType::Emote: {
|
||||||
|
http::v2::client()->send_room_message<msg::Emote, EventType::RoomMessage>(
|
||||||
|
room_id_.toStdString(),
|
||||||
|
m.txn_id,
|
||||||
|
toRoomMessage<msg::Emote>(m),
|
||||||
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
|
this,
|
||||||
|
m.txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
http::client()->sendRoomMessage(
|
nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString());
|
||||||
m.ty, m.txn_id, room_id_, m.body, m.mime, m.media_size);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -593,7 +771,7 @@ TimelineView::notifyForLastEvent()
|
||||||
if (lastTimelineItem)
|
if (lastTimelineItem)
|
||||||
emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
|
emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
|
||||||
else
|
else
|
||||||
qWarning() << "Cast to TimelineView failed" << room_id_;
|
nhlog::ui()->warn("cast to TimelineView failed: {}", room_id_.toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -606,51 +784,51 @@ TimelineView::notifyForLastEvent(const TimelineEvent &event)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
TimelineView::isPendingMessage(const QString &txnid,
|
TimelineView::isPendingMessage(const std::string &txn_id,
|
||||||
const QString &sender,
|
const QString &sender,
|
||||||
const QString &local_userid)
|
const QString &local_userid)
|
||||||
{
|
{
|
||||||
if (sender != local_userid)
|
if (sender != local_userid)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto match_txnid = [txnid](const auto &msg) -> bool {
|
auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; };
|
||||||
return QString::number(msg.txn_id) == txnid;
|
|
||||||
};
|
|
||||||
|
|
||||||
return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) ||
|
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);
|
std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineView::removePendingMessage(const QString &txnid)
|
TimelineView::removePendingMessage(const std::string &txn_id)
|
||||||
{
|
{
|
||||||
if (txnid.isEmpty())
|
if (txn_id.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
|
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);
|
int index = std::distance(pending_sent_msgs_.begin(), it);
|
||||||
pending_sent_msgs_.removeAt(index);
|
pending_sent_msgs_.removeAt(index);
|
||||||
|
|
||||||
if (pending_sent_msgs_.isEmpty())
|
if (pending_sent_msgs_.isEmpty())
|
||||||
sendNextPendingMessage();
|
sendNextPendingMessage();
|
||||||
|
|
||||||
return;
|
nhlog::ui()->info("[{}] removed message with sync", txn_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
|
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);
|
if (it->widget)
|
||||||
pending_msgs_.removeAt(index);
|
it->widget->markReceived();
|
||||||
|
|
||||||
|
nhlog::ui()->info("[{}] received sync before message response", txn_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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.
|
// Note: We do this even if the message has already been echoed.
|
||||||
QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage()));
|
QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage()));
|
||||||
}
|
}
|
||||||
|
@ -673,7 +851,16 @@ TimelineView::readLastEvent() const
|
||||||
const auto eventId = getLastEventId();
|
const auto eventId = getLastEventId();
|
||||||
|
|
||||||
if (!eventId.isEmpty())
|
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) {
|
||||||
|
nhlog::net()->warn(
|
||||||
|
"failed to read event ({}, {})",
|
||||||
|
room_id_.toStdString(),
|
||||||
|
eventId.toStdString());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
|
@ -743,7 +930,8 @@ void
|
||||||
TimelineView::removeEvent(const QString &event_id)
|
TimelineView::removeEvent(const QString &event_id)
|
||||||
{
|
{
|
||||||
if (!eventIds_.contains(event_id)) {
|
if (!eventIds_.contains(event_id)) {
|
||||||
qWarning() << "unknown event_id couldn't be removed:" << event_id;
|
nhlog::ui()->warn("cannot remove widget with unknown event_id: {}",
|
||||||
|
event_id.toStdString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -860,3 +1048,332 @@ TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second)
|
||||||
|
|
||||||
return diffInSeconds > fifteenMins;
|
return diffInSeconds > fifteenMins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TimelineView::sendRoomMessageHandler(const std::string &txn_id,
|
||||||
|
const mtx::responses::EventId &res,
|
||||||
|
mtx::http::RequestErr err)
|
||||||
|
{
|
||||||
|
if (err) {
|
||||||
|
const int status_code = static_cast<int>(err->status_code);
|
||||||
|
nhlog::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<mtx::events::msg::Audio>(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<mtx::events::msg::Image>(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<mtx::events::msg::Video>(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<mtx::events::msg::Emote>(const PendingMessage &m)
|
||||||
|
{
|
||||||
|
mtx::events::msg::Emote emote;
|
||||||
|
emote.body = m.body.toStdString();
|
||||||
|
return emote;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
mtx::events::msg::File
|
||||||
|
toRoomMessage<mtx::events::msg::File>(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<mtx::events::msg::Text>(const PendingMessage &m)
|
||||||
|
{
|
||||||
|
mtx::events::msg::Text text;
|
||||||
|
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::Audio>(msg));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType::Emote: {
|
||||||
|
content = json(toRoomMessage<msg::Emote>(msg));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType::File: {
|
||||||
|
content = json(toRoomMessage<msg::File>(msg));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType::Image: {
|
||||||
|
content = json(toRoomMessage<msg::Image>(msg));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType::Text: {
|
||||||
|
content = json(toRoomMessage<msg::Text>(msg));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType::Video: {
|
||||||
|
content = json(toRoomMessage<msg::Video>(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<msg::Encrypted, EventType::RoomEncrypted>(
|
||||||
|
room_id,
|
||||||
|
msg.txn_id,
|
||||||
|
data,
|
||||||
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
|
this,
|
||||||
|
msg.txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nhlog::ui()->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);
|
||||||
|
nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
|
||||||
|
|
||||||
|
auto keeper = std::make_shared<StateKeeper>(
|
||||||
|
[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<msg::Encrypted, EventType::RoomEncrypted>(
|
||||||
|
room_id,
|
||||||
|
txn_id,
|
||||||
|
data,
|
||||||
|
std::bind(&TimelineView::sendRoomMessageHandler,
|
||||||
|
this,
|
||||||
|
txn_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
nhlog::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, this](
|
||||||
|
const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to query device keys: {} {}",
|
||||||
|
err->matrix_error.error,
|
||||||
|
static_cast<int>(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) {
|
||||||
|
nhlog::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())) {
|
||||||
|
nhlog::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) {
|
||||||
|
nhlog::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},
|
||||||
|
std::bind(&TimelineView::handleClaimedKeys,
|
||||||
|
this,
|
||||||
|
keeper,
|
||||||
|
room_key,
|
||||||
|
pks,
|
||||||
|
dev.second.user_id,
|
||||||
|
dev.second.device_id,
|
||||||
|
std::placeholders::_1,
|
||||||
|
std::placeholders::_2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
nhlog::db()->critical(
|
||||||
|
"failed to open outbound megolm session ({}): {}", room_id, e.what());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TimelineView::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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,12 +18,10 @@
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDebug>
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
#include "MatrixClient.h"
|
#include "Logging.hpp"
|
||||||
|
|
||||||
#include "timeline/TimelineView.h"
|
#include "timeline/TimelineView.h"
|
||||||
#include "timeline/TimelineViewManager.h"
|
#include "timeline/TimelineViewManager.h"
|
||||||
#include "timeline/widgets/AudioItem.h"
|
#include "timeline/widgets/AudioItem.h"
|
||||||
|
@ -35,42 +33,15 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
|
||||||
: QStackedWidget(parent)
|
: QStackedWidget(parent)
|
||||||
{
|
{
|
||||||
setStyleSheet("border: none;");
|
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
|
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.
|
auto view = views_[room_id];
|
||||||
QSettings settings;
|
|
||||||
settings.setValue("client/transaction_id", txn_id + 1);
|
|
||||||
|
|
||||||
auto view = views_[roomid];
|
if (view)
|
||||||
view->updatePendingMessage(txn_id, event_id);
|
view->removeEvent(event_id);
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TimelineViewManager::messageSendFailed(const QString &roomid, int txn_id)
|
|
||||||
{
|
|
||||||
auto view = views_[roomid];
|
|
||||||
view->handleFailedMessage(txn_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -105,7 +76,7 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
|
||||||
uint64_t size)
|
uint64_t size)
|
||||||
{
|
{
|
||||||
if (!timelineViewExists(roomid)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +93,7 @@ TimelineViewManager::queueFileMessage(const QString &roomid,
|
||||||
uint64_t size)
|
uint64_t size)
|
||||||
{
|
{
|
||||||
if (!timelineViewExists(roomid)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +110,7 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
|
||||||
uint64_t size)
|
uint64_t size)
|
||||||
{
|
{
|
||||||
if (!timelineViewExists(roomid)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +127,7 @@ TimelineViewManager::queueVideoMessage(const QString &roomid,
|
||||||
uint64_t size)
|
uint64_t size)
|
||||||
{
|
{
|
||||||
if (!timelineViewExists(roomid)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +198,8 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
|
||||||
auto roomid = QString::fromStdString(room.first);
|
auto roomid = QString::fromStdString(room.first);
|
||||||
|
|
||||||
if (!timelineViewExists(roomid)) {
|
if (!timelineViewExists(roomid)) {
|
||||||
qDebug() << "Ignoring event from unknown room" << roomid;
|
nhlog::ui()->warn("ignoring event from unknown room: {}",
|
||||||
|
roomid.toStdString());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +213,8 @@ void
|
||||||
TimelineViewManager::setHistoryView(const QString &room_id)
|
TimelineViewManager::setHistoryView(const QString &room_id)
|
||||||
{
|
{
|
||||||
if (!timelineViewExists(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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QDebug>
|
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
|
@ -50,21 +50,12 @@ AudioItem::init()
|
||||||
playIcon_.addFile(":/icons/icons/ui/play-sign.png");
|
playIcon_.addFile(":/icons/icons/ui/play-sign.png");
|
||||||
pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
|
pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
|
||||||
|
|
||||||
QList<QString> 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_ = new QMediaPlayer;
|
||||||
player_->setMedia(QUrl(url_));
|
player_->setMedia(QUrl(url_));
|
||||||
player_->setVolume(100);
|
player_->setVolume(100);
|
||||||
player_->setNotifyInterval(1000);
|
player_->setNotifyInterval(1000);
|
||||||
|
|
||||||
|
connect(this, &AudioItem::fileDownloadedCb, this, &AudioItem::fileDownloaded);
|
||||||
connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
|
connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
|
||||||
if (state == QMediaPlayer::StoppedState) {
|
if (state == QMediaPlayer::StoppedState) {
|
||||||
state_ = AudioState::Play;
|
state_ = AudioState::Play;
|
||||||
|
@ -129,14 +120,20 @@ AudioItem::mousePressEvent(QMouseEvent *event)
|
||||||
if (filenameToSave_.isEmpty())
|
if (filenameToSave_.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto proxy = http::client()->downloadFile(url_);
|
http::v2::client()->download(
|
||||||
connect(proxy.data(),
|
url_.toString().toStdString(),
|
||||||
&DownloadMediaProxy::fileDownloaded,
|
[this](const std::string &data,
|
||||||
this,
|
const std::string &,
|
||||||
[proxy, this](const QByteArray &data) {
|
const std::string &,
|
||||||
proxy->deleteLater();
|
mtx::http::RequestErr err) {
|
||||||
fileDownloaded(data);
|
if (err) {
|
||||||
});
|
nhlog::net()->info("failed to retrieve m.audio content: {}",
|
||||||
|
url_.toString().toStdString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit fileDownloadedCb(QByteArray(data.data(), data.size()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,8 +148,8 @@ AudioItem::fileDownloaded(const QByteArray &data)
|
||||||
|
|
||||||
file.write(data);
|
file.write(data);
|
||||||
file.close();
|
file.close();
|
||||||
} catch (const std::exception &ex) {
|
} catch (const std::exception &e) {
|
||||||
qDebug() << "Error while saving file to:" << ex.what();
|
nhlog::ui()->warn("error while saving file: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QDebug>
|
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
|
@ -49,17 +49,9 @@ FileItem::init()
|
||||||
|
|
||||||
icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
|
icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
|
||||||
|
|
||||||
QList<QString> 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);
|
setFixedHeight(Height);
|
||||||
|
|
||||||
|
connect(this, &FileItem::fileDownloadedCb, this, &FileItem::fileDownloaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
|
FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
|
||||||
|
@ -89,8 +81,15 @@ FileItem::openUrl()
|
||||||
if (url_.toString().isEmpty())
|
if (url_.toString().isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!QDesktopServices::openUrl(url_))
|
auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
|
||||||
qWarning() << "Could not open url" << url_.toString();
|
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))
|
||||||
|
nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
QSize
|
QSize
|
||||||
|
@ -115,14 +114,20 @@ FileItem::mousePressEvent(QMouseEvent *event)
|
||||||
if (filenameToSave_.isEmpty())
|
if (filenameToSave_.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto proxy = http::client()->downloadFile(url_);
|
http::v2::client()->download(
|
||||||
connect(proxy.data(),
|
url_.toString().toStdString(),
|
||||||
&DownloadMediaProxy::fileDownloaded,
|
[this](const std::string &data,
|
||||||
this,
|
const std::string &,
|
||||||
[proxy, this](const QByteArray &data) {
|
const std::string &,
|
||||||
proxy->deleteLater();
|
mtx::http::RequestErr err) {
|
||||||
fileDownloaded(data);
|
if (err) {
|
||||||
});
|
nhlog::ui()->warn("failed to retrieve m.file content: {}",
|
||||||
|
url_.toString().toStdString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit fileDownloadedCb(QByteArray(data.data(), data.size()));
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
openUrl();
|
openUrl();
|
||||||
}
|
}
|
||||||
|
@ -139,8 +144,8 @@ FileItem::fileDownloaded(const QByteArray &data)
|
||||||
|
|
||||||
file.write(data);
|
file.write(data);
|
||||||
file.close();
|
file.close();
|
||||||
} catch (const std::exception &ex) {
|
} catch (const std::exception &e) {
|
||||||
qDebug() << "Error while saving file to:" << ex.what();
|
nhlog::ui()->warn("Error while saving file to: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QDebug>
|
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
@ -25,42 +24,71 @@
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "Logging.hpp"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "dialogs/ImageOverlay.h"
|
#include "dialogs/ImageOverlay.h"
|
||||||
#include "timeline/widgets/ImageItem.h"
|
#include "timeline/widgets/ImageItem.h"
|
||||||
|
|
||||||
ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
|
void
|
||||||
: QWidget(parent)
|
ImageItem::downloadMedia(const QUrl &url)
|
||||||
, event_{event}
|
{
|
||||||
|
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) {
|
||||||
|
nhlog::net()->warn(
|
||||||
|
"failed to retrieve image {}: {} {}",
|
||||||
|
url.toString().toStdString(),
|
||||||
|
err->matrix_error.error,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
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 &e) {
|
||||||
|
nhlog::ui()->warn("Error while saving file to: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ImageItem::init()
|
||||||
{
|
{
|
||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
setCursor(Qt::PointingHandCursor);
|
setCursor(Qt::PointingHandCursor);
|
||||||
setAttribute(Qt::WA_Hover, true);
|
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<mtx::events::msg::Image> &event, QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, event_{event}
|
||||||
|
{
|
||||||
url_ = QString::fromStdString(event.content.url);
|
url_ = QString::fromStdString(event.content.url);
|
||||||
text_ = QString::fromStdString(event.content.body);
|
text_ = QString::fromStdString(event.content.body);
|
||||||
|
|
||||||
QList<QString> url_parts = url_.toString().split("mxc://");
|
init();
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
|
ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
|
||||||
|
@ -69,31 +97,7 @@ ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size,
|
||||||
, text_{filename}
|
, text_{filename}
|
||||||
{
|
{
|
||||||
Q_UNUSED(size);
|
Q_UNUSED(size);
|
||||||
|
init();
|
||||||
setMouseTracking(true);
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
setAttribute(Qt::WA_Hover, true);
|
|
||||||
|
|
||||||
QList<QString> 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -102,8 +106,15 @@ ImageItem::openUrl()
|
||||||
if (url_.toString().isEmpty())
|
if (url_.toString().isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!QDesktopServices::openUrl(url_))
|
auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
|
||||||
qWarning() << "Could not open url" << url_.toString();
|
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))
|
||||||
|
nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
QSize
|
QSize
|
||||||
|
@ -231,23 +242,22 @@ ImageItem::saveAs()
|
||||||
if (filename.isEmpty())
|
if (filename.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto proxy = http::client()->downloadFile(url_);
|
const auto url = url_.toString().toStdString();
|
||||||
connect(proxy.data(),
|
|
||||||
&DownloadMediaProxy::fileDownloaded,
|
|
||||||
this,
|
|
||||||
[proxy, filename](const QByteArray &data) {
|
|
||||||
proxy->deleteLater();
|
|
||||||
|
|
||||||
try {
|
http::v2::client()->download(
|
||||||
QFile file(filename);
|
url,
|
||||||
|
[this, filename, url](const std::string &data,
|
||||||
|
const std::string &,
|
||||||
|
const std::string &,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to retrieve image {}: {} {}",
|
||||||
|
url,
|
||||||
|
err->matrix_error.error,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!file.open(QIODevice::WriteOnly))
|
emit imageSaved(filename, QByteArray(data.data(), data.size()));
|
||||||
return;
|
});
|
||||||
|
|
||||||
file.write(data);
|
|
||||||
file.close();
|
|
||||||
} catch (const std::exception &ex) {
|
|
||||||
qDebug() << "Error while saving file to:" << ex.what();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
@ -27,15 +26,15 @@
|
||||||
void
|
void
|
||||||
VideoItem::init()
|
VideoItem::init()
|
||||||
{
|
{
|
||||||
QList<QString> url_parts = url_.toString().split("mxc://");
|
// QList<QString> url_parts = url_.toString().split("mxc://");
|
||||||
if (url_parts.size() != 2) {
|
// if (url_parts.size() != 2) {
|
||||||
qDebug() << "Invalid format for image" << url_.toString();
|
// qDebug() << "Invalid format for image" << url_.toString();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
QString media_params = url_parts[1];
|
// QString media_params = url_parts[1];
|
||||||
url_ = QString("%1/_matrix/media/r0/download/%2")
|
// url_ = QString("%1/_matrix/media/r0/download/%2")
|
||||||
.arg(http::client()->getHomeServer().toString(), media_params);
|
// .arg(http::client()->getHomeServer().toString(), media_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)
|
VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)
|
||||||
|
|
Loading…
Reference in a new issue