Merge remote-tracking branch 'nheko-im/master' into video_player_enhancements

pull/812/head
Joseph Donofry 3 years ago
commit 093f9f9e33
No known key found for this signature in database
GPG Key ID: E8A1D78EF044B0CB
  1. 2
      .ci/macos/deploy.sh
  2. 61
      .github/ISSUE_TEMPLATE/bug_report.md
  3. 150
      .github/ISSUE_TEMPLATE/bug_report.yaml
  4. 20
      .github/ISSUE_TEMPLATE/feature_request.md
  5. 49
      .github/ISSUE_TEMPLATE/feature_request.yaml
  6. 4
      CMakeLists.txt
  7. 2
      README.md
  8. 4
      io.github.NhekoReborn.Nheko.yaml
  9. 20
      resources/qml/Avatar.qml
  10. 4
      resources/qml/ForwardCompleter.qml
  11. 1
      resources/qml/MessageView.qml
  12. 3
      resources/qml/QuickSwitcher.qml
  13. 1
      resources/qml/ReplyPopup.qml
  14. 12
      resources/qml/RoomList.qml
  15. 42
      resources/qml/RoomMembers.qml
  16. 4
      resources/qml/RoomSettings.qml
  17. 5
      resources/qml/Root.qml
  18. 10
      resources/qml/TimelineView.qml
  19. 28
      resources/qml/TopBar.qml
  20. 2
      resources/qml/dialogs/ImagePackEditorDialog.qml
  21. 388
      src/Cache.cpp
  22. 10
      src/CacheCryptoStructs.h
  23. 45
      src/Cache_p.h
  24. 2
      src/ChatPage.cpp
  25. 2
      src/ChatPage.h
  26. 12
      src/MemberList.cpp
  27. 1
      src/MemberList.h
  28. 96
      src/MxcImageProvider.cpp
  29. 7
      src/MxcImageProvider.h
  30. 33
      src/Olm.cpp
  31. 18
      src/RegisterPage.cpp
  32. 3
      src/UserSettingsPage.cpp
  33. 5
      src/UserSettingsPage.h
  34. 4
      src/dialogs/ImageOverlay.cpp
  35. 2
      src/dialogs/ImageOverlay.h
  36. 19
      src/timeline/TimelineModel.cpp
  37. 3
      src/timeline/TimelineModel.h
  38. 8
      src/timeline/TimelineViewManager.cpp
  39. 4
      src/timeline/TimelineViewManager.h

@ -6,7 +6,7 @@ set -eux
#TAG=$(git tag -l --points-at HEAD)
# Add Qt binaries to path
PATH=/usr/local/opt/qt/bin/:${PATH}
PATH=/usr/local/opt/qt@5/bin/:${PATH}
( cd build
# macdeployqt does not copy symlinks over.

@ -1,61 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
### Describe the bug
A clear and concise description of what the bug is.
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
### Expected behavior
A clear and concise description of what you expected to happen.
### Screenshots
If applicable, add screenshots to help explain your problem.
### System:
- Nheko version: <!-- Get the version from the settings menu (bottom left corner) -->
- Profile used: <!-- If you are not using the default profile, mention it here -->
- Installation method: <!-- AppImage, some repository, local build etc -->
- Operating System:
- Qt version: <!-- If you compiled it yourself -->
- C++ compiler: <!-- if you compiled it yourself -->
- Desktop Environment: <!-- for Linux -->
### Logs
<!-- If applicable -->
<!-- The log file is located in
Linux: ~/.cache/nheko/
macOS: ~/Library/Caches/nheko or /Library/Caches/nheko
Windows: C:/Users/<USER>/AppData/Local/nheko/cache
-->
### Debugger backtrace
<!--
If the program crashed send a backtrace:
You can retrieve a backtrace by building nheko with -DCMAKE_BUILD_TYPE=Debug
and running it through gdb or lldb.
gdb ./build/nheko
>> run
... Make the program crash
>> bt
... Paste a link of the output below (Use a pastebin, don't paste directly in the github issue).
-->

@ -0,0 +1,150 @@
name: Bug Report
description: Create a report to help us improve
#title: "[Bug]: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please try to fill out all fields to the best of your ability.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: Enter your description here.
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: behaviour
attributes:
label: What happened?
description: A clear and concise description of what actually happened.
validations:
required: false
- type: textarea
id: expected-behaviour
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
placeholder: Upload your screenshots here. You can paste them or click on "Attach files".
validations:
required: false
- type: input
id: version
attributes:
label: Version
description: Get the version from the settings menu (bottom left corner)
placeholder: 0.0.1-deafbeef
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
multiple: true
options:
- Linux
- macOS
- Windows
- BSD
- Haiku
- Other
- type: dropdown
id: install-method
attributes:
label: Installation method
multiple: true
options:
- Flathub
- Flatpak nightly repo or download
- AppImage
- Windows download
- macOS DMG file
- Some repository (AUR, homebrew, distribution repository, PPA, etc)
- Local build
- type: input
id: qt-version
attributes:
label: Qt version
description: What version of Qt does your system use? (If you compiled Nheko yourself.)
placeholder: 5.15.2.
validations:
required: false
- type: input
id: compiler
attributes:
label: C++ compiler
description: What compiler (and version) did you use (if you compiled Nheko yourself)?
placeholder: gcc-9000
validations:
required: false
- type: input
id: de
attributes:
label: Desktop Environment
description: If you are on Linux, describe your desktop environment.
placeholder: KDE with i3 as the window manager
validations:
required: false
- type: checkboxes
id: profiles
attributes:
label: Did you use profiles?
description: Usually by passing the --profile command line parameter. If you don't know, answer 'no'.
options:
- label: Profiles used?
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
placeholder: |
The log file is located in
Linux: ~/.cache/nheko/
macOS: ~/Library/Caches/nheko or /Library/Caches/nheko
Windows: C:/Users/<USER>/AppData/Local/nheko/cache
render: shell
- type: textarea
id: backtrace
attributes:
label: Backtrace
description: If the program crashed send a backtrace.
placeholder: |
You can retrieve a backtrace by building nheko with -DCMAKE_BUILD_TYPE=Debug and running it through gdb or lldb.
gdb ./build/nheko
>> run
... Make the program crash
>> bt
render: shell

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

@ -0,0 +1,49 @@
name: Feature request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Please verify that there is no feature request for this already!
- type: textarea
id: problem
attributes:
label: The Problem
description: Is your feature request related to a problem? Please describe.
placeholder: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
validations:
required: true
- type: textarea
id: solution
attributes:
label: The Solution
description: Describe the solution you'd like
placeholder: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives
description: Describe alternatives you've considered.
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Describe alternatives you've considered.
placeholder: Add any other context or screenshots about the feature request here.
validations:
required: false
- type: checkboxes
id: version-check
attributes:
label: Happens in the latest version
description: Please verify that this is still missing in the latest version.
options:
- label: Yes, this feature is still missing.
required: true

@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
GIT_TAG deb51ef1d6df870098069312f0a1999550e1eb85
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@ -676,7 +676,7 @@ if(USE_BUNDLED_COEURL)
FetchContent_Declare(
coeurl
GIT_REPOSITORY https://nheko.im/Nheko-Reborn/coeurl.git
GIT_TAG e9010d1ce14e7163d1cb5407ed27b23303781796
GIT_TAG 3901507db25cf3f9364b58cd8c7880640900c992
)
FetchContent_MakeAvailable(coeurl)
target_link_libraries(nheko PUBLIC coeurl::coeurl)

@ -213,7 +213,7 @@ sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig dev-libs/qtkeychain
```bash
# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev libevent-dev libcurl-dev
```
This will install all dependencies, except for tweeny (use bundled tweeny)
and mtxclient (needs to be build separately).

@ -152,7 +152,7 @@ modules:
- -Ddefault_library=static
name: coeurl
sources:
- commit: 417821a07cfe4429b08a2efed5e480a498087afd
- commit: 3901507db25cf3f9364b58cd8c7880640900c992
type: git
url: https://nheko.im/nheko-reborn/coeurl.git
- config-opts:
@ -163,7 +163,7 @@ modules:
buildsystem: cmake-ninja
name: mtxclient
sources:
- commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80
- commit: deb51ef1d6df870098069312f0a1999550e1eb85
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git
- config-opts:

@ -3,7 +3,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import "./ui"
import QtGraphicalEffects 1.0
import QtQuick 2.6
import QtQuick.Controls 2.3
import im.nheko 1.0
@ -21,7 +20,7 @@ Rectangle {
width: 48
height: 48
radius: Settings.avatarCircles ? height / 2 : 3
radius: Settings.avatarCircles ? height / 2 : height / 8
color: Nheko.colors.alternateBase
Component.onCompleted: {
mouseArea.clicked.connect(clicked);
@ -50,8 +49,7 @@ Rectangle {
smooth: true
sourceSize.width: avatar.width
sourceSize.height: avatar.height
layer.enabled: true
source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100.0 : 25.0) + ((avatar.crop) ? "" : "&scale")) : ""
MouseArea {
id: mouseArea
@ -65,18 +63,6 @@ Rectangle {
}
layer.effect: OpacityMask {
cached: true
maskSource: Rectangle {
anchors.fill: parent
width: avatar.width
height: avatar.height
radius: Settings.avatarCircles ? height / 2 : 3
}
}
}
Rectangle {
@ -85,7 +71,7 @@ Rectangle {
visible: !!userid
height: avatar.height / 6
width: height
radius: Settings.avatarCircles ? height / 2 : height / 4
radius: Settings.avatarCircles ? height / 2 : height / 8
color: {
switch (TimelineManager.userPresence(userid)) {
case "online":

@ -68,6 +68,7 @@ Popup {
isOnlyEmoji: modelData.isOnlyEmoji ?? false
userId: modelData.userId ?? ""
userName: modelData.userName ?? ""
encryptionError: modelData.encryptionError ?? ""
}
MatrixTextField {
@ -85,6 +86,9 @@ Popup {
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion();
event.accepted = true;

@ -6,7 +6,6 @@ import "./delegates"
import "./emoji"
import "./ui"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2

@ -45,6 +45,9 @@ Popup {
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion();
event.accepted = true;

@ -45,6 +45,7 @@ Rectangle {
isOnlyEmoji: modelData.isOnlyEmoji ?? false
userId: modelData.userId ?? ""
userName: modelData.userName ?? ""
encryptionError: modelData.encryptionError ?? ""
}
ImageButton {

@ -33,8 +33,9 @@ Page {
Connections {
function onCurrentRoomChanged() {
roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
console.log("Test" + Rooms.currentRoom.roomId + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId));
if (Rooms.currentRoom)
roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
}
target: Rooms
@ -190,7 +191,12 @@ Page {
TapHandler {
margin: -Nheko.paddingSmall
onSingleTapped: Rooms.setCurrentRoom(roomId)
onSingleTapped: {
if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
Rooms.setCurrentRoom(roomId);
else
Rooms.resetCurrentRoom();
}
onLongPressed: {
if (!isInvite)
roomContextMenu.show(roomId, tags);

@ -13,6 +13,7 @@ ApplicationWindow {
id: roomMembersRoot
property MemberList members
property Room room
title: qsTr("Members of %1").arg(members.roomName)
height: 650
@ -83,9 +84,14 @@ ApplicationWindow {
}
delegate: RowLayout {
id: del
width: ListView.view.width
spacing: Nheko.paddingMedium
Avatar {
id: avatar
width: Nheko.avatarSize
height: Nheko.avatarSize
userid: model.mxid
@ -97,16 +103,18 @@ ApplicationWindow {
ColumnLayout {
spacing: Nheko.paddingSmall
Label {
text: model.displayName
ElidedLabel {
fullText: model.displayName
color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
font.pointSize: fontMetrics.font.pointSize
font.pixelSize: fontMetrics.font.pixelSize
elideWidth: del.width - Nheko.paddingMedium * 2 - avatar.width - encryptInd.width
}
Label {
text: model.mxid
ElidedLabel {
fullText: model.mxid
color: Nheko.colors.buttonText
font.pointSize: fontMetrics.font.pointSize * 0.9
font.pixelSize: Math.ceil(fontMetrics.font.pixelSize * 0.9)
elideWidth: del.width - Nheko.paddingMedium * 2 - avatar.width - encryptInd.width
}
Item {
@ -116,6 +124,28 @@ ApplicationWindow {
}
EncryptionIndicator {
id: encryptInd
Layout.alignment: Qt.AlignRight
visible: room.isEncrypted
encrypted: room.isEncrypted
trust: encrypted ? model.trustlevel : Crypto.Unverified
ToolTip.text: {
if (!encrypted)
return qsTr("This room is not encrypted!");
switch (trust) {
case Crypto.Verified:
return qsTr("This user is verified.");
case Crypto.TOFU:
return qsTr("This user isn't verified, but is still using the same master key from the first time you met.");
default:
return qsTr("This user has unverified devices!");
}
}
}
}
footer: Item {

@ -15,8 +15,8 @@ ApplicationWindow {
property var roomSettings
minimumWidth: 420
minimumHeight: 650
minimumWidth: 450
minimumHeight: 680
palette: Nheko.colors
color: Nheko.colors.window
modality: Qt.NonModal

@ -8,7 +8,6 @@ import "./dialogs"
import "./emoji"
import "./voip"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
@ -153,10 +152,10 @@ Page {
packSet.show();
}
function onOpenRoomMembersDialog(members) {
function onOpenRoomMembersDialog(members, room) {
var membersDialog = roomMembersComponent.createObject(timelineRoot, {
"members": members,
"roomName": Rooms.currentRoom.roomName
"room": room
});
membersDialog.show();
}

@ -9,7 +9,6 @@ import "./emoji"
import "./ui"
import "./voip"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3
@ -85,9 +84,14 @@ Item {
target: timelineView
}
MessageView {
Loader {
active: room || roomPreview
Layout.fillWidth: true
implicitHeight: msgView.height - typingIndicator.height
sourceComponent: MessageView {
implicitHeight: msgView.height - typingIndicator.height
}
}
Loader {

@ -15,6 +15,8 @@ Rectangle {
property string roomName: room ? room.roomName : qsTr("No room selected")
property string avatarUrl: room ? room.roomAvatarUrl : ""
property string roomTopic: room ? room.roomTopic : ""
property bool isEncrypted: room ? room.isEncrypted : false
property int trustlevel: room ? room.trustlevel : Crypto.Unverified
Layout.fillWidth: true
implicitHeight: topLayout.height + Nheko.paddingMedium * 2
@ -92,11 +94,33 @@ Rectangle {
text: roomTopic
}
EncryptionIndicator {
Layout.column: 3
Layout.row: 0
Layout.rowSpan: 2
visible: isEncrypted
encrypted: isEncrypted
trust: trustlevel
ToolTip.text: {
if (!encrypted)
return qsTr("This room is not encrypted!");
switch (trust) {
case Crypto.Verified:
return qsTr("This room contains only verified devices.");
case Crypto.TOFU:
return qsTr("This rooms contain verified devices and devices which have never changed their master key.");
default:
return qsTr("This room contains unverified devices!");
}
}
}
ImageButton {
id: roomOptionsButton
visible: !!room
Layout.column: 3
Layout.column: 4
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
@ -116,7 +140,7 @@ Rectangle {
Platform.MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openRoomMembers(room.roomId)
onTriggered: TimelineManager.openRoomMembers(room)
}
Platform.MenuItem {

@ -171,7 +171,7 @@ ApplicationWindow {
}
MatrixText {
text: qsTr("Attrbution")
text: qsTr("Attribution")
}
MatrixTextField {

@ -114,7 +114,13 @@ ro_txn(lmdb::env &env)
txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
reuse_counter = 0;
} else if (reuse_counter > 0) {
txn.renew();
try {
txn.renew();
} catch (...) {
txn.abort();
txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
reuse_counter = 0;
}
}
reuse_counter++;
@ -289,7 +295,9 @@ Cache::setup()
megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
// What rooms are encrypted
encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
[[maybe_unused]] auto verificationDb = getVerificationDb(txn);
[[maybe_unused]] auto userKeysDb = getUserKeysDb(txn);
txn.commit();
@ -720,20 +728,35 @@ Cache::storeSecret(const std::string name, const std::string secret)
{
auto settings = UserSettings::instance();
auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setKey("matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) +
"." + name.c_str());
job->setSettings(UserSettings::instance()->qsettings());
job->setKey(
"matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
job->setTextData(QString::fromStdString(secret));
QObject::connect(job, &QKeychain::Job::finished, job, [name, this](QKeychain::Job *job) {
if (job->error()) {
nhlog::db()->warn(
"Storing secret '{}' failed: {}", name, job->errorString().toStdString());
} else {
emit secretChanged(name);
}
});
QObject::connect(
job,
&QKeychain::WritePasswordJob::finished,
this,
[name, this](QKeychain::Job *job) {
if (job->error()) {
nhlog::db()->warn("Storing secret '{}' failed: {}",
name,
job->errorString().toStdString());
} else {
// if we emit the signal directly, qtkeychain breaks and won't execute new
// jobs. You can't start a job from the finish signal of a job.
QTimer::singleShot(100, [this, name] { emit secretChanged(name); });
nhlog::db()->info("Storing secret '{}' successful", name);
}
},
Qt::ConnectionType::DirectConnection);
job->start();
}
@ -744,10 +767,14 @@ Cache::deleteSecret(const std::string name)
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false);
job.setInsecureFallback(true);
job.setKey("matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) +
"." + name.c_str());
job.setSettings(UserSettings::instance()->qsettings());
job.setKey(
"matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
// FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
// time!
QEventLoop loop;
@ -765,10 +792,14 @@ Cache::secret(const std::string name)
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false);
job.setInsecureFallback(true);
job.setKey("matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) +
"." + name.c_str());
job.setSettings(UserSettings::instance()->qsettings());
job.setKey(
"matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
// FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
// time!
QEventLoop loop;
@ -838,6 +869,9 @@ Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
bool
Cache::isInitialized()
{
if (!env_.handle())
return false;
auto txn = ro_txn(env_);
std::string_view token;
@ -1563,26 +1597,32 @@ Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
RoomInfo
Cache::singleRoomInfo(const std::string &room_id)
{
auto txn = ro_txn(env_);
auto statesdb = getStatesDb(txn, room_id);
auto txn = ro_txn(env_);
std::string_view data;
try {
auto statesdb = getStatesDb(txn, room_id);
// Check if the room is joined.
if (roomsDb_.get(txn, room_id, data)) {
try {
RoomInfo tmp = json::parse(data);
tmp.member_count = getMembersDb(txn, room_id).size(txn);
tmp.join_rule = getRoomJoinRule(txn, statesdb);
tmp.guest_access = getRoomGuestAccess(txn, statesdb);
std::string_view data;
return tmp;
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
room_id,
std::string(data.data(), data.size()),
e.what());
// Check if the room is joined.
if (roomsDb_.get(txn, room_id, data)) {
try {
RoomInfo tmp = json::parse(data);
tmp.member_count = getMembersDb(txn, room_id).size(txn);
tmp.join_rule = getRoomJoinRule(txn, statesdb);
tmp.guest_access = getRoomGuestAccess(txn, statesdb);
return tmp;
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
room_id,
std::string(data.data(), data.size()),
e.what());
}
}
} catch (const lmdb::error &e) {
nhlog::db()->warn(
"failed to read room info from db: room_id ({}), {}", room_id, e.what());
}
return RoomInfo();
@ -3541,6 +3581,44 @@ Cache::roomMembers(const std::string &room_id)
return members;
}
crypto::Trust
Cache::roomVerificationStatus(const std::string &room_id)
{
crypto::Trust trust = crypto::Verified;
try {
auto txn = lmdb::txn::begin(env_);
auto db = getMembersDb(txn, room_id);
auto keysDb = getUserKeysDb(txn);
std::vector<std::string> keysToRequest;
std::string_view user_id, unused;
auto cursor = lmdb::cursor::open(txn, db);
while (cursor.get(user_id, unused, MDB_NEXT)) {
auto verif = verificationStatus_(std::string(user_id), txn);
if (verif.unverified_device_count) {
trust = crypto::Unverified;
if (verif.verified_devices.empty() && verif.no_keys) {
// we probably don't have the keys yet, so query them
keysToRequest.push_back(std::string(user_id));
}
} else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified)
trust = crypto::TOFU;
}
if (!keysToRequest.empty())
markUserKeysOutOfDate(txn, keysDb, keysToRequest, "");
} catch (std::exception &e) {
nhlog::db()->error(
"Failed to calculate verification status for {}: {}", room_id, e.what());
trust = crypto::Unverified;
}
return trust;
}
std::map<std::string, std::optional<UserKeyCache>>
Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
{
@ -3722,11 +3800,17 @@ from_json(const json &j, UserKeyCache &info)
std::optional<UserKeyCache>
Cache::userKeys(const std::string &user_id)
{
auto txn = ro_txn(env_);
return userKeys_(user_id, txn);
}
std::optional<UserKeyCache>
Cache::userKeys_(const std::string &user_id, lmdb::txn &txn)
{
std::string_view keys;
try {
auto txn = ro_txn(env_);
auto db = getUserKeysDb(txn);
auto res = db.get(txn, user_id, keys);
@ -3735,7 +3819,8 @@ Cache::userKeys(const std::string &user_id)
} else {
return {};
}
} catch (std::exception &) {
} catch (std::exception &e) {
nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
return {};
}
}
@ -3770,8 +3855,14 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
auto last_changed = updateToWrite.last_changed;
// skip if we are tracking this and expect it to be up to date with the last
// sync token
if (!last_changed.empty() && last_changed != sync_token)
if (!last_changed.empty() && last_changed != sync_token) {
nhlog::db()->debug("Not storing update for user {}, because "
"last_changed {}, but we fetched update for {}",
user,
last_changed,
sync_token);
continue;
}
if (!updateToWrite.master_keys.keys.empty() &&
update.master_keys.keys != updateToWrite.master_keys.keys) {
@ -3819,8 +3910,43 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
}
}
if (!keyReused && !oldDeviceKeys.count(device_id))
if (!keyReused && !oldDeviceKeys.count(device_id)) {
// ensure the key has a valid signature from itself
std::string device_signing_key =
"ed25519:" + device_keys.device_id;
if (device_id != device_keys.device_id) {
nhlog::crypto()->warn(
"device {}:{} has a different device id "
"in the body: {}",
user,
device_id,
device_keys.device_id);
continue;
}
if (!device_keys.signatures.count(user) ||
!device_keys.signatures.at(user).count(
device_signing_key)) {
nhlog::crypto()->warn(
"device {}:{} has no signature",
user,
device_id);
continue;
}
if (!mtx::crypto::ed25519_verify_signature(
device_keys.keys.at(device_signing_key),
json(device_keys),
device_keys.signatures.at(user).at(
device_signing_key))) {
nhlog::crypto()->warn(
"device {}:{} has an invalid signature",
user,
device_id);
continue;
}
updateToWrite.device_keys[device_id] = device_keys;
}
}
for (const auto &[key_id, key] : device_keys.keys) {
@ -3830,6 +3956,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
updateToWrite.seen_device_ids.insert(device_id);
}
}
updateToWrite.updated_at = sync_token;
db.put(txn, user, json(updateToWrite).dump());
}
@ -3882,14 +4009,15 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn,
nhlog::db()->debug("Marking user keys out of date: {}", user);
std::string_view oldKeys;
auto res = db.get(txn, user, oldKeys);
if (!res)
continue;
auto cacheEntry =
json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>();
UserKeyCache cacheEntry;
auto res = db.get(txn, user, oldKeys);
if (res) {
cacheEntry = json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
.get<UserKeyCache>();
}
cacheEntry.last_changed = sync_token;
db.put(txn, user, json(cacheEntry).dump());
query.device_keys[user] = {};
@ -3915,35 +4043,46 @@ void
Cache::query_keys(const std::string &user_id,
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb)
{
auto cache_ = cache::userKeys(user_id);
mtx::requests::QueryKeys req;
std::string last_changed;
{
auto txn = ro_txn(env_);
auto cache_ = userKeys_(user_id, txn);
if (cache_.has_value()) {
if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) {
cb(cache_.value(), {});
return;
}
}
if (cache_.has_value()) {
if (cache_->updated_at == cache_->last_changed) {
cb(cache_.value(), {});
return;
} else
nhlog::db()->info("Keys outdated for {}: {} vs {}",
user_id,
cache_->updated_at,
cache_->last_changed);
} else
nhlog::db()->info("No keys found for {}", user_id);
mtx::requests::QueryKeys req;
req.device_keys[user_id] = {};
req.device_keys[user_id] = {};
std::string last_changed;
if (cache_)
last_changed = cache_->last_changed;
req.token = last_changed;
if (cache_)
last_changed = cache_->last_changed;
req.token = last_changed;
}
// use context object so that we can disconnect again
QObject *context{new QObject(this)};
QObject::connect(this,
&Cache::verificationStatusChanged,
context,
[cb, user_id, context_ = context](std::string updated_user) mutable {
if (user_id == updated_user) {
context_->deleteLater();
auto keys = cache::userKeys(user_id);
cb(keys.value_or(UserKeyCache{}), {});
}
});
QObject::connect(
this,
&Cache::verificationStatusChanged,
context,
[cb, user_id, context_ = context, this](std::string updated_user) mutable {
if (user_id == updated_user) {
context_->deleteLater();
auto txn = ro_txn(env_);
auto keys = this->userKeys_(user_id, txn);
cb(keys.value_or(UserKeyCache{}), {});
}
},
Qt::QueuedConnection);
http::client()->query_keys(
req,
@ -3971,17 +4110,16 @@ to_json(json &j, const VerificationCache &info)
void
from_json(const json &j, VerificationCache &info)
{
info.device_verified = j.at("device_verified").get<std::vector<std::string>>();
info.device_blocked = j.at("device_blocked").get<std::vector<std::string>>();
info.device_verified = j.at("device_verified").get<std::set<std::string>>();
info.device_blocked = j.at("device_blocked").get<std::set<std::string>>();
}
std::optional<VerificationCache>
Cache::verificationCache(const std::string &user_id)
Cache::verificationCache(const std::string &user_id, lmdb::txn &txn)
{
std::string_view verifiedVal;
auto txn = lmdb::txn::begin(env_);
auto db = getVerificationDb(txn);
auto db = getVerificationDb(txn);
try {
VerificationCache verified_state;
@ -4000,26 +4138,28 @@ Cache::verificationCache(const std::string &user_id)
void
Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
{
std::string_view val;
{
std::string_view val;
auto txn = lmdb::txn::begin(env_);
auto db = getVerificationDb(txn);
auto txn = lmdb::txn::begin(env_);
auto db = getVerificationDb(txn);
try {
VerificationCache verified_state;
auto res = db.get(txn, user_id, val);
if (res) {
verified_state = json::parse(val);
}
try {
VerificationCache verified_state;
auto res = db.get(txn, user_id, val);
if (res) {
verified_state = json::parse(val);
}
for (const auto &device : verified_state.device_verified)
if (device == key)
return;
for (const auto &device : verified_state.device_verified)
if (device == key)
return;
verified_state.device_verified.push_back(key);
db.put(txn, user_id, json(verified_state).dump());
txn.commit();
} catch (std::exception &) {
verified_state.device_verified.insert(key);
db.put(txn, user_id, json(verified_state).dump());
txn.commit();
} catch (std::exception &) {
}
}
const auto local_user = utils::localUser().toStdString();
@ -4057,11 +4197,7 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
verified_state = json::parse(val);
}
verified_state.device_verified.erase(
std::remove(verified_state.device_verified.begin(),
verified_state.device_verified.end(),
key),
verified_state.device_verified.end());
verified_state.device_verified.erase(key);
db.put(txn, user_id, json(verified_state).dump());
txn.commit();
@ -4090,6 +4226,13 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
VerificationStatus
Cache::verificationStatus(const std::string &user_id)
{
auto txn = ro_txn(env_);
return verificationStatus_(user_id, txn);
}
VerificationStatus
Cache::verificationStatus_(const std::string &user_id, lmdb::txn &txn)
{
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
if (verification_storage.status.count(user_id))
@ -4097,7 +4240,12 @@ Cache::verificationStatus(const std::string &user_id)
VerificationStatus status;
if (auto verifCache = verificationCache(user_id)) {
// assume there is at least one unverified device until we have checked we have the device
// list for that user.
status.unverified_device_count = 1;
status.no_keys = true;
if (auto verifCache = verificationCache(user_id, txn)) {
status.verified_devices = verifCache->device_verified;
}
@ -4105,12 +4253,10 @@ Cache::verificationStatus(const std::string &user_id)
crypto::Trust trustlevel = crypto::Trust::Unverified;
if (user_id == local_user) {
status.verified_devices.push_back(http::client()->device_id());
status.verified_devices.insert(http::client()->device_id());
trustlevel = crypto::Trust::Verified;
}
verification_storage.status[user_id] = status;
auto verifyAtLeastOneSig = [](const auto &toVerif,
const std::map<std::string, std::string> &keys,
const std::string &keyOwner) {
@ -4128,6 +4274,16 @@ Cache::verificationStatus(const std::string &user_id)
return false;
};
auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) {
int currentVerifiedDevices = 0;
for (auto device_id : status.verified_devices) {
if (theirDeviceKeys.count(device_id))
currentVerifiedDevices++;
}
status.unverified_device_count =
static_cast<int>(theirDeviceKeys.size()) - currentVerifiedDevices;
};
try {
// for local user verify this device_key -> our master_key -> our self_signing_key
// -> our device_keys
@ -4137,17 +4293,27 @@ Cache::verificationStatus(const std::string &user_id)
//
// This means verifying the other user adds 2 extra steps,verifying our user_signing
// key and their master key
auto ourKeys = userKeys(local_user);
auto theirKeys = userKeys(user_id);
if (!ourKeys || !theirKeys)
auto ourKeys = userKeys_(local_user, txn);
auto theirKeys = userKeys_(user_id, txn);
if (theirKeys)
status.no_keys = false;
if (!ourKeys || !theirKeys) {
verification_storage.status[user_id] = status;
return status;
}
// Update verified devices count to count without cross-signing
updateUnverifiedDevices(theirKeys->device_keys);
if (!mtx::crypto::ed25519_verify_signature(
olm::client()->identity_keys().ed25519,
json(ourKeys->master_keys),
ourKeys->master_keys.signatures.at(local_user)
.at("ed25519:" + http::client()->device_id())))
.at("ed25519:" + http::client()->device_id()))) {
verification_storage.status[user_id] = status;
return status;
}
auto master_keys = ourKeys->master_keys.keys;
@ -4162,14 +4328,17 @@ Cache::verificationStatus(const std::string &user_id)
trustlevel = crypto::Trust::Verified;
else if (!theirKeys->master_key_changed)
trustlevel = crypto::Trust::TOFU;
else
else {
verification_storage.status[user_id] = status;
return status;
}
master_keys = theirKeys->master_keys.keys;
}
status.user_verified = trustlevel;
verification_storage.status[user_id] = status;
if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
return status;
@ -4180,16 +4349,19 @@ Cache::verificationStatus(const std::string &user_id)
device_key.keys.at("curve25519:" + device_key.device_id);
if (verifyAtLeastOneSig(
device_key, theirKeys->self_signing_keys.keys, user_id)) {
status.verified_devices.push_back(device_key.device_id);
status.verified_devices.insert(device_key.device_id);
status.verified_device_keys[identkey] = trustlevel;
}
} catch (...) {
}
}
updateUnverifiedDevices(theirKeys->device_keys);
verification_storage.status[user_id] = status;
return status;
} catch (std::exception &) {
} catch (std::exception &e) {
nhlog::db()->error(
"Failed to calculate verification status of {}: {}", user_id, e.what());
return status;
}
}

@ -112,9 +112,13 @@ struct VerificationStatus
//! True, if the users master key is verified
crypto::Trust user_verified = crypto::Trust::Unverified;
//! List of all devices marked as verified
std::vector<std::string> verified_devices;
std::set<std::string> verified_devices;
//! Map from sender key/curve25519 to trust status
std::map<std::string, crypto::Trust> verified_device_keys;
//! Count of unverified devices
int unverified_device_count = 0;
// if the keys are not in cache
bool no_keys = false;
};
//! In memory cache of verification status
@ -154,9 +158,9 @@ from_json(const nlohmann::json &j, UserKeyCache &info);
struct VerificationCache
{
//! list of verified device_ids with device-verification
std::vector<std::string> device_verified;
std::set<std::string> device_verified;
//! list of devices the user blocks
std::vector<std::string> device_blocked;
std::set<std::string> device_blocked;
};
void

@ -46,7 +46,6 @@ public:
std::string statusMessage(const std::string &user_id);
// user cache stores user keys
std::optional<UserKeyCache> userKeys(const std::string &user_id);
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
const std::string &room_id,
bool verified_only);
@ -63,9 +62,11 @@ public:
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
// device & user verification cache
std::optional<UserKeyCache> userKeys(const std::string &user_id);
VerificationStatus verificationStatus(const std::string &user_id);
void markDeviceVerified(const std::string &user_id, const std::string &device);
void markDeviceUnverified(const std::string &user_id, const std::string &device);
crypto::Trust roomVerificationStatus(const std::string &room_id);
std::vector<std::string> joinedRooms();
@ -414,24 +415,25 @@ private:
if constexpr (isStateEvent_<decltype(e)>) {
eventsDb.put(txn, e.event_id, json(e).dump());
if (std::is_same_v<
std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
StateEvent<mtx::events::msg::Redacted>>) {
if (e.type == EventType::RoomMember)
membersdb.del(txn, e.state_key, "");
else if (e.state_key.empty())
statesdb.del(txn, to_string(e.type));
else
stateskeydb.del(
txn,
to_string(e.type),
json::object({
{"key", e.state_key},
{"id", e.event_id},
})
.dump());
} else if (e.type != EventType::Unsupported) {
if (e.state_key.empty())
if (e.type != EventType::Unsupported) {
if (std::is_same_v<
std::remove_cv_t<
std::remove_reference_t<decltype(e)>>,
StateEvent<mtx::events::msg::Redacted>>) {
if (e.type == EventType::RoomMember)
membersdb.del(txn, e.state_key, "");
else if (e.state_key.empty())
statesdb.del(txn, to_string(e.type));
else
stateskeydb.del(
txn,
to_string(e.type),
json::object({
{"key", e.state_key},
{"id", e.event_id},
})
.dump());
} else if (e.state_key.empty())
statesdb.put(
txn, to_string(e.type), json(e).dump());
else
@ -680,7 +682,10 @@ private:
return QString::fromStdString(event.state_key);
}
std::optional<VerificationCache> verificationCache(const std::string &user_id);
std::optional<VerificationCache> verificationCache(const std::string &user_id,
lmdb::txn &txn);
VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn);
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
void setNextBatchToken(lmdb::txn &txn, const QString &token);

@ -4,11 +4,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication>
#include <QImageReader>
#include <QInputDialog>
#include <QMessageBox>
#include <QSettings>
#include <QShortcut>
#include <mtx/responses.hpp>

@ -17,10 +17,8 @@
#include <mtx/events/presence.hpp>
#include <mtx/secret_storage.hpp>
#include <QFrame>
#include <QHBoxLayout>
#include <QMap>
#include <QPixmap>
#include <QPoint>
#include <QTimer>
#include <QWidget>

@ -53,6 +53,7 @@ MemberList::roleNames() const
{Mxid, "mxid"},
{DisplayName, "displayName"},
{AvatarUrl, "avatarUrl"},
{Trustlevel, "trustlevel"},
};
}
@ -69,6 +70,17 @@ MemberList::data(const QModelIndex &index, int role) const
return m_memberList[index.row()].first.display_name;
case AvatarUrl:
return m_memberList[index.row()].second;
case Trustlevel: {
auto stat =
cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString());
if (!stat)
return crypto::Unverified;
if (stat->unverified_device_count)
return crypto::Unverified;
else
return stat->user_verified;
}
default:
return {};
}

@ -25,6 +25,7 @@ public:
Mxid,
DisplayName,
AvatarUrl,
Trustlevel,
};
MemberList(const QString &room_id, QObject *parent = nullptr);

@ -11,6 +11,8 @@
#include <QByteArray>
#include <QDir>
#include <QFileInfo>
#include <QPainter>
#include <QPainterPath>
#include <QStandardPaths>
#include "Logging.h"
@ -22,14 +24,26 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
QQuickImageResponse *
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
auto id_ = id;
bool crop = true;
if (id.endsWith("?scale")) {
crop = false;
id_.remove("?scale");
auto id_ = id;
bool crop = true;
double radius = 0;
auto queryStart = id.lastIndexOf('?');
if (queryStart != -1) {
id_ = id.left(queryStart);
auto query = id.midRef(queryStart + 1);
auto queryBits = query.split('&');
for (auto b : queryBits) {
if (b == "scale") {
crop = false;
} else if (b.startsWith("radius=")) {
radius = b.mid(7).toDouble();
}
}
}
MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize);
pool.start(response);
return response;
}
@ -53,14 +67,35 @@ MxcImageResponse::run()
}
emit finished();
},
m_crop);
m_crop,
m_radius);
}
static QImage
clipRadius(QImage img, double radius)
{
QImage out(img.size(), QImage::Format_ARGB32_Premultiplied);
out.fill(Qt::transparent);
QPainter painter(&out);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
QPainterPath ppath;
ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
painter.setClipPath(ppath);
painter.drawImage(img.rect(), img);
return out;
}
void
MxcImageProvider::download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then,
bool crop)
bool crop,
double radius)
{
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id);
@ -69,12 +104,13 @@ MxcImageProvider::download(const QString &id,
if (requestedSize.isValid() && !encryptionInfo) {
QString fileName =
QString("%1_%2x%3_%4")
QString("%1_%2x%3_%4_radius%5")
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width())
.arg(requestedSize.height())
.arg(crop ? "crop" : "scale");
.arg(crop ? "crop" : "scale")
.arg(radius);
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
@ -86,6 +122,10 @@ MxcImageProvider::download(const QString &id,
image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
if (!image.isNull()) {
then(id, requestedSize, image, fileInfo.absoluteFilePath());
return;
@ -100,8 +140,8 @@ MxcImageProvider::download(const QString &id,
opts.method = crop ? "crop" : "scale";
http::client()->get_thumbnail(
opts,
[fileInfo, requestedSize, then, id](const std::string &res,
mtx::http::RequestErr err) {
[fileInfo, requestedSize, radius, then, id](const std::string &res,
mtx::http::RequestErr err) {
if (err || res.empty()) {
then(id, QSize(), {}, "");
@ -113,6 +153,10 @@ MxcImageProvider::download(const QString &id,
if (!image.isNull()) {
image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
}
image.setText("mxc url", "mxc://" + id);
if (image.save(fileInfo.absoluteFilePath(), "png"))
@ -126,8 +170,12 @@ MxcImageProvider::download(const QString &id,
});
} else {
try {
QString fileName = QString::fromUtf8(id.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
QString fileName =
QString("%1_radius%2")
.arg(QString::fromUtf8(id.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
.arg(radius);
QFileInfo fileInfo(
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
@ -148,6 +196,11 @@ MxcImageProvider::download(const QString &id,
QImage image = utils::readImage(data);
image.setText("mxc url", "mxc://" + id);
if (!image.isNull()) {
if (radius != 0) {
image =
clipRadius(std::move(image), radius);
}
then(id,
requestedSize,
image,
@ -158,6 +211,11 @@ MxcImageProvider::download(const QString &id,
QImage image =
utils::readImageFromFile(fileInfo.absoluteFilePath());
if (!image.isNull()) {
if (radius != 0) {
image =
clipRadius(std::move(image), radius);
}
then(id,
requestedSize,
image,
@ -169,7 +227,7 @@ MxcImageProvider::download(const QString &id,
http::client()->download(
"mxc://" + id.toStdString(),
[fileInfo, requestedSize, then, id, encryptionInfo](
[fileInfo, requestedSize, then, id, radius, encryptionInfo](
const std::string &res,
const std::string &,
const std::string &originalFilename,
@ -195,6 +253,10 @@ MxcImageProvider::download(const QString &id,
auto data =
QByteArray(tempData.data(), (int)tempData.size());
QImage image = utils::readImage(data);
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
image.setText("original filename",
QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id);
@ -205,6 +267,10 @@ MxcImageProvider::download(const QString &id,
QImage image =
utils::readImageFromFile(fileInfo.absoluteFilePath());
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
image.setText("original filename",
QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id);

@ -19,10 +19,11 @@ class MxcImageResponse
, public QRunnable
{
public:
MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
, m_crop(crop)
, m_radius(radius)
{
setAutoDelete(false);
}
@ -39,6 +40,7 @@ public:
QSize m_requestedSize;
QImage m_image;
bool m_crop;
double m_radius;
};
class MxcImageProvider
@ -54,7 +56,8 @@ public slots:
static void download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then,
bool crop = true);
bool crop = true,
double radius = 0);
private:
QThreadPool pool;

@ -425,6 +425,8 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
}
});
nhlog::crypto()->info("Storing secret {}",
secret_name->second);
cache::client()->storeSecret(secret_name->second,
e->content.secret);
@ -1110,6 +1112,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
const mtx::events::collections::DeviceEvents &event,
bool force_new_session)
{
static QMap<QPair<std::string, std::string>, qint64> rateLimit;
nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event);
std::map<std::string, std::vector<std::string>> keysToQuery;
@ -1162,7 +1166,6 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
auto session = cache::getLatestOlmSession(device_curve);
if (!session || force_new_session) {
static QMap<QPair<std::string, std::string>, qint64> rateLimit;
auto currentTime = QDateTime::currentSecsSinceEpoch();
if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
currentTime) {
@ -1318,7 +1321,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
};
};
http::client()->claim_keys(claims, BindPks(pks));
if (!claims.one_time_keys.empty())
http::client()->claim_keys(claims, BindPks(pks));
if (!keysToQuery.empty()) {
mtx::requests::QueryKeys req;
@ -1395,9 +1399,25 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
continue;
}
deviceKeys[user_id].emplace(device_id, pks);
claim_keys.one_time_keys[user.first][device_id] =
mtx::crypto::SIGNED_CURVE25519;
auto currentTime = QDateTime::currentSecsSinceEpoch();
if (rateLimit.value(QPair(user.first, device_id.get())) +
60 * 60 * 10 <
currentTime) {
deviceKeys[user_id].emplace(device_id, pks);
claim_keys.one_time_keys[user.first][device_id] =
mtx::crypto::SIGNED_CURVE25519;
rateLimit.insert(
QPair(user.first, device_id.get()),
currentTime);
} else {
nhlog::crypto()->warn(
"Not creating new session with {}:{} "
"because of rate limit",
user.first,
device_id.get());
continue;
}
nhlog::net()->info("{}", device_id.get());
nhlog::net()->info(" curve25519 {}", pks.curve25519);
@ -1405,7 +1425,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
}
}
http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
if (!claim_keys.one_time_keys.empty())
http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
});
}
}

@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QInputDialog>
#include <QLabel>
#include <QMetaType>
#include <QPainter>
@ -481,6 +482,23 @@ RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
doRegistrationWithAuth(
mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}});
} else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
bool ok;
QString token =
QInputDialog::getText(this,
tr("Registration token"),
tr("Please enter a valid registration token."),
QLineEdit::Normal,
QString(),
&ok);
if (ok) {
emit registrationWithAuth(mtx::user_interactive::Auth{
session,
mtx::user_interactive::auth::RegistrationToken{token.toStdString()}});
} else {
emit errorOccurred();
}
} else {
// use fallback
auto dialog = new dialogs::FallbackAuth(

@ -19,7 +19,6 @@
#include <QResizeEvent>
#include <QScrollArea>
#include <QScroller>
#include <QSettings>
#include <QSpinBox>
#include <QStandardPaths>
#include <QString>
@ -63,7 +62,6 @@ UserSettings::initialize(std::optional<QString> profile)
void
UserSettings::load(std::optional<QString> profile)
{
QSettings settings;
tray_ = settings.value("user/window/tray", false).toBool();
startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
@ -601,7 +599,6 @@ UserSettings::applyTheme()
void
UserSettings::save()
{
QSettings settings;
settings.beginGroup("user");
settings.beginGroup("window");

@ -8,6 +8,7 @@
#include <QFontDatabase>
#include <QFrame>
#include <QProcessEnvironment>
#include <QSettings>
#include <QSharedPointer>
#include <QWidget>
@ -107,6 +108,8 @@ public:
static QSharedPointer<UserSettings> instance();
static void initialize(std::optional<QString> profile);
QSettings *qsettings() { return &settings; }
enum class Presence
{
AutomaticPresence,
@ -316,6 +319,8 @@ private:
QString homeserver_;
QStringList hiddenTags_;
QSettings settings;
static QSharedPointer<UserSettings> instance_;
};

@ -28,8 +28,10 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_DeleteOnClose, true);
setWindowState(Qt::WindowFullScreen);
close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this);
connect(this, SIGNAL(closing()), this, SLOT(close()));
connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing);
connect(this, &ImageOverlay::closing, this, &ImageOverlay::close);
raise();
}

@ -8,6 +8,7 @@
#include <QDialog>
#include <QMouseEvent>
#include <QPixmap>
#include <QShortcut>
namespace dialogs {
@ -32,5 +33,6 @@ private:
QRect content_;
QRect close_button_;
QRect save_button_;
QShortcut *close_shortcut_;
};
} // dialogs

@ -310,7 +310,7 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
return mtx::events::EventType::RoomMessage;
//! m.image_pack, currently im.ponies.room_emotes
case qml_mtx_events::ImagePackInRoom:
return mtx::events::EventType::ImagePackRooms;
return mtx::events::EventType::ImagePackInRoom;
//! m.image_pack, currently im.ponies.user_emotes
case qml_mtx_events::ImagePackInAccountData:
return mtx::events::EventType::ImagePackInAccountData;
@ -418,6 +418,14 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
&events,
&EventStore::enableKeyRequests);
connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged);
connect(
this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged);
connect(cache::client(),
&Cache::verificationStatusChanged,
this,
&TimelineModel::trustlevelChanged);
showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
}
@ -1993,6 +2001,15 @@ TimelineModel::roomTopic() const
QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
}
crypto::Trust
TimelineModel::trustlevel() const
{
if (!isEncrypted_)
return crypto::Trust::Unverified;
return cache::client()->roomVerificationStatus(room_id_.toStdString());
}
int
TimelineModel::roomMemberCount() const
{

@ -175,6 +175,7 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged)
Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged)
Q_PROPERTY(InputBar *input READ input CONSTANT)
Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged)
@ -287,6 +288,7 @@ public:
DescInfo lastMessage() const { return lastMessage_; }
bool isSpace() const { return isSpace_; }
bool isEncrypted() const { return isEncrypted_; }
crypto::Trust trustlevel() const;
int roomMemberCount() const;
public slots:
@ -372,6 +374,7 @@ signals:
void updateFlowEventId(std::string event_id);
void encryptionChanged();
void trustlevelChanged();
void roomNameChanged();
void plainRoomNameChanged();
void roomTopicChanged();

@ -375,10 +375,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
}
void
TimelineViewManager::openRoomMembers(QString room_id)
TimelineViewManager::openRoomMembers(TimelineModel *room)
{
MemberList *memberList = new MemberList(room_id, this);
emit openRoomMembersDialog(memberList);
if (!room)
return;
MemberList *memberList = new MemberList(room->roomId(), this);
emit openRoomMembersDialog(memberList, room);
}
void

@ -66,7 +66,7 @@ public:
Q_INVOKABLE QString userPresence(QString id) const;
Q_INVOKABLE QString userStatus(QString id) const;
Q_INVOKABLE void openRoomMembers(QString room_id);
Q_INVOKABLE void openRoomMembers(TimelineModel *room);
Q_INVOKABLE void openRoomSettings(QString room_id);
Q_INVOKABLE void openInviteUsers(QString roomId);
Q_INVOKABLE void openGlobalUserProfile(QString userId);
@ -92,7 +92,7 @@ signals:
void focusChanged();
void focusInput();
void openImageOverlayInternalCb(QString eventId, QImage img);
void openRoomMembersDialog(MemberList *members);
void openRoomMembersDialog(MemberList *members, TimelineModel *room);
void openRoomSettingsDialog(RoomSettings *settings);
void openInviteUsersDialog(InviteesModel *invitees);
void openProfile(UserProfile *profile);

Loading…
Cancel
Save