Merge pull request #270 from Chethan2k1/device-verification
Device verification and Cross-Signing
This commit is contained in:
commit
517a126a44
32
.travis.yml
32
.travis.yml
@ -63,21 +63,21 @@ matrix:
|
|||||||
env:
|
env:
|
||||||
- CXX=g++-8
|
- CXX=g++-8
|
||||||
- CC=gcc-8
|
- CC=gcc-8
|
||||||
- QT_PKG=59
|
- QT_PKG=510
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- ubuntu-toolchain-r-test
|
- ubuntu-toolchain-r-test
|
||||||
- sourceline: 'ppa:beineri/opt-qt597-xenial'
|
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial'
|
||||||
packages:
|
packages:
|
||||||
- g++-8
|
- g++-8
|
||||||
- ninja-build
|
- ninja-build
|
||||||
- qt59base
|
- qt510base
|
||||||
- qt59tools
|
- qt510tools
|
||||||
- qt59svg
|
- qt510svg
|
||||||
- qt59multimedia
|
- qt510multimedia
|
||||||
- qt59quickcontrols2
|
- qt510quickcontrols2
|
||||||
- qt59graphicaleffects
|
- qt510graphicaleffects
|
||||||
- liblmdb-dev
|
- liblmdb-dev
|
||||||
- libgl1-mesa-dev # needed for missing gl.h
|
- libgl1-mesa-dev # needed for missing gl.h
|
||||||
- os: linux
|
- os: linux
|
||||||
@ -85,23 +85,23 @@ matrix:
|
|||||||
env:
|
env:
|
||||||
- CXX=clang++-6.0
|
- CXX=clang++-6.0
|
||||||
- CC=clang-6.0
|
- CC=clang-6.0
|
||||||
- QT_PKG=59
|
- QT_PKG=510
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- ubuntu-toolchain-r-test
|
- ubuntu-toolchain-r-test
|
||||||
- llvm-toolchain-xenial-6.0
|
- llvm-toolchain-xenial-6.0
|
||||||
- sourceline: 'ppa:beineri/opt-qt597-xenial'
|
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial'
|
||||||
packages:
|
packages:
|
||||||
- clang++-6.0
|
- clang++-6.0
|
||||||
- g++-7
|
- g++-7
|
||||||
- ninja-build
|
- ninja-build
|
||||||
- qt59base
|
- qt510base
|
||||||
- qt59tools
|
- qt510tools
|
||||||
- qt59svg
|
- qt510svg
|
||||||
- qt59multimedia
|
- qt510multimedia
|
||||||
- qt59quickcontrols2
|
- qt510quickcontrols2
|
||||||
- qt59graphicaleffects
|
- qt510graphicaleffects
|
||||||
- liblmdb-dev
|
- liblmdb-dev
|
||||||
- libgl1-mesa-dev # needed for missing gl.h
|
- libgl1-mesa-dev # needed for missing gl.h
|
||||||
- os: linux
|
- os: linux
|
||||||
|
@ -142,9 +142,9 @@ if (APPLE)
|
|||||||
endif(APPLE)
|
endif(APPLE)
|
||||||
|
|
||||||
if (Qt5Widgets_FOUND)
|
if (Qt5Widgets_FOUND)
|
||||||
if (Qt5Widgets_VERSION VERSION_LESS 5.9.0)
|
if (Qt5Widgets_VERSION VERSION_LESS 5.10.0)
|
||||||
message(STATUS "Qt version ${Qt5Widgets_VERSION}")
|
message(STATUS "Qt version ${Qt5Widgets_VERSION}")
|
||||||
message(WARNING "Minimum supported Qt5 version is 5.9!")
|
message(WARNING "Minimum supported Qt5 version is 5.10!")
|
||||||
endif()
|
endif()
|
||||||
endif(Qt5Widgets_FOUND)
|
endif(Qt5Widgets_FOUND)
|
||||||
|
|
||||||
@ -239,7 +239,6 @@ set(SRC_FILES
|
|||||||
src/dialogs/ReCaptcha.cpp
|
src/dialogs/ReCaptcha.cpp
|
||||||
src/dialogs/ReadReceipts.cpp
|
src/dialogs/ReadReceipts.cpp
|
||||||
src/dialogs/RoomSettings.cpp
|
src/dialogs/RoomSettings.cpp
|
||||||
src/dialogs/UserProfile.cpp
|
|
||||||
|
|
||||||
# Emoji
|
# Emoji
|
||||||
src/emoji/Category.cpp
|
src/emoji/Category.cpp
|
||||||
@ -278,6 +277,7 @@ set(SRC_FILES
|
|||||||
src/ui/ToggleButton.cpp
|
src/ui/ToggleButton.cpp
|
||||||
src/ui/Theme.cpp
|
src/ui/Theme.cpp
|
||||||
src/ui/ThemeManager.cpp
|
src/ui/ThemeManager.cpp
|
||||||
|
src/ui/UserProfile.cpp
|
||||||
|
|
||||||
src/AvatarProvider.cpp
|
src/AvatarProvider.cpp
|
||||||
src/BlurhashProvider.cpp
|
src/BlurhashProvider.cpp
|
||||||
@ -287,6 +287,7 @@ set(SRC_FILES
|
|||||||
src/ColorImageProvider.cpp
|
src/ColorImageProvider.cpp
|
||||||
src/CommunitiesList.cpp
|
src/CommunitiesList.cpp
|
||||||
src/CommunitiesListItem.cpp
|
src/CommunitiesListItem.cpp
|
||||||
|
src/DeviceVerificationFlow.cpp
|
||||||
src/EventAccessors.cpp
|
src/EventAccessors.cpp
|
||||||
src/InviteeItem.cpp
|
src/InviteeItem.cpp
|
||||||
src/Logging.cpp
|
src/Logging.cpp
|
||||||
@ -339,7 +340,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
MatrixClient
|
MatrixClient
|
||||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
||||||
GIT_TAG 21a55ba65d0712a441fbef2af2ede66771430247
|
GIT_TAG ad5575bc24089dc385e97d9ace026414b618775c
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(MatrixClient)
|
FetchContent_MakeAvailable(MatrixClient)
|
||||||
else()
|
else()
|
||||||
@ -451,7 +452,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/dialogs/ReCaptcha.h
|
src/dialogs/ReCaptcha.h
|
||||||
src/dialogs/ReadReceipts.h
|
src/dialogs/ReadReceipts.h
|
||||||
src/dialogs/RoomSettings.h
|
src/dialogs/RoomSettings.h
|
||||||
src/dialogs/UserProfile.h
|
|
||||||
|
|
||||||
# Emoji
|
# Emoji
|
||||||
src/emoji/Category.h
|
src/emoji/Category.h
|
||||||
@ -487,6 +487,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/ui/ToggleButton.h
|
src/ui/ToggleButton.h
|
||||||
src/ui/Theme.h
|
src/ui/Theme.h
|
||||||
src/ui/ThemeManager.h
|
src/ui/ThemeManager.h
|
||||||
|
src/ui/UserProfile.h
|
||||||
|
|
||||||
src/notifications/Manager.h
|
src/notifications/Manager.h
|
||||||
|
|
||||||
@ -497,6 +498,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/ChatPage.h
|
src/ChatPage.h
|
||||||
src/CommunitiesList.h
|
src/CommunitiesList.h
|
||||||
src/CommunitiesListItem.h
|
src/CommunitiesListItem.h
|
||||||
|
src/DeviceVerificationFlow.h
|
||||||
src/InviteeItem.h
|
src/InviteeItem.h
|
||||||
src/LoginPage.h
|
src/LoginPage.h
|
||||||
src/MainWindow.h
|
src/MainWindow.h
|
||||||
|
@ -174,7 +174,7 @@ sudo pacman -S qt5-base \
|
|||||||
##### Gentoo Linux
|
##### Gentoo Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig
|
sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Ubuntu 20.04
|
##### Ubuntu 20.04
|
||||||
|
@ -73,9 +73,9 @@
|
|||||||
],
|
],
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"sha256": "3dbcbfd8c07e25f5e0d662b194d3a7772ef214358c49ada23c044c4747ce8b19",
|
"sha256": "5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb",
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://github.com/gabime/spdlog/archive/v1.1.0.tar.gz"
|
"url": "https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -146,7 +146,7 @@
|
|||||||
"name": "mtxclient",
|
"name": "mtxclient",
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"commit": "21a55ba65d0712a441fbef2af2ede66771430247",
|
"commit": "ad5575bc24089dc385e97d9ace026414b618775c",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
|
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import im.nheko 1.0
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: activeCallBar
|
id: activeCallBar
|
||||||
visible: timelineManager.callState != WebRTCState.DISCONNECTED
|
visible: TimelineManager.callState != WebRTCState.DISCONNECTED
|
||||||
color: "#2ECC71"
|
color: "#2ECC71"
|
||||||
implicitHeight: rowLayout.height + 8
|
implicitHeight: rowLayout.height + 8
|
||||||
|
|
||||||
@ -21,13 +21,13 @@ Rectangle {
|
|||||||
width: avatarSize
|
width: avatarSize
|
||||||
height: avatarSize
|
height: avatarSize
|
||||||
|
|
||||||
url: timelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||||
displayName: timelineManager.callPartyName
|
displayName: TimelineManager.callPartyName
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||||
text: " " + timelineManager.callPartyName + " "
|
text: " " + TimelineManager.callPartyName + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
@ -42,7 +42,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: timelineManager
|
target: TimelineManager
|
||||||
function onCallStateChanged(state) {
|
function onCallStateChanged(state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case WebRTCState.INITIATING:
|
case WebRTCState.INITIATING:
|
||||||
@ -69,7 +69,7 @@ Rectangle {
|
|||||||
id: callTimer
|
id: callTimer
|
||||||
property int startTime
|
property int startTime
|
||||||
interval: 1000
|
interval: 1000
|
||||||
running: timelineManager.callState == WebRTCState.CONNECTED
|
running: TimelineManager.callState == WebRTCState.CONNECTED
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
var d = new Date()
|
var d = new Date()
|
||||||
@ -93,15 +93,15 @@ Rectangle {
|
|||||||
width: 24
|
width: 24
|
||||||
height: 24
|
height: 24
|
||||||
buttonTextColor: "#000000"
|
buttonTextColor: "#000000"
|
||||||
image: timelineManager.isMicMuted ?
|
image: TimelineManager.isMicMuted ?
|
||||||
":/icons/icons/ui/microphone-unmute.png" :
|
":/icons/icons/ui/microphone-unmute.png" :
|
||||||
":/icons/icons/ui/microphone-mute.png"
|
":/icons/icons/ui/microphone-mute.png"
|
||||||
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: timelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
|
ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
|
||||||
|
|
||||||
onClicked: timelineManager.toggleMicMute()
|
onClicked: TimelineManager.toggleMicMute()
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
@ -2,11 +2,13 @@ import QtQuick 2.6
|
|||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtGraphicalEffects 1.0
|
import QtGraphicalEffects 1.0
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: avatar
|
id: avatar
|
||||||
width: 48
|
width: 48
|
||||||
height: 48
|
height: 48
|
||||||
radius: settings.avatarCircles ? height/2 : 3
|
radius: Settings.avatarCircles ? height/2 : 3
|
||||||
|
|
||||||
property alias url: img.source
|
property alias url: img.source
|
||||||
property string userid
|
property string userid
|
||||||
@ -14,7 +16,7 @@ Rectangle {
|
|||||||
|
|
||||||
Label {
|
Label {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
text: timelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
|
text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
font.pixelSize: avatar.height/2
|
font.pixelSize: avatar.height/2
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
@ -40,7 +42,7 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
width: avatar.width
|
width: avatar.width
|
||||||
height: avatar.height
|
height: avatar.height
|
||||||
radius: settings.avatarCircles ? height/2 : 3
|
radius: Settings.avatarCircles ? height/2 : 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,8 +56,8 @@ Rectangle {
|
|||||||
|
|
||||||
height: avatar.height / 6
|
height: avatar.height / 6
|
||||||
width: height
|
width: height
|
||||||
radius: settings.avatarCircles ? height / 2 : height / 4
|
radius: Settings.avatarCircles ? height / 2 : height / 4
|
||||||
color: switch (timelineManager.userPresence(userid)) {
|
color: switch (TimelineManager.userPresence(userid)) {
|
||||||
case "online": return "#00cc66"
|
case "online": return "#00cc66"
|
||||||
case "unavailable": return "#ff9933"
|
case "unavailable": return "#ff9933"
|
||||||
case "offline": // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled
|
case "offline": // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import QtQuick 2.5
|
import QtQuick 2.5
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
textFormat: TextEdit.RichText
|
textFormat: TextEdit.RichText
|
||||||
readOnly: true
|
readOnly: true
|
||||||
@ -11,13 +13,13 @@ TextEdit {
|
|||||||
|
|
||||||
onLinkActivated: {
|
onLinkActivated: {
|
||||||
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1])
|
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1])
|
||||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1])
|
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1])
|
||||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
|
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
|
||||||
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link)
|
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link)
|
||||||
timelineManager.setHistoryView(match[1])
|
TimelineManager.setHistoryView(match[1])
|
||||||
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain)
|
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain)
|
||||||
}
|
}
|
||||||
else timelineManager.openLink(link)
|
else TimelineManager.openLink(link)
|
||||||
}
|
}
|
||||||
MouseArea
|
MouseArea
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
// This class is for showing Reactions in the timeline row, not for
|
// This class is for showing Reactions in the timeline row, not for
|
||||||
// adding new reactions via the emoji picker
|
// adding new reactions via the emoji picker
|
||||||
Flow {
|
Flow {
|
||||||
@ -12,7 +14,6 @@ Flow {
|
|||||||
property real highlightLight: colors.highlight.hslLightness
|
property real highlightLight: colors.highlight.hslLightness
|
||||||
|
|
||||||
property string eventId
|
property string eventId
|
||||||
property string roomId
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@ -33,8 +34,8 @@ Flow {
|
|||||||
ToolTip.text: modelData.users
|
ToolTip.text: modelData.users
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + " in room " + reactionFlow.roomId + ". selfReactedEvent: " + modelData.selfReactedEvent)
|
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent)
|
||||||
timelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key)
|
TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ Flow {
|
|||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: textMetrics
|
id: textMetrics
|
||||||
font.family: settings.emojiFont
|
font.family: Settings.emojiFont
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
elideWidth: 150
|
elideWidth: 150
|
||||||
text: modelData.key
|
text: modelData.key
|
||||||
@ -56,7 +57,7 @@ Flow {
|
|||||||
anchors.baseline: reactionCounter.baseline
|
anchors.baseline: reactionCounter.baseline
|
||||||
id: reactionText
|
id: reactionText
|
||||||
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…")
|
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…")
|
||||||
font.family: settings.emojiFont
|
font.family: Settings.emojiFont
|
||||||
color: reaction.hovered ? colors.highlight : colors.text
|
color: reaction.hovered ? colors.highlight : colors.text
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
color: (settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent"
|
color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent"
|
||||||
anchors.fill: row
|
anchors.fill: row
|
||||||
}
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@ -49,7 +49,7 @@ Item {
|
|||||||
Reply {
|
Reply {
|
||||||
visible: model.replyTo
|
visible: model.replyTo
|
||||||
modelData: chat.model.getDump(model.replyTo,model.id)
|
modelData: chat.model.getDump(model.replyTo,model.id)
|
||||||
userColor: timelineManager.userColor(modelData.userId, colors.window)
|
userColor: TimelineManager.userColor(modelData.userId, colors.window)
|
||||||
}
|
}
|
||||||
|
|
||||||
// actual message content
|
// actual message content
|
||||||
@ -64,7 +64,6 @@ Item {
|
|||||||
Reactions {
|
Reactions {
|
||||||
id: reactionRow
|
id: reactionRow
|
||||||
reactions: model.reactions
|
reactions: model.reactions
|
||||||
roomId: model.roomId
|
|
||||||
eventId: model.id
|
eventId: model.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,7 +83,7 @@ Item {
|
|||||||
width: 16
|
width: 16
|
||||||
}
|
}
|
||||||
EmojiButton {
|
EmojiButton {
|
||||||
visible: settings.buttonsInTimeline
|
visible: Settings.buttonsInTimeline
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||||
Layout.preferredHeight: 16
|
Layout.preferredHeight: 16
|
||||||
width: 16
|
width: 16
|
||||||
@ -96,7 +95,7 @@ Item {
|
|||||||
event_id: model.id
|
event_id: model.id
|
||||||
}
|
}
|
||||||
ImageButton {
|
ImageButton {
|
||||||
visible: settings.buttonsInTimeline
|
visible: Settings.buttonsInTimeline
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||||
Layout.preferredHeight: 16
|
Layout.preferredHeight: 16
|
||||||
width: 16
|
width: 16
|
||||||
@ -112,7 +111,7 @@ Item {
|
|||||||
onClicked: chat.model.replyAction(model.id)
|
onClicked: chat.model.replyAction(model.id)
|
||||||
}
|
}
|
||||||
ImageButton {
|
ImageButton {
|
||||||
visible: settings.buttonsInTimeline
|
visible: Settings.buttonsInTimeline
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||||
Layout.preferredHeight: 16
|
Layout.preferredHeight: 16
|
||||||
width: 16
|
width: 16
|
||||||
|
@ -9,6 +9,7 @@ import im.nheko.EmojiModel 1.0
|
|||||||
|
|
||||||
import "./delegates"
|
import "./delegates"
|
||||||
import "./emoji"
|
import "./emoji"
|
||||||
|
import "./device-verification"
|
||||||
|
|
||||||
Page {
|
Page {
|
||||||
id: timelineRoot
|
id: timelineRoot
|
||||||
@ -90,7 +91,7 @@ Page {
|
|||||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
|
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
text: qsTr("Save as")
|
text: qsTr("Save as")
|
||||||
onTriggered: timelineManager.timeline.saveMedia(messageContextMenu.eventId)
|
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,8 +99,27 @@ Page {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: colors.window
|
color: colors.window
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: deviceVerificationDialog
|
||||||
|
DeviceVerification {}
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: TimelineManager
|
||||||
|
function onNewDeviceVerificationRequest(flow,transactionId,userId,deviceId,isRequest) {
|
||||||
|
var dialog = deviceVerificationDialog.createObject(timelineRoot, {flow: flow});
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: TimelineManager.timeline
|
||||||
|
function onOpenProfile(profile) {
|
||||||
|
var userProfile = userProfileComponent.createObject(timelineRoot,{profile: profile});
|
||||||
|
userProfile.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
visible: !timelineManager.timeline && !timelineManager.isInitialSync
|
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: qsTr("No room open")
|
text: qsTr("No room open")
|
||||||
font.pointSize: 24
|
font.pointSize: 24
|
||||||
@ -109,7 +129,7 @@ Page {
|
|||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
visible: running
|
visible: running
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
running: timelineManager.isInitialSync
|
running: TimelineManager.isInitialSync
|
||||||
height: 200
|
height: 200
|
||||||
width: 200
|
width: 200
|
||||||
z: 3
|
z: 3
|
||||||
@ -128,7 +148,7 @@ Page {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: timelineManager.openRoomSettings();
|
onClicked: TimelineManager.openRoomSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
@ -149,14 +169,14 @@ Page {
|
|||||||
Layout.rowSpan: 2
|
Layout.rowSpan: 2
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
visible: timelineManager.isNarrowView
|
visible: TimelineManager.isNarrowView
|
||||||
|
|
||||||
image: ":/icons/icons/ui/angle-pointing-to-left.png"
|
image: ":/icons/icons/ui/angle-pointing-to-left.png"
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: qsTr("Back to room list")
|
ToolTip.text: qsTr("Back to room list")
|
||||||
|
|
||||||
onClicked: timelineManager.backToRooms()
|
onClicked: TimelineManager.backToRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
Avatar {
|
Avatar {
|
||||||
@ -173,7 +193,7 @@ Page {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: timelineManager.openRoomSettings();
|
onClicked: TimelineManager.openRoomSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +201,7 @@ Page {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.column: 2
|
Layout.column: 2
|
||||||
Layout.row: 0
|
Layout.row: 0
|
||||||
|
color: colors.text
|
||||||
|
|
||||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||||
|
|
||||||
@ -188,7 +209,7 @@ Page {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: timelineManager.openRoomSettings();
|
onClicked: TimelineManager.openRoomSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MatrixText {
|
MatrixText {
|
||||||
@ -220,19 +241,19 @@ Page {
|
|||||||
id: roomOptionsMenu
|
id: roomOptionsMenu
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Invite users")
|
text: qsTr("Invite users")
|
||||||
onTriggered: timelineManager.openInviteUsersDialog();
|
onTriggered: TimelineManager.openInviteUsersDialog();
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Members")
|
text: qsTr("Members")
|
||||||
onTriggered: timelineManager.openMemberListDialog();
|
onTriggered: TimelineManager.openMemberListDialog();
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Leave room")
|
text: qsTr("Leave room")
|
||||||
onTriggered: timelineManager.openLeaveRoomDialog();
|
onTriggered: TimelineManager.openLeaveRoomDialog();
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Settings")
|
text: qsTr("Settings")
|
||||||
onTriggered: timelineManager.openRoomSettings();
|
onTriggered: TimelineManager.openRoomSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,14 +263,14 @@ Page {
|
|||||||
ListView {
|
ListView {
|
||||||
id: chat
|
id: chat
|
||||||
|
|
||||||
visible: !!timelineManager.timeline
|
visible: TimelineManager.timeline != null
|
||||||
|
|
||||||
cacheBuffer: 400
|
cacheBuffer: 400
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
model: timelineManager.timeline
|
model: TimelineManager.timeline
|
||||||
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
@ -293,7 +314,7 @@ Page {
|
|||||||
|
|
||||||
onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom
|
onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom
|
||||||
|
|
||||||
property int delegateMaxWidth: (settings.timelineMaxWidth > 100 && (parent.width - settings.timelineMaxWidth) > scrollbar.width*2) ? settings.timelineMaxWidth : (parent.width - scrollbar.width*2)
|
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width*2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width*2)
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
// This would normally be previousSection, but our model's order is inverted.
|
// This would normally be previousSection, but our model's order is inverted.
|
||||||
@ -333,6 +354,11 @@ Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component{
|
||||||
|
id: userProfileComponent
|
||||||
|
UserProfile{}
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
property: "section"
|
property: "section"
|
||||||
}
|
}
|
||||||
@ -369,6 +395,7 @@ Page {
|
|||||||
color: colors.base
|
color: colors.base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
height: userName.height
|
height: userName.height
|
||||||
spacing: 8
|
spacing: 8
|
||||||
@ -390,26 +417,18 @@ Page {
|
|||||||
|
|
||||||
Label {
|
Label {
|
||||||
id: userName
|
id: userName
|
||||||
text: timelineManager.escapeEmoji(modelData.userName)
|
text: TimelineManager.escapeEmoji(modelData.userName)
|
||||||
color: timelineManager.userColor(modelData.userId, colors.window)
|
color: TimelineManager.userColor(modelData.userId, colors.window)
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: chat.model.openUserProfile(section.split(" ")[0])
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
propagateComposedEvents: true
|
propagateComposedEvents: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
|
||||||
color: colors.buttonText
|
|
||||||
text: timelineManager.userStatus(modelData.userId)
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: chat.delegateMaxWidth - parent.spacing*2 - userName.implicitWidth - avatarSize
|
|
||||||
font.italic: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -475,7 +494,7 @@ Page {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
|
|
||||||
modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {}
|
modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {}
|
||||||
userColor: timelineManager.userColor(modelData.userId, colors.window)
|
userColor: TimelineManager.userColor(modelData.userId, colors.window)
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageButton {
|
ImageButton {
|
||||||
|
172
resources/qml/UserProfile.qml
Normal file
172
resources/qml/UserProfile.qml
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import QtQuick.Window 2.3
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
import "./device-verification"
|
||||||
|
|
||||||
|
ApplicationWindow{
|
||||||
|
property var profile
|
||||||
|
|
||||||
|
id: userProfileDialog
|
||||||
|
height: 650
|
||||||
|
width: 420
|
||||||
|
minimumHeight: 420
|
||||||
|
|
||||||
|
palette: colors
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: deviceVerificationDialog
|
||||||
|
DeviceVerification {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout{
|
||||||
|
id: contentL
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||||
|
height: 130
|
||||||
|
width: 130
|
||||||
|
displayName: profile.displayName
|
||||||
|
userid: profile.userid
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: profile.displayName
|
||||||
|
fontSizeMode: Text.HorizontalFit
|
||||||
|
font.pixelSize: 20
|
||||||
|
color: TimelineManager.userColor(profile.userid, colors.window)
|
||||||
|
font.bold: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: profile.userid
|
||||||
|
font.pixelSize: 15
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: verifyUserButton
|
||||||
|
text: qsTr("Verify")
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
enabled: !profile.isUserVerified
|
||||||
|
visible: !profile.isUserVerified
|
||||||
|
|
||||||
|
onClicked: profile.verify()
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
image:":/icons/icons/ui/do-not-disturb-rounded-sign.png"
|
||||||
|
hoverEnabled: true
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Ban the user")
|
||||||
|
onClicked: profile.banUser()
|
||||||
|
}
|
||||||
|
// ImageButton{
|
||||||
|
// image:":/icons/icons/ui/volume-off-indicator.png"
|
||||||
|
// Layout.margins: {
|
||||||
|
// left: 5
|
||||||
|
// right: 5
|
||||||
|
// }
|
||||||
|
// ToolTip.visible: hovered
|
||||||
|
// ToolTip.text: qsTr("Ignore messages from this user")
|
||||||
|
// onClicked : {
|
||||||
|
// profile.ignoreUser()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
ImageButton{
|
||||||
|
image:":/icons/icons/ui/black-bubble-speech.png"
|
||||||
|
hoverEnabled: true
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Start a private chat")
|
||||||
|
onClicked: profile.startChat()
|
||||||
|
}
|
||||||
|
ImageButton{
|
||||||
|
image:":/icons/icons/ui/round-remove-button.png"
|
||||||
|
hoverEnabled: true
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Kick the user")
|
||||||
|
onClicked: profile.kickUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView{
|
||||||
|
id: devicelist
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.minimumHeight: 200
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
spacing: 8
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
model: profile.deviceList
|
||||||
|
|
||||||
|
delegate: RowLayout{
|
||||||
|
width: devicelist.width
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
ColumnLayout{
|
||||||
|
spacing: 0
|
||||||
|
Text{
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.bold: true
|
||||||
|
color: colors.text
|
||||||
|
text: model.deviceId
|
||||||
|
}
|
||||||
|
Text{
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: colors.text
|
||||||
|
text: model.deviceName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image{
|
||||||
|
Layout.preferredHeight: 16
|
||||||
|
Layout.preferredWidth: 16
|
||||||
|
|
||||||
|
source: ((model.verificationStatus == VerificationStatus.VERIFIED)?"image://colorimage/:/icons/icons/ui/lock.png?green":
|
||||||
|
((model.verificationStatus == VerificationStatus.UNVERIFIED)?"image://colorimage/:/icons/icons/ui/unlock.png?yellow":
|
||||||
|
"image://colorimage/:/icons/icons/ui/unlock.png?red"))
|
||||||
|
}
|
||||||
|
Button{
|
||||||
|
id: verifyButton
|
||||||
|
text: (model.verificationStatus != VerificationStatus.VERIFIED)?"Verify":"Unverify"
|
||||||
|
onClicked: {
|
||||||
|
if(model.verificationStatus == VerificationStatus.VERIFIED){
|
||||||
|
profile.unverify(model.deviceId)
|
||||||
|
}else{
|
||||||
|
profile.verify(model.deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: DialogButtonBox {
|
||||||
|
standardButtons: DialogButtonBox.Ok
|
||||||
|
|
||||||
|
onAccepted: userProfileDialog.close()
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
height: row.height + 24
|
height: row.height + 24
|
||||||
width: parent ? parent.width : undefined
|
width: parent ? parent.width : undefined
|
||||||
@ -29,7 +31,7 @@ Item {
|
|||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: timelineManager.timeline.saveMedia(model.data.id)
|
onClicked: TimelineManager.timeline.saveMedia(model.data.id)
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
|
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id)
|
onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ Item {
|
|||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MtxEvent.EmoteMessage
|
roleValue: MtxEvent.EmoteMessage
|
||||||
NoticeMessage {
|
NoticeMessage {
|
||||||
formatted: timelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
|
formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
|
||||||
color: timelineManager.userColor(modelData.userId, colors.window)
|
color: TimelineManager.userColor(modelData.userId, colors.window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
@ -128,31 +128,85 @@ Item {
|
|||||||
// TODO: make a more complex formatter for the power levels.
|
// TODO: make a more complex formatter for the power levels.
|
||||||
roleValue: MtxEvent.PowerLevels
|
roleValue: MtxEvent.PowerLevels
|
||||||
NoticeMessage {
|
NoticeMessage {
|
||||||
text: timelineManager.timeline.formatPowerLevelEvent(model.data.id)
|
text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MtxEvent.RoomJoinRules
|
roleValue: MtxEvent.RoomJoinRules
|
||||||
NoticeMessage {
|
NoticeMessage {
|
||||||
text: timelineManager.timeline.formatJoinRuleEvent(model.data.id)
|
text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MtxEvent.RoomHistoryVisibility
|
roleValue: MtxEvent.RoomHistoryVisibility
|
||||||
NoticeMessage {
|
NoticeMessage {
|
||||||
text: timelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
|
text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MtxEvent.RoomGuestAccess
|
roleValue: MtxEvent.RoomGuestAccess
|
||||||
NoticeMessage {
|
NoticeMessage {
|
||||||
text: timelineManager.timeline.formatGuestAccessEvent(model.data.id)
|
text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MtxEvent.Member
|
roleValue: MtxEvent.Member
|
||||||
NoticeMessage {
|
NoticeMessage {
|
||||||
text: timelineManager.timeline.formatMemberEvent(model.data.id);
|
text: TimelineManager.timeline.formatMemberEvent(model.data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationRequest
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationRequest";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationStart
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationStart";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationReady
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationReady";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationCancel
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationCancel";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationKey
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationKey";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationMac
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationMac";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationDone
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationDone";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationDone
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationDone";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MtxEvent.KeyVerificationAccept
|
||||||
|
NoticeMessage {
|
||||||
|
text: "KeyVerificationAccept";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
|
@ -106,7 +106,7 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: {
|
onClicked: {
|
||||||
switch (button.state) {
|
switch (button.state) {
|
||||||
case "": timelineManager.timeline.cacheMedia(model.data.id); break;
|
case "": TimelineManager.timeline.cacheMedia(model.data.id); break;
|
||||||
case "stopped":
|
case "stopped":
|
||||||
media.play(); console.log("play");
|
media.play(); console.log("play");
|
||||||
button.state = "playing"
|
button.state = "playing"
|
||||||
@ -127,7 +127,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: timelineManager.timeline
|
target: TimelineManager.timeline
|
||||||
onMediaCached: {
|
onMediaCached: {
|
||||||
if (mxcUrl == model.data.url) {
|
if (mxcUrl == model.data.url) {
|
||||||
media.source = "file://" + cacheUrl
|
media.source = "file://" + cacheUrl
|
||||||
|
@ -3,6 +3,8 @@ import QtQuick.Controls 2.3
|
|||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: replyComponent
|
id: replyComponent
|
||||||
|
|
||||||
@ -26,7 +28,7 @@ Item {
|
|||||||
anchors.bottom: replyContainer.bottom
|
anchors.bottom: replyContainer.bottom
|
||||||
width: 4
|
width: 4
|
||||||
|
|
||||||
color: timelineManager.userColor(reply.modelData.userId, colors.window)
|
color: TimelineManager.userColor(reply.modelData.userId, colors.window)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@ -37,7 +39,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: userName
|
id: userName
|
||||||
text: timelineManager.escapeEmoji(reply.modelData.userName)
|
text: TimelineManager.escapeEmoji(reply.modelData.userName)
|
||||||
color: replyComponent.userColor
|
color: replyComponent.userColor
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import ".."
|
import ".."
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
MatrixText {
|
MatrixText {
|
||||||
property string formatted: model.data.formattedBody
|
property string formatted: model.data.formattedBody
|
||||||
text: "<style type=\"text/css\">a { color:"+colors.link+";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>")
|
text: "<style type=\"text/css\">a { color:"+colors.link+";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>")
|
||||||
width: parent ? parent.width : undefined
|
width: parent ? parent.width : undefined
|
||||||
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
|
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
|
||||||
clip: true
|
clip: true
|
||||||
font.pointSize: (settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? settings.fontSize * 3 : settings.fontSize
|
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
property string title: qsTr("Awaiting Confirmation")
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Label {
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
id: content
|
||||||
|
text: qsTr("Waiting for other side to complete verification.")
|
||||||
|
color:colors.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
BusyIndicator {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
flow.cancel();
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
resources/qml/device-verification/DeviceVerification.qml
Normal file
97
resources/qml/device-verification/DeviceVerification.qml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import QtQuick 2.10
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Window 2.10
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
property var flow
|
||||||
|
|
||||||
|
onClosing: TimelineManager.removeVerificationFlow(flow)
|
||||||
|
|
||||||
|
title: stack.currentItem.title
|
||||||
|
id: dialog
|
||||||
|
|
||||||
|
flags: Qt.Dialog
|
||||||
|
|
||||||
|
palette: colors
|
||||||
|
|
||||||
|
height: stack.implicitHeight
|
||||||
|
width: stack.implicitWidth
|
||||||
|
|
||||||
|
StackView {
|
||||||
|
id: stack
|
||||||
|
initialItem: newVerificationRequest
|
||||||
|
implicitWidth: currentItem.implicitWidth
|
||||||
|
implicitHeight: currentItem.implicitHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
Component{
|
||||||
|
id: newVerificationRequest
|
||||||
|
NewVerificationRequest {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: waiting
|
||||||
|
Waiting {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: success
|
||||||
|
Success {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: failed
|
||||||
|
Failed {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: digitVerification
|
||||||
|
DigitVerification {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: emojiVerification
|
||||||
|
EmojiVerification {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
state: flow.state
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "PromptStartVerification"
|
||||||
|
StateChangeScript { script: stack.replace(newVerificationRequest) }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "CompareEmoji"
|
||||||
|
StateChangeScript { script: stack.replace(emojiVerification) }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "CompareNumber"
|
||||||
|
StateChangeScript { script: stack.replace(digitVerification) }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "WaitingForKeys"
|
||||||
|
StateChangeScript { script: stack.replace(waiting) }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "WaitingForOtherToAccept"
|
||||||
|
StateChangeScript { script: stack.replace(waiting) }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "WaitingForMac"
|
||||||
|
StateChangeScript { script: stack.replace(waiting) }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "Success"
|
||||||
|
StateChangeScript { script: stack.replace(success) }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "Failed"
|
||||||
|
StateChangeScript { script: stack.replace(failed); }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
60
resources/qml/device-verification/DigitVerification.qml
Normal file
60
resources/qml/device-verification/DigitVerification.qml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
property string title: qsTr("Verification Code")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Label {
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
|
||||||
|
color:colors.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Label {
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||||
|
text: flow.sasList[0]
|
||||||
|
color:colors.text
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||||
|
text: flow.sasList[1]
|
||||||
|
color:colors.text
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||||
|
text: flow.sasList[2]
|
||||||
|
color:colors.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
text: qsTr("They do not match!")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
flow.cancel();
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
text: qsTr("They match!")
|
||||||
|
|
||||||
|
onClicked: flow.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
resources/qml/device-verification/EmojiElement.qml
Normal file
26
resources/qml/device-verification/EmojiElement.qml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: "red"
|
||||||
|
implicitHeight: Qt.application.font.pixelSize * 4
|
||||||
|
implicitWidth: col.width
|
||||||
|
height: Qt.application.font.pixelSize * 4
|
||||||
|
width: col.width
|
||||||
|
ColumnLayout {
|
||||||
|
id: col
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
property var emoji: emojis.mapping[Math.floor(Math.random()*64)]
|
||||||
|
Label {
|
||||||
|
height: font.pixelSize * 2
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: col.emoji.emoji
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
||||||
|
text: col.emoji.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
resources/qml/device-verification/EmojiVerification.qml
Normal file
140
resources/qml/device-verification/EmojiVerification.qml
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
property string title: qsTr("Verification Code")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Label {
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
|
||||||
|
color:colors.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
id: emojis
|
||||||
|
property var mapping: [
|
||||||
|
{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
|
||||||
|
{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
|
||||||
|
{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
|
||||||
|
{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
|
||||||
|
{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
|
||||||
|
{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
|
||||||
|
{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
|
||||||
|
{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
|
||||||
|
{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
|
||||||
|
{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
|
||||||
|
{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
|
||||||
|
{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
|
||||||
|
{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
|
||||||
|
{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
|
||||||
|
{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
|
||||||
|
{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
|
||||||
|
{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
|
||||||
|
{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
|
||||||
|
{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
|
||||||
|
{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
|
||||||
|
{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
|
||||||
|
{"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"},
|
||||||
|
{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
|
||||||
|
{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
|
||||||
|
{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
|
||||||
|
{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
|
||||||
|
{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
|
||||||
|
{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
|
||||||
|
{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
|
||||||
|
{"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"},
|
||||||
|
{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
|
||||||
|
{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
|
||||||
|
{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
|
||||||
|
{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
|
||||||
|
{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
|
||||||
|
{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
|
||||||
|
{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
|
||||||
|
{"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
|
||||||
|
{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
|
||||||
|
{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
|
||||||
|
{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
|
||||||
|
{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
|
||||||
|
{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
|
||||||
|
{"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"},
|
||||||
|
{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
|
||||||
|
{"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"},
|
||||||
|
{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
|
||||||
|
{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
|
||||||
|
{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
|
||||||
|
{"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"},
|
||||||
|
{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
|
||||||
|
{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
|
||||||
|
{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
|
||||||
|
{"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
|
||||||
|
{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
|
||||||
|
{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
|
||||||
|
{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
|
||||||
|
{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
|
||||||
|
{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
|
||||||
|
{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
|
||||||
|
{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
|
||||||
|
{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
|
||||||
|
{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
|
||||||
|
{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
|
||||||
|
]
|
||||||
|
Repeater {
|
||||||
|
id: repeater
|
||||||
|
model: 7
|
||||||
|
delegate: Rectangle {
|
||||||
|
color: "transparent"
|
||||||
|
implicitHeight: Qt.application.font.pixelSize * 8
|
||||||
|
implicitWidth: col.width
|
||||||
|
ColumnLayout {
|
||||||
|
id: col
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
property var emoji: emojis.mapping[flow.sasList[index]]
|
||||||
|
Label {
|
||||||
|
//height: font.pixelSize * 2
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: col.emoji.emoji
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2
|
||||||
|
font.family: Settings.emojiFont
|
||||||
|
color:colors.text
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
||||||
|
text: col.emoji.description
|
||||||
|
color:colors.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
text: qsTr("They do not match!")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
flow.cancel();
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
text: qsTr("They match!")
|
||||||
|
|
||||||
|
onClicked: flow.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
resources/qml/device-verification/Failed.qml
Normal file
44
resources/qml/device-verification/Failed.qml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
property string title: qsTr("Verification failed")
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Text {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
text: switch (flow.error) {
|
||||||
|
case DeviceVerificationFlow.UnknownMethod: return qsTr("Other client does not support our verification protocol.")
|
||||||
|
case DeviceVerificationFlow.MismatchedCommitment:
|
||||||
|
case DeviceVerificationFlow.MismatchedSAS:
|
||||||
|
case DeviceVerificationFlow.KeyMismatch: return qsTr("Key mismatch detected!")
|
||||||
|
case DeviceVerificationFlow.Timeout: return qsTr("Device verification timed out.")
|
||||||
|
case DeviceVerificationFlow.User: return qsTr("Other party canceled the verification.")
|
||||||
|
case DeviceVerificationFlow.OutOfOrder: return qsTr("Device verification timed out.")
|
||||||
|
default: return "Unknown verification error.";
|
||||||
|
}
|
||||||
|
color:colors.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
text: qsTr("Close")
|
||||||
|
|
||||||
|
onClicked: dialog.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
resources/qml/device-verification/NewVerificationRequest.qml
Normal file
44
resources/qml/device-verification/NewVerificationRequest.qml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Label {
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
text: flow.sender ?
|
||||||
|
qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.")
|
||||||
|
: qsTr("The device was requested to be verified")
|
||||||
|
color:colors.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
text: flow.sender ? qsTr("Cancel") : qsTr("Deny")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
flow.cancel();
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
text: flow.sender ? qsTr("Start verification") : qsTr("Accept")
|
||||||
|
|
||||||
|
onClicked: flow.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
resources/qml/device-verification/Success.qml
Normal file
31
resources/qml/device-verification/Success.qml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
property string title: qsTr("Successful Verification")
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Label {
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
id: content
|
||||||
|
text: qsTr("Verification successful! Both sides verified their devices!")
|
||||||
|
color:colors.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
text: qsTr("Close")
|
||||||
|
|
||||||
|
onClicked: dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
resources/qml/device-verification/Waiting.qml
Normal file
45
resources/qml/device-verification/Waiting.qml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 2.10
|
||||||
|
import QtQuick.Layouts 1.10
|
||||||
|
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
property string title: qsTr("Waiting for other party")
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
Label {
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
id: content
|
||||||
|
text: switch (flow.state) {
|
||||||
|
case "WaitingForOtherToAccept": return qsTr("Waiting for other side to accept the verification request.")
|
||||||
|
case "WaitingForKeys": return qsTr("Waiting for other side to continue the verification request.")
|
||||||
|
case "WaitingForMac": return qsTr("Waiting for other side to complete the verification request.")
|
||||||
|
}
|
||||||
|
|
||||||
|
color: colors.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
BusyIndicator {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
palette: colors
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
flow.cancel();
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
resources/qml/device-verification/sas-emoji.json
Normal file
66
resources/qml/device-verification/sas-emoji.json
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
[
|
||||||
|
{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
|
||||||
|
{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
|
||||||
|
{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
|
||||||
|
{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
|
||||||
|
{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
|
||||||
|
{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
|
||||||
|
{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
|
||||||
|
{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
|
||||||
|
{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
|
||||||
|
{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
|
||||||
|
{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
|
||||||
|
{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
|
||||||
|
{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
|
||||||
|
{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
|
||||||
|
{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
|
||||||
|
{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
|
||||||
|
{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
|
||||||
|
{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
|
||||||
|
{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
|
||||||
|
{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
|
||||||
|
{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
|
||||||
|
{"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"},
|
||||||
|
{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
|
||||||
|
{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
|
||||||
|
{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
|
||||||
|
{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
|
||||||
|
{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
|
||||||
|
{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
|
||||||
|
{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
|
||||||
|
{"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"},
|
||||||
|
{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
|
||||||
|
{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
|
||||||
|
{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
|
||||||
|
{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
|
||||||
|
{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
|
||||||
|
{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
|
||||||
|
{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
|
||||||
|
{"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
|
||||||
|
{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
|
||||||
|
{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
|
||||||
|
{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
|
||||||
|
{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
|
||||||
|
{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
|
||||||
|
{"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"},
|
||||||
|
{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
|
||||||
|
{"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"},
|
||||||
|
{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
|
||||||
|
{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
|
||||||
|
{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
|
||||||
|
{"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"},
|
||||||
|
{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
|
||||||
|
{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
|
||||||
|
{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
|
||||||
|
{"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
|
||||||
|
{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
|
||||||
|
{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
|
||||||
|
{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
|
||||||
|
{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
|
||||||
|
{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
|
||||||
|
{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
|
||||||
|
{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
|
||||||
|
{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
|
||||||
|
{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
|
||||||
|
{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
|
||||||
|
]
|
@ -73,7 +73,7 @@ Popup {
|
|||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
font.family: settings.emojiFont
|
font.family: Settings.emojiFont
|
||||||
|
|
||||||
font.pixelSize: 36
|
font.pixelSize: 36
|
||||||
text: model.unicode
|
text: model.unicode
|
||||||
@ -104,7 +104,7 @@ Popup {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id)
|
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id)
|
||||||
emojiPopup.close()
|
emojiPopup.close()
|
||||||
timelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode)
|
TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +132,7 @@
|
|||||||
<file>qml/TimelineRow.qml</file>
|
<file>qml/TimelineRow.qml</file>
|
||||||
<file>qml/emoji/EmojiButton.qml</file>
|
<file>qml/emoji/EmojiButton.qml</file>
|
||||||
<file>qml/emoji/EmojiPicker.qml</file>
|
<file>qml/emoji/EmojiPicker.qml</file>
|
||||||
|
<file>qml/UserProfile.qml</file>
|
||||||
<file>qml/delegates/MessageDelegate.qml</file>
|
<file>qml/delegates/MessageDelegate.qml</file>
|
||||||
<file>qml/delegates/TextMessage.qml</file>
|
<file>qml/delegates/TextMessage.qml</file>
|
||||||
<file>qml/delegates/NoticeMessage.qml</file>
|
<file>qml/delegates/NoticeMessage.qml</file>
|
||||||
@ -141,6 +142,13 @@
|
|||||||
<file>qml/delegates/Pill.qml</file>
|
<file>qml/delegates/Pill.qml</file>
|
||||||
<file>qml/delegates/Placeholder.qml</file>
|
<file>qml/delegates/Placeholder.qml</file>
|
||||||
<file>qml/delegates/Reply.qml</file>
|
<file>qml/delegates/Reply.qml</file>
|
||||||
|
<file>qml/device-verification/Waiting.qml</file>
|
||||||
|
<file>qml/device-verification/DeviceVerification.qml</file>
|
||||||
|
<file>qml/device-verification/DigitVerification.qml</file>
|
||||||
|
<file>qml/device-verification/EmojiVerification.qml</file>
|
||||||
|
<file>qml/device-verification/NewVerificationRequest.qml</file>
|
||||||
|
<file>qml/device-verification/Failed.qml</file>
|
||||||
|
<file>qml/device-verification/Success.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/media">
|
<qresource prefix="/media">
|
||||||
<file>media/ring.ogg</file>
|
<file>media/ring.ogg</file>
|
||||||
|
444
src/Cache.cpp
444
src/Cache.cpp
@ -31,8 +31,10 @@
|
|||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "Cache_p.h"
|
#include "Cache_p.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
#include "EventAccessors.h"
|
#include "EventAccessors.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
|
#include "MatrixClient.h"
|
||||||
#include "Olm.h"
|
#include "Olm.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
@ -89,6 +91,7 @@ Q_DECLARE_METATYPE(RoomMember)
|
|||||||
Q_DECLARE_METATYPE(mtx::responses::Timeline)
|
Q_DECLARE_METATYPE(mtx::responses::Timeline)
|
||||||
Q_DECLARE_METATYPE(RoomSearchResult)
|
Q_DECLARE_METATYPE(RoomSearchResult)
|
||||||
Q_DECLARE_METATYPE(RoomInfo)
|
Q_DECLARE_METATYPE(RoomInfo)
|
||||||
|
Q_DECLARE_METATYPE(mtx::responses::QueryKeys)
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
std::unique_ptr<Cache> instance_ = nullptr;
|
std::unique_ptr<Cache> instance_ = nullptr;
|
||||||
@ -153,6 +156,7 @@ Cache::Cache(const QString &userId, QObject *parent)
|
|||||||
, localUserId_{userId}
|
, localUserId_{userId}
|
||||||
{
|
{
|
||||||
setup();
|
setup();
|
||||||
|
connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -368,6 +372,25 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index
|
|||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::dropOutboundMegolmSession(const std::string &room_id)
|
||||||
|
{
|
||||||
|
using namespace mtx::crypto;
|
||||||
|
|
||||||
|
if (!outboundMegolmSessionExists(room_id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
|
||||||
|
session_storage.group_outbound_session_data.erase(room_id);
|
||||||
|
session_storage.group_outbound_sessions.erase(room_id);
|
||||||
|
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
lmdb::dbi_del(txn, outboundMegolmSessionDb_, lmdb::val(room_id), nullptr);
|
||||||
|
txn.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Cache::saveOutboundMegolmSession(const std::string &room_id,
|
Cache::saveOutboundMegolmSession(const std::string &room_id,
|
||||||
const OutboundGroupSessionData &data,
|
const OutboundGroupSessionData &data,
|
||||||
@ -683,11 +706,14 @@ Cache::nextBatchToken() const
|
|||||||
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
||||||
lmdb::val token;
|
lmdb::val token;
|
||||||
|
|
||||||
lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
|
auto result = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
|
||||||
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
|
|
||||||
|
if (result)
|
||||||
return std::string(token.data(), token.size());
|
return std::string(token.data(), token.size());
|
||||||
|
else
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -995,6 +1021,8 @@ Cache::saveState(const mtx::responses::Sync &res)
|
|||||||
using namespace mtx::events;
|
using namespace mtx::events;
|
||||||
auto user_id = this->localUserId_.toStdString();
|
auto user_id = this->localUserId_.toStdString();
|
||||||
|
|
||||||
|
auto currentBatchToken = nextBatchToken();
|
||||||
|
|
||||||
auto txn = lmdb::txn::begin(env_);
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
|
||||||
setNextBatchToken(txn, res.next_batch);
|
setNextBatchToken(txn, res.next_batch);
|
||||||
@ -1012,6 +1040,8 @@ Cache::saveState(const mtx::responses::Sync &res)
|
|||||||
ev);
|
ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto userKeyCacheDb = getUserKeysDb(txn);
|
||||||
|
|
||||||
// Save joined rooms
|
// Save joined rooms
|
||||||
for (const auto &room : res.rooms.join) {
|
for (const auto &room : res.rooms.join) {
|
||||||
auto statesdb = getStatesDb(txn, room.first);
|
auto statesdb = getStatesDb(txn, room.first);
|
||||||
@ -1085,6 +1115,9 @@ Cache::saveState(const mtx::responses::Sync &res)
|
|||||||
|
|
||||||
savePresence(txn, res.presence);
|
savePresence(txn, res.presence);
|
||||||
|
|
||||||
|
markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken);
|
||||||
|
deleteUserKeys(txn, userKeyCacheDb, res.device_lists.left);
|
||||||
|
|
||||||
removeLeftRooms(txn, res.rooms.leave);
|
removeLeftRooms(txn, res.rooms.leave);
|
||||||
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
@ -3073,6 +3106,378 @@ Cache::statusMessage(const std::string &user_id)
|
|||||||
return status_msg;
|
return status_msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
to_json(json &j, const UserKeyCache &info)
|
||||||
|
{
|
||||||
|
j["device_keys"] = info.device_keys;
|
||||||
|
j["master_keys"] = info.master_keys;
|
||||||
|
j["user_signing_keys"] = info.user_signing_keys;
|
||||||
|
j["self_signing_keys"] = info.self_signing_keys;
|
||||||
|
j["updated_at"] = info.updated_at;
|
||||||
|
j["last_changed"] = info.last_changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
from_json(const json &j, UserKeyCache &info)
|
||||||
|
{
|
||||||
|
info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{});
|
||||||
|
info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
|
||||||
|
info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
|
||||||
|
info.self_signing_keys = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{});
|
||||||
|
info.updated_at = j.value("updated_at", "");
|
||||||
|
info.last_changed = j.value("last_changed", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<UserKeyCache>
|
||||||
|
Cache::userKeys(const std::string &user_id)
|
||||||
|
{
|
||||||
|
lmdb::val keys;
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
||||||
|
auto db = getUserKeysDb(txn);
|
||||||
|
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), keys);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
return json::parse(std::string_view(keys.data(), keys.size()))
|
||||||
|
.get<UserKeyCache>();
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} catch (std::exception &) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
|
||||||
|
{
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
auto db = getUserKeysDb(txn);
|
||||||
|
|
||||||
|
std::map<std::string, UserKeyCache> updates;
|
||||||
|
|
||||||
|
for (const auto &[user, keys] : keyQuery.device_keys)
|
||||||
|
updates[user].device_keys = keys;
|
||||||
|
for (const auto &[user, keys] : keyQuery.master_keys)
|
||||||
|
updates[user].master_keys = keys;
|
||||||
|
for (const auto &[user, keys] : keyQuery.user_signing_keys)
|
||||||
|
updates[user].user_signing_keys = keys;
|
||||||
|
for (const auto &[user, keys] : keyQuery.self_signing_keys)
|
||||||
|
updates[user].self_signing_keys = keys;
|
||||||
|
|
||||||
|
for (auto &[user, update] : updates) {
|
||||||
|
lmdb::val oldKeys;
|
||||||
|
auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
auto last_changed =
|
||||||
|
json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
|
||||||
|
.get<UserKeyCache>()
|
||||||
|
.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)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(update).dump()));
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.commit();
|
||||||
|
|
||||||
|
std::map<std::string, VerificationStatus> tmp;
|
||||||
|
const auto local_user = utils::localUser().toStdString();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||||
|
for (auto &[user_id, update] : updates) {
|
||||||
|
(void)update;
|
||||||
|
if (user_id == local_user) {
|
||||||
|
std::swap(tmp, verification_storage.status);
|
||||||
|
} else {
|
||||||
|
verification_storage.status.erase(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto &[user_id, update] : updates) {
|
||||||
|
(void)update;
|
||||||
|
if (user_id == local_user) {
|
||||||
|
for (const auto &[user, status] : tmp) {
|
||||||
|
(void)status;
|
||||||
|
emit verificationStatusChanged(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit verificationStatusChanged(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::deleteUserKeys(lmdb::txn &txn, lmdb::dbi &db, const std::vector<std::string> &user_ids)
|
||||||
|
{
|
||||||
|
for (const auto &user_id : user_ids)
|
||||||
|
lmdb::dbi_del(txn, db, lmdb::val(user_id), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::markUserKeysOutOfDate(lmdb::txn &txn,
|
||||||
|
lmdb::dbi &db,
|
||||||
|
const std::vector<std::string> &user_ids,
|
||||||
|
const std::string &sync_token)
|
||||||
|
{
|
||||||
|
mtx::requests::QueryKeys query;
|
||||||
|
query.token = sync_token;
|
||||||
|
|
||||||
|
for (const auto &user : user_ids) {
|
||||||
|
lmdb::val oldKeys;
|
||||||
|
auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys);
|
||||||
|
|
||||||
|
if (!res)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto cacheEntry =
|
||||||
|
json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>();
|
||||||
|
cacheEntry.last_changed = sync_token;
|
||||||
|
lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(cacheEntry).dump()));
|
||||||
|
|
||||||
|
query.device_keys[user] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.device_keys.empty())
|
||||||
|
http::client()->query_keys(query,
|
||||||
|
[this, sync_token](const mtx::responses::QueryKeys &keys,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn(
|
||||||
|
"failed to query device keys: {} {}",
|
||||||
|
err->matrix_error.error,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit userKeysUpdate(sync_token, keys);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
to_json(json &j, const VerificationCache &info)
|
||||||
|
{
|
||||||
|
j["device_verified"] = info.device_verified;
|
||||||
|
j["device_blocked"] = info.device_blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<VerificationCache>
|
||||||
|
Cache::verificationCache(const std::string &user_id)
|
||||||
|
{
|
||||||
|
lmdb::val verifiedVal;
|
||||||
|
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
auto db = getVerificationDb(txn);
|
||||||
|
|
||||||
|
try {
|
||||||
|
VerificationCache verified_state;
|
||||||
|
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), verifiedVal);
|
||||||
|
if (res) {
|
||||||
|
verified_state =
|
||||||
|
json::parse(std::string_view(verifiedVal.data(), verifiedVal.size()));
|
||||||
|
return verified_state;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} catch (std::exception &) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
|
||||||
|
{
|
||||||
|
lmdb::val val;
|
||||||
|
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
auto db = getVerificationDb(txn);
|
||||||
|
|
||||||
|
try {
|
||||||
|
VerificationCache verified_state;
|
||||||
|
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val);
|
||||||
|
if (res) {
|
||||||
|
verified_state = json::parse(std::string_view(val.data(), val.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &device : verified_state.device_verified)
|
||||||
|
if (device == key)
|
||||||
|
return;
|
||||||
|
|
||||||
|
verified_state.device_verified.push_back(key);
|
||||||
|
lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump()));
|
||||||
|
txn.commit();
|
||||||
|
} catch (std::exception &) {
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto local_user = utils::localUser().toStdString();
|
||||||
|
std::map<std::string, VerificationStatus> tmp;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||||
|
if (user_id == local_user) {
|
||||||
|
std::swap(tmp, verification_storage.status);
|
||||||
|
} else {
|
||||||
|
verification_storage.status.erase(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (user_id == local_user) {
|
||||||
|
for (const auto &[user, status] : tmp) {
|
||||||
|
(void)status;
|
||||||
|
emit verificationStatusChanged(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit verificationStatusChanged(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
|
||||||
|
{
|
||||||
|
lmdb::val val;
|
||||||
|
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
auto db = getVerificationDb(txn);
|
||||||
|
|
||||||
|
try {
|
||||||
|
VerificationCache verified_state;
|
||||||
|
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val);
|
||||||
|
if (res) {
|
||||||
|
verified_state = json::parse(std::string_view(val.data(), val.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
verified_state.device_verified.erase(
|
||||||
|
std::remove(verified_state.device_verified.begin(),
|
||||||
|
verified_state.device_verified.end(),
|
||||||
|
key),
|
||||||
|
verified_state.device_verified.end());
|
||||||
|
|
||||||
|
lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump()));
|
||||||
|
txn.commit();
|
||||||
|
} catch (std::exception &) {
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto local_user = utils::localUser().toStdString();
|
||||||
|
std::map<std::string, VerificationStatus> tmp;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||||
|
if (user_id == local_user) {
|
||||||
|
std::swap(tmp, verification_storage.status);
|
||||||
|
} else {
|
||||||
|
verification_storage.status.erase(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (user_id == local_user) {
|
||||||
|
for (const auto &[user, status] : tmp) {
|
||||||
|
(void)status;
|
||||||
|
emit verificationStatusChanged(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit verificationStatusChanged(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerificationStatus
|
||||||
|
Cache::verificationStatus(const std::string &user_id)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||||
|
if (verification_storage.status.count(user_id))
|
||||||
|
return verification_storage.status.at(user_id);
|
||||||
|
|
||||||
|
VerificationStatus status;
|
||||||
|
|
||||||
|
if (auto verifCache = verificationCache(user_id)) {
|
||||||
|
status.verified_devices = verifCache->device_verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto local_user = utils::localUser().toStdString();
|
||||||
|
|
||||||
|
if (user_id == local_user)
|
||||||
|
status.verified_devices.push_back(http::client()->device_id());
|
||||||
|
|
||||||
|
verification_storage.status[user_id] = status;
|
||||||
|
|
||||||
|
auto verifyAtLeastOneSig = [](const auto &toVerif,
|
||||||
|
const std::map<std::string, std::string> &keys,
|
||||||
|
const std::string &keyOwner) {
|
||||||
|
if (!toVerif.signatures.count(keyOwner))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
|
||||||
|
if (!keys.count(key_id))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (mtx::crypto::ed25519_verify_signature(
|
||||||
|
keys.at(key_id), json(toVerif), signature))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// for local user verify this device_key -> our master_key -> our self_signing_key
|
||||||
|
// -> our device_keys
|
||||||
|
//
|
||||||
|
// for other user verify this device_key -> our master_key -> our user_signing_key
|
||||||
|
// -> their master_key -> their self_signing_key -> their device_keys
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
return status;
|
||||||
|
|
||||||
|
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())))
|
||||||
|
return status;
|
||||||
|
|
||||||
|
auto master_keys = ourKeys->master_keys.keys;
|
||||||
|
|
||||||
|
if (user_id != local_user) {
|
||||||
|
if (!verifyAtLeastOneSig(
|
||||||
|
ourKeys->user_signing_keys, master_keys, local_user))
|
||||||
|
return status;
|
||||||
|
|
||||||
|
if (!verifyAtLeastOneSig(
|
||||||
|
theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user))
|
||||||
|
return status;
|
||||||
|
|
||||||
|
master_keys = theirKeys->master_keys.keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.user_verified = true;
|
||||||
|
|
||||||
|
if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
|
||||||
|
return status;
|
||||||
|
|
||||||
|
for (const auto &[device, device_key] : theirKeys->device_keys) {
|
||||||
|
(void)device;
|
||||||
|
if (verifyAtLeastOneSig(
|
||||||
|
device_key, theirKeys->self_signing_keys.keys, user_id))
|
||||||
|
status.verified_devices.push_back(device_key.device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
verification_storage.status[user_id] = status;
|
||||||
|
return status;
|
||||||
|
} catch (std::exception &) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
to_json(json &j, const RoomInfo &info)
|
to_json(json &j, const RoomInfo &info)
|
||||||
{
|
{
|
||||||
@ -3195,6 +3600,7 @@ init(const QString &user_id)
|
|||||||
qRegisterMetaType<QMap<QString, RoomInfo>>();
|
qRegisterMetaType<QMap<QString, RoomInfo>>();
|
||||||
qRegisterMetaType<std::map<QString, RoomInfo>>();
|
qRegisterMetaType<std::map<QString, RoomInfo>>();
|
||||||
qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>();
|
qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>();
|
||||||
|
qRegisterMetaType<mtx::responses::QueryKeys>();
|
||||||
|
|
||||||
instance_ = std::make_unique<Cache>(user_id);
|
instance_ = std::make_unique<Cache>(user_id);
|
||||||
}
|
}
|
||||||
@ -3262,6 +3668,37 @@ populateMembers()
|
|||||||
instance_->populateMembers();
|
instance_->populateMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// user cache stores user keys
|
||||||
|
std::optional<UserKeyCache>
|
||||||
|
userKeys(const std::string &user_id)
|
||||||
|
{
|
||||||
|
return instance_->userKeys(user_id);
|
||||||
|
}
|
||||||
|
void
|
||||||
|
updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
|
||||||
|
{
|
||||||
|
instance_->updateUserKeys(sync_token, keyQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// device & user verification cache
|
||||||
|
std::optional<VerificationStatus>
|
||||||
|
verificationStatus(const std::string &user_id)
|
||||||
|
{
|
||||||
|
return instance_->verificationStatus(user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
markDeviceVerified(const std::string &user_id, const std::string &device)
|
||||||
|
{
|
||||||
|
instance_->markDeviceVerified(user_id, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
markDeviceUnverified(const std::string &user_id, const std::string &device)
|
||||||
|
{
|
||||||
|
instance_->markDeviceUnverified(user_id, device);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string>
|
std::vector<std::string>
|
||||||
joinedRooms()
|
joinedRooms()
|
||||||
{
|
{
|
||||||
@ -3595,6 +4032,11 @@ updateOutboundMegolmSession(const std::string &room_id, int message_index)
|
|||||||
{
|
{
|
||||||
instance_->updateOutboundMegolmSession(room_id, message_index);
|
instance_->updateOutboundMegolmSession(room_id, message_index);
|
||||||
}
|
}
|
||||||
|
void
|
||||||
|
dropOutboundMegolmSession(const std::string &room_id)
|
||||||
|
{
|
||||||
|
instance_->dropOutboundMegolmSession(room_id);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
|
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
|
||||||
|
16
src/Cache.h
16
src/Cache.h
@ -60,6 +60,20 @@ presenceState(const std::string &user_id);
|
|||||||
std::string
|
std::string
|
||||||
statusMessage(const std::string &user_id);
|
statusMessage(const std::string &user_id);
|
||||||
|
|
||||||
|
// user cache stores user keys
|
||||||
|
std::optional<UserKeyCache>
|
||||||
|
userKeys(const std::string &user_id);
|
||||||
|
void
|
||||||
|
updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
|
||||||
|
|
||||||
|
// device & user verification cache
|
||||||
|
std::optional<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);
|
||||||
|
|
||||||
//! Load saved data for the display names & avatars.
|
//! Load saved data for the display names & avatars.
|
||||||
void
|
void
|
||||||
populateMembers();
|
populateMembers();
|
||||||
@ -255,6 +269,8 @@ bool
|
|||||||
outboundMegolmSessionExists(const std::string &room_id) noexcept;
|
outboundMegolmSessionExists(const std::string &room_id) noexcept;
|
||||||
void
|
void
|
||||||
updateOutboundMegolmSession(const std::string &room_id, int message_index);
|
updateOutboundMegolmSession(const std::string &room_id, int message_index);
|
||||||
|
void
|
||||||
|
dropOutboundMegolmSession(const std::string &room_id);
|
||||||
|
|
||||||
void
|
void
|
||||||
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
|
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
|
||||||
|
@ -65,3 +65,53 @@ struct OlmSessionStorage
|
|||||||
std::mutex group_outbound_mtx;
|
std::mutex group_outbound_mtx;
|
||||||
std::mutex group_inbound_mtx;
|
std::mutex group_inbound_mtx;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//! Verification status of a single user
|
||||||
|
struct VerificationStatus
|
||||||
|
{
|
||||||
|
//! True, if the users master key is verified
|
||||||
|
bool user_verified = false;
|
||||||
|
//! List of all devices marked as verified
|
||||||
|
std::vector<std::string> verified_devices;
|
||||||
|
};
|
||||||
|
|
||||||
|
//! In memory cache of verification status
|
||||||
|
struct VerificationStorage
|
||||||
|
{
|
||||||
|
//! mapping of user to verification status
|
||||||
|
std::map<std::string, VerificationStatus> status;
|
||||||
|
std::mutex verification_storage_mtx;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this will store the keys of the user with whom a encrypted room is shared with
|
||||||
|
struct UserKeyCache
|
||||||
|
{
|
||||||
|
//! Device id to device keys
|
||||||
|
std::map<std::string, mtx::crypto::DeviceKeys> device_keys;
|
||||||
|
//! corss signing keys
|
||||||
|
mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
|
||||||
|
//! Sync token when nheko last fetched the keys
|
||||||
|
std::string updated_at;
|
||||||
|
//! Sync token when the keys last changed. updated != last_changed means they are outdated.
|
||||||
|
std::string last_changed;
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
to_json(nlohmann::json &j, const UserKeyCache &info);
|
||||||
|
void
|
||||||
|
from_json(const nlohmann::json &j, UserKeyCache &info);
|
||||||
|
|
||||||
|
// the reason these are stored in a seperate cache rather than storing it in the user cache is
|
||||||
|
// UserKeyCache stores only keys of users with which encrypted room is shared
|
||||||
|
struct VerificationCache
|
||||||
|
{
|
||||||
|
//! list of verified device_ids with device-verification
|
||||||
|
std::vector<std::string> device_verified;
|
||||||
|
//! list of devices the user blocks
|
||||||
|
std::vector<std::string> device_blocked;
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
to_json(nlohmann::json &j, const VerificationCache &info);
|
||||||
|
void
|
||||||
|
from_json(const nlohmann::json &j, VerificationCache &info);
|
||||||
|
@ -54,6 +54,23 @@ public:
|
|||||||
mtx::presence::PresenceState presenceState(const std::string &user_id);
|
mtx::presence::PresenceState presenceState(const std::string &user_id);
|
||||||
std::string statusMessage(const std::string &user_id);
|
std::string statusMessage(const std::string &user_id);
|
||||||
|
|
||||||
|
// user cache stores user keys
|
||||||
|
std::optional<UserKeyCache> userKeys(const std::string &user_id);
|
||||||
|
void updateUserKeys(const std::string &sync_token,
|
||||||
|
const mtx::responses::QueryKeys &keyQuery);
|
||||||
|
void markUserKeysOutOfDate(lmdb::txn &txn,
|
||||||
|
lmdb::dbi &db,
|
||||||
|
const std::vector<std::string> &user_ids,
|
||||||
|
const std::string &sync_token);
|
||||||
|
void deleteUserKeys(lmdb::txn &txn,
|
||||||
|
lmdb::dbi &db,
|
||||||
|
const std::vector<std::string> &user_ids);
|
||||||
|
|
||||||
|
// device & user verification cache
|
||||||
|
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);
|
||||||
|
|
||||||
static void removeDisplayName(const QString &room_id, const QString &user_id);
|
static void removeDisplayName(const QString &room_id, const QString &user_id);
|
||||||
static void removeAvatarUrl(const QString &room_id, const QString &user_id);
|
static void removeAvatarUrl(const QString &room_id, const QString &user_id);
|
||||||
|
|
||||||
@ -233,6 +250,7 @@ public:
|
|||||||
OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
|
OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
|
||||||
bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
|
bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
|
||||||
void updateOutboundMegolmSession(const std::string &room_id, int message_index);
|
void updateOutboundMegolmSession(const std::string &room_id, int message_index);
|
||||||
|
void dropOutboundMegolmSession(const std::string &room_id);
|
||||||
|
|
||||||
void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
|
void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
|
||||||
mtx::crypto::ExportedSessionKeys exportSessionKeys();
|
mtx::crypto::ExportedSessionKeys exportSessionKeys();
|
||||||
@ -262,6 +280,9 @@ signals:
|
|||||||
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
|
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
|
||||||
void roomReadStatus(const std::map<QString, bool> &status);
|
void roomReadStatus(const std::map<QString, bool> &status);
|
||||||
void removeNotification(const QString &room_id, const QString &event_id);
|
void removeNotification(const QString &room_id, const QString &event_id);
|
||||||
|
void userKeysUpdate(const std::string &sync_token,
|
||||||
|
const mtx::responses::QueryKeys &keyQuery);
|
||||||
|
void verificationStatusChanged(const std::string &userid);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
//! Save an invited room.
|
//! Save an invited room.
|
||||||
@ -527,6 +548,16 @@ private:
|
|||||||
return lmdb::dbi::open(txn, "presence", MDB_CREATE);
|
return lmdb::dbi::open(txn, "presence", MDB_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lmdb::dbi getUserKeysDb(lmdb::txn &txn)
|
||||||
|
{
|
||||||
|
return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
lmdb::dbi getVerificationDb(lmdb::txn &txn)
|
||||||
|
{
|
||||||
|
return lmdb::dbi::open(txn, "verified", MDB_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
//! Retrieves or creates the database that stores the open OLM sessions between our device
|
//! Retrieves or creates the database that stores the open OLM sessions between our device
|
||||||
//! and the given curve25519 key which represents another device.
|
//! and the given curve25519 key which represents another device.
|
||||||
//!
|
//!
|
||||||
@ -545,6 +576,8 @@ private:
|
|||||||
return QString::fromStdString(event.state_key);
|
return QString::fromStdString(event.state_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<VerificationCache> verificationCache(const std::string &user_id);
|
||||||
|
|
||||||
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
|
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
|
||||||
void setNextBatchToken(lmdb::txn &txn, const QString &token);
|
void setNextBatchToken(lmdb::txn &txn, const QString &token);
|
||||||
|
|
||||||
@ -569,6 +602,7 @@ private:
|
|||||||
static QHash<QString, QString> AvatarUrls;
|
static QHash<QString, QString> AvatarUrls;
|
||||||
|
|
||||||
OlmSessionStorage session_storage;
|
OlmSessionStorage session_storage;
|
||||||
|
VerificationStorage verification_storage;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace cache {
|
namespace cache {
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "Cache_p.h"
|
#include "Cache_p.h"
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
|
#include "DeviceVerificationFlow.h"
|
||||||
#include "EventAccessors.h"
|
#include "EventAccessors.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
@ -159,6 +160,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||||||
view_manager_,
|
view_manager_,
|
||||||
&TimelineViewManager::clearCurrentRoomTimeline);
|
&TimelineViewManager::clearCurrentRoomTimeline);
|
||||||
|
|
||||||
|
connect(text_input_, &TextInputWidget::rotateMegolmSession, this, [this]() {
|
||||||
|
cache::dropOutboundMegolmSession(current_room_.toStdString());
|
||||||
|
});
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
|
new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
|
||||||
if (isVisible())
|
if (isVisible())
|
||||||
@ -1456,6 +1461,46 @@ ChatPage::initiateLogout()
|
|||||||
emit showOverlayProgressBar();
|
emit showOverlayProgressBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ChatPage::query_keys(const std::string &user_id,
|
||||||
|
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb)
|
||||||
|
{
|
||||||
|
auto cache_ = cache::userKeys(user_id);
|
||||||
|
|
||||||
|
if (cache_.has_value()) {
|
||||||
|
if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) {
|
||||||
|
cb(cache_.value(), {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mtx::requests::QueryKeys req;
|
||||||
|
req.device_keys[user_id] = {};
|
||||||
|
|
||||||
|
std::string last_changed;
|
||||||
|
if (cache_)
|
||||||
|
last_changed = cache_->last_changed;
|
||||||
|
req.token = last_changed;
|
||||||
|
|
||||||
|
http::client()->query_keys(req,
|
||||||
|
[cb, user_id, last_changed](const mtx::responses::QueryKeys &res,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn(
|
||||||
|
"failed to query device keys: {},{}",
|
||||||
|
err->matrix_error.errcode,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
cb({}, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache::updateUserKeys(last_changed, res);
|
||||||
|
|
||||||
|
auto keys = cache::userKeys(user_id);
|
||||||
|
cb(keys.value_or(UserKeyCache{}), err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void
|
void
|
||||||
ChatPage::connectCallMessage()
|
ChatPage::connectCallMessage()
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <stack>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
#include <mtx/common.hpp>
|
#include <mtx/common.hpp>
|
||||||
@ -34,6 +35,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "CacheCryptoStructs.h"
|
||||||
#include "CacheStructs.h"
|
#include "CacheStructs.h"
|
||||||
#include "CallManager.h"
|
#include "CallManager.h"
|
||||||
#include "CommunitiesList.h"
|
#include "CommunitiesList.h"
|
||||||
@ -50,6 +52,8 @@ class TextInputWidget;
|
|||||||
class TimelineViewManager;
|
class TimelineViewManager;
|
||||||
class UserInfoWidget;
|
class UserInfoWidget;
|
||||||
class UserSettings;
|
class UserSettings;
|
||||||
|
class NotificationsManager;
|
||||||
|
class TimelineModel;
|
||||||
|
|
||||||
constexpr int CONSENSUS_TIMEOUT = 1000;
|
constexpr int CONSENSUS_TIMEOUT = 1000;
|
||||||
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
|
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
|
||||||
@ -85,6 +89,8 @@ public:
|
|||||||
//! Show the room/group list (if it was visible).
|
//! Show the room/group list (if it was visible).
|
||||||
void showSideBars();
|
void showSideBars();
|
||||||
void initiateLogout();
|
void initiateLogout();
|
||||||
|
void query_keys(const std::string &req,
|
||||||
|
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
|
||||||
void focusMessageInput();
|
void focusMessageInput();
|
||||||
|
|
||||||
QString status() const;
|
QString status() const;
|
||||||
@ -161,6 +167,24 @@ signals:
|
|||||||
void themeChanged();
|
void themeChanged();
|
||||||
void decryptSidebarChanged();
|
void decryptSidebarChanged();
|
||||||
|
|
||||||
|
//! Signals for device verificaiton
|
||||||
|
void receivedDeviceVerificationAccept(
|
||||||
|
const mtx::events::msg::KeyVerificationAccept &message);
|
||||||
|
void receivedDeviceVerificationRequest(
|
||||||
|
const mtx::events::msg::KeyVerificationRequest &message,
|
||||||
|
std::string sender);
|
||||||
|
void receivedRoomDeviceVerificationRequest(
|
||||||
|
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
|
||||||
|
TimelineModel *model);
|
||||||
|
void receivedDeviceVerificationCancel(
|
||||||
|
const mtx::events::msg::KeyVerificationCancel &message);
|
||||||
|
void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
|
||||||
|
void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
|
||||||
|
void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
|
||||||
|
std::string sender);
|
||||||
|
void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
|
||||||
|
void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void showUnreadMessageNotification(int count);
|
void showUnreadMessageNotification(int count);
|
||||||
void logout();
|
void logout();
|
||||||
|
794
src/DeviceVerificationFlow.cpp
Normal file
794
src/DeviceVerificationFlow.cpp
Normal file
@ -0,0 +1,794 @@
|
|||||||
|
#include "DeviceVerificationFlow.h"
|
||||||
|
|
||||||
|
#include "Cache.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
|
#include "Logging.h"
|
||||||
|
#include "timeline/TimelineModel.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
|
namespace msgs = mtx::events::msg;
|
||||||
|
|
||||||
|
static mtx::events::msg::KeyVerificationMac
|
||||||
|
key_verification_mac(mtx::crypto::SAS *sas,
|
||||||
|
mtx::identifiers::User sender,
|
||||||
|
const std::string &senderDevice,
|
||||||
|
mtx::identifiers::User receiver,
|
||||||
|
const std::string &receiverDevice,
|
||||||
|
const std::string &transactionId,
|
||||||
|
std::map<std::string, std::string> keys);
|
||||||
|
|
||||||
|
DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
|
||||||
|
DeviceVerificationFlow::Type flow_type,
|
||||||
|
TimelineModel *model,
|
||||||
|
QString userID,
|
||||||
|
QString deviceId_)
|
||||||
|
: sender(false)
|
||||||
|
, type(flow_type)
|
||||||
|
, deviceId(deviceId_)
|
||||||
|
, model_(model)
|
||||||
|
{
|
||||||
|
timeout = new QTimer(this);
|
||||||
|
timeout->setSingleShot(true);
|
||||||
|
this->sas = olm::client()->sas_init();
|
||||||
|
this->isMacVerified = false;
|
||||||
|
|
||||||
|
auto user_id = userID.toStdString();
|
||||||
|
this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id);
|
||||||
|
ChatPage::instance()->query_keys(
|
||||||
|
user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||||
|
err->matrix_error.errcode,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->deviceId.isEmpty() &&
|
||||||
|
(res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
|
||||||
|
nhlog::net()->warn("no devices retrieved {}", user_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->their_keys = res;
|
||||||
|
});
|
||||||
|
|
||||||
|
ChatPage::instance()->query_keys(
|
||||||
|
http::client()->user_id().to_string(),
|
||||||
|
[this](const UserKeyCache &res, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||||
|
err->matrix_error.errcode,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.master_keys.keys.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (auto status =
|
||||||
|
cache::verificationStatus(http::client()->user_id().to_string());
|
||||||
|
status && status->user_verified)
|
||||||
|
this->our_trusted_master_key = res.master_keys.keys.begin()->second;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (model) {
|
||||||
|
connect(this->model_,
|
||||||
|
&TimelineModel::updateFlowEventId,
|
||||||
|
this,
|
||||||
|
[this](std::string event_id_) {
|
||||||
|
this->relation.rel_type = mtx::common::RelationType::Reference;
|
||||||
|
this->relation.event_id = event_id_;
|
||||||
|
this->transaction_id = event_id_;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(timeout, &QTimer::timeout, this, [this]() {
|
||||||
|
if (state_ != Success && state_ != Failed)
|
||||||
|
this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ChatPage::instance(),
|
||||||
|
&ChatPage::receivedDeviceVerificationStart,
|
||||||
|
this,
|
||||||
|
&DeviceVerificationFlow::handleStartMessage);
|
||||||
|
connect(ChatPage::instance(),
|
||||||
|
&ChatPage::receivedDeviceVerificationAccept,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationAccept &msg) {
|
||||||
|
if (msg.transaction_id.has_value()) {
|
||||||
|
if (msg.transaction_id.value() != this->transaction_id)
|
||||||
|
return;
|
||||||
|
} else if (msg.relates_to.has_value()) {
|
||||||
|
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
|
||||||
|
(msg.hash == "sha256") &&
|
||||||
|
(msg.message_authentication_code == "hkdf-hmac-sha256")) {
|
||||||
|
this->commitment = msg.commitment;
|
||||||
|
if (std::find(msg.short_authentication_string.begin(),
|
||||||
|
msg.short_authentication_string.end(),
|
||||||
|
mtx::events::msg::SASMethods::Emoji) !=
|
||||||
|
msg.short_authentication_string.end()) {
|
||||||
|
this->method = mtx::events::msg::SASMethods::Emoji;
|
||||||
|
} else {
|
||||||
|
this->method = mtx::events::msg::SASMethods::Decimal;
|
||||||
|
}
|
||||||
|
this->mac_method = msg.message_authentication_code;
|
||||||
|
this->sendVerificationKey();
|
||||||
|
} else {
|
||||||
|
this->cancelVerification(
|
||||||
|
DeviceVerificationFlow::Error::UnknownMethod);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ChatPage::instance(),
|
||||||
|
&ChatPage::receivedDeviceVerificationCancel,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationCancel &msg) {
|
||||||
|
if (msg.transaction_id.has_value()) {
|
||||||
|
if (msg.transaction_id.value() != this->transaction_id)
|
||||||
|
return;
|
||||||
|
} else if (msg.relates_to.has_value()) {
|
||||||
|
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error_ = User;
|
||||||
|
emit errorChanged();
|
||||||
|
setState(Failed);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ChatPage::instance(),
|
||||||
|
&ChatPage::receivedDeviceVerificationKey,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationKey &msg) {
|
||||||
|
if (msg.transaction_id.has_value()) {
|
||||||
|
if (msg.transaction_id.value() != this->transaction_id)
|
||||||
|
return;
|
||||||
|
} else if (msg.relates_to.has_value()) {
|
||||||
|
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sender) {
|
||||||
|
if (state_ != WaitingForOtherToAccept) {
|
||||||
|
this->cancelVerification(OutOfOrder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state_ != WaitingForKeys) {
|
||||||
|
this->cancelVerification(OutOfOrder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->sas->set_their_key(msg.key);
|
||||||
|
std::string info;
|
||||||
|
if (this->sender == true) {
|
||||||
|
info = "MATRIX_KEY_VERIFICATION_SAS|" +
|
||||||
|
http::client()->user_id().to_string() + "|" +
|
||||||
|
http::client()->device_id() + "|" + this->sas->public_key() +
|
||||||
|
"|" + this->toClient.to_string() + "|" +
|
||||||
|
this->deviceId.toStdString() + "|" + msg.key + "|" +
|
||||||
|
this->transaction_id;
|
||||||
|
} else {
|
||||||
|
info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
|
||||||
|
"|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
|
||||||
|
http::client()->user_id().to_string() + "|" +
|
||||||
|
http::client()->device_id() + "|" + this->sas->public_key() +
|
||||||
|
"|" + this->transaction_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
nhlog::ui()->info("Info is: '{}'", info);
|
||||||
|
|
||||||
|
if (this->sender == false) {
|
||||||
|
this->sendVerificationKey();
|
||||||
|
} else {
|
||||||
|
if (this->commitment !=
|
||||||
|
mtx::crypto::bin2base64_unpadded(
|
||||||
|
mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
|
||||||
|
this->cancelVerification(
|
||||||
|
DeviceVerificationFlow::Error::MismatchedCommitment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->method == mtx::events::msg::SASMethods::Emoji) {
|
||||||
|
this->sasList = this->sas->generate_bytes_emoji(info);
|
||||||
|
setState(CompareEmoji);
|
||||||
|
} else if (this->method == mtx::events::msg::SASMethods::Decimal) {
|
||||||
|
this->sasList = this->sas->generate_bytes_decimal(info);
|
||||||
|
setState(CompareNumber);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(
|
||||||
|
ChatPage::instance(),
|
||||||
|
&ChatPage::receivedDeviceVerificationMac,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationMac &msg) {
|
||||||
|
if (msg.transaction_id.has_value()) {
|
||||||
|
if (msg.transaction_id.value() != this->transaction_id)
|
||||||
|
return;
|
||||||
|
} else if (msg.relates_to.has_value()) {
|
||||||
|
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, std::string> key_list;
|
||||||
|
std::string key_string;
|
||||||
|
for (const auto &mac : msg.mac) {
|
||||||
|
for (const auto &[deviceid, key] : their_keys.device_keys) {
|
||||||
|
(void)deviceid;
|
||||||
|
if (key.keys.count(mac.first))
|
||||||
|
key_list[mac.first] = key.keys.at(mac.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (their_keys.master_keys.keys.count(mac.first))
|
||||||
|
key_list[mac.first] = their_keys.master_keys.keys[mac.first];
|
||||||
|
if (their_keys.user_signing_keys.keys.count(mac.first))
|
||||||
|
key_list[mac.first] =
|
||||||
|
their_keys.user_signing_keys.keys[mac.first];
|
||||||
|
if (their_keys.self_signing_keys.keys.count(mac.first))
|
||||||
|
key_list[mac.first] =
|
||||||
|
their_keys.self_signing_keys.keys[mac.first];
|
||||||
|
}
|
||||||
|
auto macs = key_verification_mac(sas.get(),
|
||||||
|
toClient,
|
||||||
|
this->deviceId.toStdString(),
|
||||||
|
http::client()->user_id(),
|
||||||
|
http::client()->device_id(),
|
||||||
|
this->transaction_id,
|
||||||
|
key_list);
|
||||||
|
|
||||||
|
for (const auto &[key, mac] : macs.mac) {
|
||||||
|
if (mac != msg.mac.at(key)) {
|
||||||
|
this->cancelVerification(
|
||||||
|
DeviceVerificationFlow::Error::KeyMismatch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.keys == macs.keys) {
|
||||||
|
mtx::requests::KeySignaturesUpload req;
|
||||||
|
if (utils::localUser().toStdString() == this->toClient.to_string()) {
|
||||||
|
// self verification, sign master key with device key, if we
|
||||||
|
// verified it
|
||||||
|
for (const auto &mac : msg.mac) {
|
||||||
|
if (their_keys.master_keys.keys.count(mac.first)) {
|
||||||
|
json j = their_keys.master_keys;
|
||||||
|
j.erase("signatures");
|
||||||
|
j.erase("unsigned");
|
||||||
|
mtx::crypto::CrossSigningKeys master_key = j;
|
||||||
|
master_key
|
||||||
|
.signatures[utils::localUser().toStdString()]
|
||||||
|
["ed25519:" +
|
||||||
|
http::client()->device_id()] =
|
||||||
|
olm::client()->sign_message(j.dump());
|
||||||
|
req.signatures[utils::localUser().toStdString()]
|
||||||
|
[master_key.keys.at(mac.first)] =
|
||||||
|
master_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(Nico): Sign their device key with self signing key
|
||||||
|
} else {
|
||||||
|
// TODO(Nico): Sign their master key with user signing key
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.signatures.empty()) {
|
||||||
|
http::client()->keys_signatures_upload(
|
||||||
|
req,
|
||||||
|
[](const mtx::responses::KeySignaturesUpload &res,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->error(
|
||||||
|
"failed to upload signatures: {},{}",
|
||||||
|
err->matrix_error.errcode,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &[user_id, tmp] : res.errors)
|
||||||
|
for (const auto &[key_id, e] : tmp)
|
||||||
|
nhlog::net()->error(
|
||||||
|
"signature error for user {} and key "
|
||||||
|
"id {}: {}, {}",
|
||||||
|
user_id,
|
||||||
|
key_id,
|
||||||
|
e.errcode,
|
||||||
|
e.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this->isMacVerified = true;
|
||||||
|
this->acceptDevice();
|
||||||
|
} else {
|
||||||
|
this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ChatPage::instance(),
|
||||||
|
&ChatPage::receivedDeviceVerificationReady,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationReady &msg) {
|
||||||
|
if (!sender) {
|
||||||
|
if (msg.from_device != http::client()->device_id()) {
|
||||||
|
error_ = User;
|
||||||
|
emit errorChanged();
|
||||||
|
setState(Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.transaction_id.has_value()) {
|
||||||
|
if (msg.transaction_id.value() != this->transaction_id)
|
||||||
|
return;
|
||||||
|
} else if ((msg.relates_to.has_value() && sender)) {
|
||||||
|
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||||
|
return;
|
||||||
|
else {
|
||||||
|
this->deviceId = QString::fromStdString(msg.from_device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->startVerificationRequest();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ChatPage::instance(),
|
||||||
|
&ChatPage::receivedDeviceVerificationDone,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationDone &msg) {
|
||||||
|
if (msg.transaction_id.has_value()) {
|
||||||
|
if (msg.transaction_id.value() != this->transaction_id)
|
||||||
|
return;
|
||||||
|
} else if (msg.relates_to.has_value()) {
|
||||||
|
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nhlog::ui()->info("Flow done on other side");
|
||||||
|
});
|
||||||
|
|
||||||
|
timeout->start(TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
DeviceVerificationFlow::state()
|
||||||
|
{
|
||||||
|
switch (state_) {
|
||||||
|
case PromptStartVerification:
|
||||||
|
return "PromptStartVerification";
|
||||||
|
case CompareEmoji:
|
||||||
|
return "CompareEmoji";
|
||||||
|
case CompareNumber:
|
||||||
|
return "CompareNumber";
|
||||||
|
case WaitingForKeys:
|
||||||
|
return "WaitingForKeys";
|
||||||
|
case WaitingForOtherToAccept:
|
||||||
|
return "WaitingForOtherToAccept";
|
||||||
|
case WaitingForMac:
|
||||||
|
return "WaitingForMac";
|
||||||
|
case Success:
|
||||||
|
return "Success";
|
||||||
|
case Failed:
|
||||||
|
return "Failed";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::next()
|
||||||
|
{
|
||||||
|
if (sender) {
|
||||||
|
switch (state_) {
|
||||||
|
case PromptStartVerification:
|
||||||
|
sendVerificationRequest();
|
||||||
|
break;
|
||||||
|
case CompareEmoji:
|
||||||
|
case CompareNumber:
|
||||||
|
sendVerificationMac();
|
||||||
|
break;
|
||||||
|
case WaitingForKeys:
|
||||||
|
case WaitingForOtherToAccept:
|
||||||
|
case WaitingForMac:
|
||||||
|
case Success:
|
||||||
|
case Failed:
|
||||||
|
nhlog::db()->error("verification: Invalid state transition!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (state_) {
|
||||||
|
case PromptStartVerification:
|
||||||
|
if (canonical_json.is_null())
|
||||||
|
sendVerificationReady();
|
||||||
|
else // legacy path without request and ready
|
||||||
|
acceptVerificationRequest();
|
||||||
|
break;
|
||||||
|
case CompareEmoji:
|
||||||
|
[[fallthrough]];
|
||||||
|
case CompareNumber:
|
||||||
|
sendVerificationMac();
|
||||||
|
break;
|
||||||
|
case WaitingForKeys:
|
||||||
|
case WaitingForOtherToAccept:
|
||||||
|
case WaitingForMac:
|
||||||
|
case Success:
|
||||||
|
case Failed:
|
||||||
|
nhlog::db()->error("verification: Invalid state transition!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
DeviceVerificationFlow::getUserId()
|
||||||
|
{
|
||||||
|
return QString::fromStdString(this->toClient.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
DeviceVerificationFlow::getDeviceId()
|
||||||
|
{
|
||||||
|
return this->deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
DeviceVerificationFlow::getSender()
|
||||||
|
{
|
||||||
|
return this->sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int>
|
||||||
|
DeviceVerificationFlow::getSasList()
|
||||||
|
{
|
||||||
|
return this->sasList;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::setEventId(std::string event_id_)
|
||||||
|
{
|
||||||
|
this->relation.rel_type = mtx::common::RelationType::Reference;
|
||||||
|
this->relation.event_id = event_id_;
|
||||||
|
this->transaction_id = event_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
|
||||||
|
std::string)
|
||||||
|
{
|
||||||
|
if (msg.transaction_id.has_value()) {
|
||||||
|
if (msg.transaction_id.value() != this->transaction_id)
|
||||||
|
return;
|
||||||
|
} else if (msg.relates_to.has_value()) {
|
||||||
|
if (msg.relates_to.value().event_id != this->relation.event_id)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((std::find(msg.key_agreement_protocols.begin(),
|
||||||
|
msg.key_agreement_protocols.end(),
|
||||||
|
"curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
|
||||||
|
(std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
|
||||||
|
(std::find(msg.message_authentication_codes.begin(),
|
||||||
|
msg.message_authentication_codes.end(),
|
||||||
|
"hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
|
||||||
|
if (std::find(msg.short_authentication_string.begin(),
|
||||||
|
msg.short_authentication_string.end(),
|
||||||
|
mtx::events::msg::SASMethods::Emoji) !=
|
||||||
|
msg.short_authentication_string.end()) {
|
||||||
|
this->method = mtx::events::msg::SASMethods::Emoji;
|
||||||
|
} else if (std::find(msg.short_authentication_string.begin(),
|
||||||
|
msg.short_authentication_string.end(),
|
||||||
|
mtx::events::msg::SASMethods::Decimal) !=
|
||||||
|
msg.short_authentication_string.end()) {
|
||||||
|
this->method = mtx::events::msg::SASMethods::Decimal;
|
||||||
|
} else {
|
||||||
|
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sender)
|
||||||
|
this->canonical_json = nlohmann::json(msg);
|
||||||
|
else {
|
||||||
|
if (utils::localUser().toStdString() < this->toClient.to_string()) {
|
||||||
|
this->canonical_json = nlohmann::json(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state_ != PromptStartVerification)
|
||||||
|
this->acceptVerificationRequest();
|
||||||
|
} else {
|
||||||
|
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! accepts a verification
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::acceptVerificationRequest()
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationAccept req;
|
||||||
|
|
||||||
|
req.method = mtx::events::msg::VerificationMethods::SASv1;
|
||||||
|
req.key_agreement_protocol = "curve25519-hkdf-sha256";
|
||||||
|
req.hash = "sha256";
|
||||||
|
req.message_authentication_code = "hkdf-hmac-sha256";
|
||||||
|
if (this->method == mtx::events::msg::SASMethods::Emoji)
|
||||||
|
req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
|
||||||
|
else if (this->method == mtx::events::msg::SASMethods::Decimal)
|
||||||
|
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
|
||||||
|
req.commitment = mtx::crypto::bin2base64_unpadded(
|
||||||
|
mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
|
||||||
|
|
||||||
|
send(req);
|
||||||
|
setState(WaitingForKeys);
|
||||||
|
}
|
||||||
|
//! responds verification request
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::sendVerificationReady()
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationReady req;
|
||||||
|
|
||||||
|
req.from_device = http::client()->device_id();
|
||||||
|
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
|
||||||
|
|
||||||
|
send(req);
|
||||||
|
setState(WaitingForKeys);
|
||||||
|
}
|
||||||
|
//! accepts a verification
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::sendVerificationDone()
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationDone req;
|
||||||
|
|
||||||
|
send(req);
|
||||||
|
}
|
||||||
|
//! starts the verification flow
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::startVerificationRequest()
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationStart req;
|
||||||
|
|
||||||
|
req.from_device = http::client()->device_id();
|
||||||
|
req.method = mtx::events::msg::VerificationMethods::SASv1;
|
||||||
|
req.key_agreement_protocols = {"curve25519-hkdf-sha256"};
|
||||||
|
req.hashes = {"sha256"};
|
||||||
|
req.message_authentication_codes = {"hkdf-hmac-sha256"};
|
||||||
|
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal,
|
||||||
|
mtx::events::msg::SASMethods::Emoji};
|
||||||
|
|
||||||
|
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
|
||||||
|
mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
|
||||||
|
req.transaction_id = this->transaction_id;
|
||||||
|
this->canonical_json = nlohmann::json(req);
|
||||||
|
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
|
||||||
|
req.relates_to = this->relation;
|
||||||
|
this->canonical_json = nlohmann::json(req);
|
||||||
|
}
|
||||||
|
send(req);
|
||||||
|
setState(WaitingForOtherToAccept);
|
||||||
|
}
|
||||||
|
//! sends a verification request
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::sendVerificationRequest()
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationRequest req;
|
||||||
|
|
||||||
|
req.from_device = http::client()->device_id();
|
||||||
|
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
|
||||||
|
|
||||||
|
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
|
||||||
|
QDateTime currentTime = QDateTime::currentDateTimeUtc();
|
||||||
|
|
||||||
|
req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
|
||||||
|
|
||||||
|
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
|
||||||
|
req.to = this->toClient.to_string();
|
||||||
|
req.msgtype = "m.key.verification.request";
|
||||||
|
req.body = "User is requesting to verify keys with you. However, your client does "
|
||||||
|
"not support this method, so you will need to use the legacy method of "
|
||||||
|
"key verification.";
|
||||||
|
}
|
||||||
|
|
||||||
|
send(req);
|
||||||
|
setState(WaitingForOtherToAccept);
|
||||||
|
}
|
||||||
|
//! cancels a verification flow
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationCancel req;
|
||||||
|
|
||||||
|
if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
|
||||||
|
req.code = "m.unknown_method";
|
||||||
|
req.reason = "unknown method received";
|
||||||
|
} else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
|
||||||
|
req.code = "m.mismatched_commitment";
|
||||||
|
req.reason = "commitment didn't match";
|
||||||
|
} else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
|
||||||
|
req.code = "m.mismatched_sas";
|
||||||
|
req.reason = "sas didn't match";
|
||||||
|
} else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
|
||||||
|
req.code = "m.key_match";
|
||||||
|
req.reason = "keys did not match";
|
||||||
|
} else if (error_code == DeviceVerificationFlow::Error::Timeout) {
|
||||||
|
req.code = "m.timeout";
|
||||||
|
req.reason = "timed out";
|
||||||
|
} else if (error_code == DeviceVerificationFlow::Error::User) {
|
||||||
|
req.code = "m.user";
|
||||||
|
req.reason = "user cancelled the verification";
|
||||||
|
} else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
|
||||||
|
req.code = "m.unexpected_message";
|
||||||
|
req.reason = "received messages out of order";
|
||||||
|
}
|
||||||
|
|
||||||
|
this->error_ = error_code;
|
||||||
|
emit errorChanged();
|
||||||
|
this->setState(Failed);
|
||||||
|
|
||||||
|
send(req);
|
||||||
|
}
|
||||||
|
//! sends the verification key
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::sendVerificationKey()
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationKey req;
|
||||||
|
|
||||||
|
req.key = this->sas->public_key();
|
||||||
|
|
||||||
|
send(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
mtx::events::msg::KeyVerificationMac
|
||||||
|
key_verification_mac(mtx::crypto::SAS *sas,
|
||||||
|
mtx::identifiers::User sender,
|
||||||
|
const std::string &senderDevice,
|
||||||
|
mtx::identifiers::User receiver,
|
||||||
|
const std::string &receiverDevice,
|
||||||
|
const std::string &transactionId,
|
||||||
|
std::map<std::string, std::string> keys)
|
||||||
|
{
|
||||||
|
mtx::events::msg::KeyVerificationMac req;
|
||||||
|
|
||||||
|
std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
|
||||||
|
receiver.to_string() + receiverDevice + transactionId;
|
||||||
|
|
||||||
|
std::string key_list;
|
||||||
|
bool first = true;
|
||||||
|
for (const auto &[key_id, key] : keys) {
|
||||||
|
req.mac[key_id] = sas->calculate_mac(key, info + key_id);
|
||||||
|
|
||||||
|
if (!first)
|
||||||
|
key_list += ",";
|
||||||
|
key_list += key_id;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
|
||||||
|
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
//! sends the mac of the keys
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::sendVerificationMac()
|
||||||
|
{
|
||||||
|
std::map<std::string, std::string> key_list;
|
||||||
|
key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
|
||||||
|
|
||||||
|
// send our master key, if we trust it
|
||||||
|
if (!this->our_trusted_master_key.empty())
|
||||||
|
key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
|
||||||
|
|
||||||
|
mtx::events::msg::KeyVerificationMac req =
|
||||||
|
key_verification_mac(sas.get(),
|
||||||
|
http::client()->user_id(),
|
||||||
|
http::client()->device_id(),
|
||||||
|
this->toClient,
|
||||||
|
this->deviceId.toStdString(),
|
||||||
|
this->transaction_id,
|
||||||
|
key_list);
|
||||||
|
|
||||||
|
send(req);
|
||||||
|
|
||||||
|
setState(WaitingForMac);
|
||||||
|
acceptDevice();
|
||||||
|
}
|
||||||
|
//! Completes the verification flow
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::acceptDevice()
|
||||||
|
{
|
||||||
|
if (!isMacVerified) {
|
||||||
|
setState(WaitingForMac);
|
||||||
|
} else if (state_ == WaitingForMac) {
|
||||||
|
cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
|
||||||
|
this->sendVerificationDone();
|
||||||
|
setState(Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DeviceVerificationFlow::unverify()
|
||||||
|
{
|
||||||
|
cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
|
||||||
|
|
||||||
|
emit refreshProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<DeviceVerificationFlow>
|
||||||
|
DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
|
||||||
|
TimelineModel *timelineModel_,
|
||||||
|
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||||
|
QString other_user_,
|
||||||
|
QString event_id_)
|
||||||
|
{
|
||||||
|
QSharedPointer<DeviceVerificationFlow> flow(
|
||||||
|
new DeviceVerificationFlow(parent_,
|
||||||
|
Type::RoomMsg,
|
||||||
|
timelineModel_,
|
||||||
|
other_user_,
|
||||||
|
QString::fromStdString(msg.from_device)));
|
||||||
|
|
||||||
|
flow->setEventId(event_id_.toStdString());
|
||||||
|
|
||||||
|
if (std::find(msg.methods.begin(),
|
||||||
|
msg.methods.end(),
|
||||||
|
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
|
||||||
|
flow->cancelVerification(UnknownMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
QSharedPointer<DeviceVerificationFlow>
|
||||||
|
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
|
||||||
|
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||||
|
QString other_user_,
|
||||||
|
QString txn_id_)
|
||||||
|
{
|
||||||
|
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
|
||||||
|
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
|
||||||
|
flow->transaction_id = txn_id_.toStdString();
|
||||||
|
|
||||||
|
if (std::find(msg.methods.begin(),
|
||||||
|
msg.methods.end(),
|
||||||
|
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
|
||||||
|
flow->cancelVerification(UnknownMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
QSharedPointer<DeviceVerificationFlow>
|
||||||
|
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
|
||||||
|
const mtx::events::msg::KeyVerificationStart &msg,
|
||||||
|
QString other_user_,
|
||||||
|
QString txn_id_)
|
||||||
|
{
|
||||||
|
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
|
||||||
|
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
|
||||||
|
flow->transaction_id = txn_id_.toStdString();
|
||||||
|
|
||||||
|
flow->handleStartMessage(msg, "");
|
||||||
|
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
QSharedPointer<DeviceVerificationFlow>
|
||||||
|
DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
|
||||||
|
TimelineModel *timelineModel_,
|
||||||
|
QString userid)
|
||||||
|
{
|
||||||
|
QSharedPointer<DeviceVerificationFlow> flow(
|
||||||
|
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
|
||||||
|
flow->sender = true;
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
QSharedPointer<DeviceVerificationFlow>
|
||||||
|
DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
|
||||||
|
{
|
||||||
|
QSharedPointer<DeviceVerificationFlow> flow(
|
||||||
|
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
|
||||||
|
|
||||||
|
flow->sender = true;
|
||||||
|
flow->transaction_id = http::client()->generate_txn_id();
|
||||||
|
|
||||||
|
return flow;
|
||||||
|
}
|
235
src/DeviceVerificationFlow.h
Normal file
235
src/DeviceVerificationFlow.h
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <mtx/responses/crypto.hpp>
|
||||||
|
|
||||||
|
#include "CacheCryptoStructs.h"
|
||||||
|
#include "Logging.h"
|
||||||
|
#include "MatrixClient.h"
|
||||||
|
#include "Olm.h"
|
||||||
|
#include "timeline/TimelineModel.h"
|
||||||
|
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
|
using sas_ptr = std::unique_ptr<mtx::crypto::SAS>;
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
/*
|
||||||
|
* Stolen from fluffy chat :D
|
||||||
|
*
|
||||||
|
* State | +-------------+ +-----------+ |
|
||||||
|
* | | AliceDevice | | BobDevice | |
|
||||||
|
* | | (sender) | | | |
|
||||||
|
* | +-------------+ +-----------+ |
|
||||||
|
* promptStartVerify | | | |
|
||||||
|
* | o | (m.key.verification.request) | |
|
||||||
|
* | p |-------------------------------->| (ASK FOR VERIFICATION REQUEST) |
|
||||||
|
* waitForOtherAccept | t | | | promptStartVerify
|
||||||
|
* && | i | (m.key.verification.ready) | |
|
||||||
|
* no commitment | o |<--------------------------------| |
|
||||||
|
* && | n | | |
|
||||||
|
* no canonical_json | a | (m.key.verification.start) | | waitingForKeys
|
||||||
|
* | l |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
|
||||||
|
* | | | | && no canonical_json
|
||||||
|
* | | m.key.verification.start | |
|
||||||
|
* waitForOtherAccept | |-------------------------------->| (IF NOT ALREADY ASKED, |
|
||||||
|
* && | | | ASK FOR VERIFICATION REQUEST) | promptStartVerify, if not accepted
|
||||||
|
* canonical_json | | m.key.verification.accept | |
|
||||||
|
* | |<--------------------------------| |
|
||||||
|
* waitForOtherAccept | | | | waitingForKeys
|
||||||
|
* && | | m.key.verification.key | | && canonical_json
|
||||||
|
* commitment | |-------------------------------->| | && commitment
|
||||||
|
* | | | |
|
||||||
|
* | | m.key.verification.key | |
|
||||||
|
* | |<--------------------------------| |
|
||||||
|
* compareEmoji/Number| | | | compareEmoji/Number
|
||||||
|
* | | COMPARE EMOJI / NUMBERS | |
|
||||||
|
* | | | |
|
||||||
|
* waitingForMac | | m.key.verification.mac | | waitingForMac
|
||||||
|
* | success |<------------------------------->| success |
|
||||||
|
* | | | |
|
||||||
|
* success/fail | | m.key.verification.done | | success/fail
|
||||||
|
* | |<------------------------------->| |
|
||||||
|
*/
|
||||||
|
// clang-format on
|
||||||
|
class DeviceVerificationFlow : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QString state READ state NOTIFY stateChanged)
|
||||||
|
Q_PROPERTY(Error error READ error NOTIFY errorChanged)
|
||||||
|
Q_PROPERTY(QString userId READ getUserId CONSTANT)
|
||||||
|
Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
|
||||||
|
Q_PROPERTY(bool sender READ getSender CONSTANT)
|
||||||
|
Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum State
|
||||||
|
{
|
||||||
|
PromptStartVerification,
|
||||||
|
WaitingForOtherToAccept,
|
||||||
|
WaitingForKeys,
|
||||||
|
CompareEmoji,
|
||||||
|
CompareNumber,
|
||||||
|
WaitingForMac,
|
||||||
|
Success,
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
Q_ENUM(State)
|
||||||
|
|
||||||
|
enum Type
|
||||||
|
{
|
||||||
|
ToDevice,
|
||||||
|
RoomMsg
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Error
|
||||||
|
{
|
||||||
|
UnknownMethod,
|
||||||
|
MismatchedCommitment,
|
||||||
|
MismatchedSAS,
|
||||||
|
KeyMismatch,
|
||||||
|
Timeout,
|
||||||
|
User,
|
||||||
|
OutOfOrder,
|
||||||
|
};
|
||||||
|
Q_ENUM(Error)
|
||||||
|
|
||||||
|
static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification(
|
||||||
|
QObject *parent_,
|
||||||
|
TimelineModel *timelineModel_,
|
||||||
|
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||||
|
QString other_user_,
|
||||||
|
QString event_id_);
|
||||||
|
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
|
||||||
|
QObject *parent_,
|
||||||
|
const mtx::events::msg::KeyVerificationRequest &msg,
|
||||||
|
QString other_user_,
|
||||||
|
QString txn_id_);
|
||||||
|
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
|
||||||
|
QObject *parent_,
|
||||||
|
const mtx::events::msg::KeyVerificationStart &msg,
|
||||||
|
QString other_user_,
|
||||||
|
QString txn_id_);
|
||||||
|
static QSharedPointer<DeviceVerificationFlow>
|
||||||
|
InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
|
||||||
|
static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent,
|
||||||
|
QString userid,
|
||||||
|
QString device);
|
||||||
|
|
||||||
|
// getters
|
||||||
|
QString state();
|
||||||
|
Error error() { return error_; }
|
||||||
|
QString getUserId();
|
||||||
|
QString getDeviceId();
|
||||||
|
bool getSender();
|
||||||
|
std::vector<int> getSasList();
|
||||||
|
QString transactionId() { return QString::fromStdString(this->transaction_id); }
|
||||||
|
// setters
|
||||||
|
void setDeviceId(QString deviceID);
|
||||||
|
void setEventId(std::string event_id);
|
||||||
|
|
||||||
|
void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
//! unverifies a device
|
||||||
|
void unverify();
|
||||||
|
//! Continues the flow
|
||||||
|
void next();
|
||||||
|
//! Cancel the flow
|
||||||
|
void cancel() { cancelVerification(User); }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void refreshProfile();
|
||||||
|
void stateChanged();
|
||||||
|
void errorChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
DeviceVerificationFlow(QObject *,
|
||||||
|
DeviceVerificationFlow::Type flow_type,
|
||||||
|
TimelineModel *model,
|
||||||
|
QString userID,
|
||||||
|
QString deviceId_);
|
||||||
|
void setState(State state)
|
||||||
|
{
|
||||||
|
if (state != state_) {
|
||||||
|
state_ = state;
|
||||||
|
emit stateChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
|
||||||
|
//! sends a verification request
|
||||||
|
void sendVerificationRequest();
|
||||||
|
//! accepts a verification request
|
||||||
|
void sendVerificationReady();
|
||||||
|
//! completes the verification flow();
|
||||||
|
void sendVerificationDone();
|
||||||
|
//! accepts a verification
|
||||||
|
void acceptVerificationRequest();
|
||||||
|
//! starts the verification flow
|
||||||
|
void startVerificationRequest();
|
||||||
|
//! cancels a verification flow
|
||||||
|
void cancelVerification(DeviceVerificationFlow::Error error_code);
|
||||||
|
//! sends the verification key
|
||||||
|
void sendVerificationKey();
|
||||||
|
//! sends the mac of the keys
|
||||||
|
void sendVerificationMac();
|
||||||
|
//! Completes the verification flow
|
||||||
|
void acceptDevice();
|
||||||
|
|
||||||
|
std::string transaction_id;
|
||||||
|
|
||||||
|
bool sender;
|
||||||
|
Type type;
|
||||||
|
mtx::identifiers::User toClient;
|
||||||
|
QString deviceId;
|
||||||
|
|
||||||
|
// public part of our master key, when trusted or empty
|
||||||
|
std::string our_trusted_master_key;
|
||||||
|
|
||||||
|
mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
|
||||||
|
QTimer *timeout = nullptr;
|
||||||
|
sas_ptr sas;
|
||||||
|
std::string mac_method;
|
||||||
|
std::string commitment;
|
||||||
|
nlohmann::json canonical_json;
|
||||||
|
|
||||||
|
std::vector<int> sasList;
|
||||||
|
UserKeyCache their_keys;
|
||||||
|
TimelineModel *model_;
|
||||||
|
mtx::common::RelatesTo relation;
|
||||||
|
|
||||||
|
State state_ = PromptStartVerification;
|
||||||
|
Error error_ = UnknownMethod;
|
||||||
|
|
||||||
|
bool isMacVerified = false;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void send(T msg)
|
||||||
|
{
|
||||||
|
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
|
||||||
|
mtx::requests::ToDeviceMessages<T> body;
|
||||||
|
msg.transaction_id = this->transaction_id;
|
||||||
|
body[this->toClient][deviceId.toStdString()] = msg;
|
||||||
|
|
||||||
|
http::client()->send_to_device<T>(
|
||||||
|
this->transaction_id, body, [](mtx::http::RequestErr err) {
|
||||||
|
if (err)
|
||||||
|
nhlog::net()->warn(
|
||||||
|
"failed to send verification to_device message: {} {}",
|
||||||
|
err->matrix_error.error,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
});
|
||||||
|
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
|
||||||
|
if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>)
|
||||||
|
msg.relates_to = this->relation;
|
||||||
|
(model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
nhlog::net()->debug(
|
||||||
|
"Sent verification step: {} in state: {}",
|
||||||
|
mtx::events::to_string(mtx::events::to_device_content_to_type<T>),
|
||||||
|
state().toStdString());
|
||||||
|
}
|
||||||
|
};
|
@ -39,8 +39,15 @@ struct EventMsgType
|
|||||||
template<class T>
|
template<class T>
|
||||||
mtx::events::MessageType operator()(const mtx::events::Event<T> &e)
|
mtx::events::MessageType operator()(const mtx::events::Event<T> &e)
|
||||||
{
|
{
|
||||||
if constexpr (is_detected<msgtype_t, T>::value)
|
if constexpr (is_detected<msgtype_t, T>::value) {
|
||||||
|
if constexpr (std::is_same_v<std::optional<std::string>,
|
||||||
|
std::remove_cv_t<decltype(e.content.msgtype)>>)
|
||||||
|
return mtx::events::getMessageType(e.content.msgtype.value());
|
||||||
|
else if constexpr (std::is_same_v<
|
||||||
|
std::string,
|
||||||
|
std::remove_cv_t<decltype(e.content.msgtype)>>)
|
||||||
return mtx::events::getMessageType(e.content.msgtype);
|
return mtx::events::getMessageType(e.content.msgtype);
|
||||||
|
}
|
||||||
return mtx::events::MessageType::Unknown;
|
return mtx::events::MessageType::Unknown;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -97,8 +104,15 @@ struct EventBody
|
|||||||
template<class T>
|
template<class T>
|
||||||
std::string operator()(const mtx::events::Event<T> &e)
|
std::string operator()(const mtx::events::Event<T> &e)
|
||||||
{
|
{
|
||||||
if constexpr (is_detected<body_t, T>::value)
|
if constexpr (is_detected<body_t, T>::value) {
|
||||||
|
if constexpr (std::is_same_v<std::optional<std::string>,
|
||||||
|
std::remove_cv_t<decltype(e.content.body)>>)
|
||||||
|
return e.content.body ? e.content.body.value() : "";
|
||||||
|
else if constexpr (std::is_same_v<
|
||||||
|
std::string,
|
||||||
|
std::remove_cv_t<decltype(e.content.body)>>)
|
||||||
return e.content.body;
|
return e.content.body;
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -328,15 +328,6 @@ MainWindow::hasActiveUser()
|
|||||||
settings.contains("auth/user_id");
|
settings.contains("auth/user_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
MainWindow::openUserProfile(const QString &user_id, const QString &room_id)
|
|
||||||
{
|
|
||||||
auto dialog = new dialogs::UserProfile(this);
|
|
||||||
dialog->init(user_id, room_id);
|
|
||||||
|
|
||||||
showDialog(dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
MainWindow::openRoomSettings(const QString &room_id)
|
MainWindow::openRoomSettings(const QString &room_id)
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
#include <QSystemTrayIcon>
|
#include <QSystemTrayIcon>
|
||||||
|
|
||||||
#include "UserSettingsPage.h"
|
#include "UserSettingsPage.h"
|
||||||
#include "dialogs/UserProfile.h"
|
|
||||||
#include "ui/OverlayModal.h"
|
#include "ui/OverlayModal.h"
|
||||||
|
|
||||||
#include "jdenticoninterface.h"
|
#include "jdenticoninterface.h"
|
||||||
@ -76,7 +75,6 @@ public:
|
|||||||
void openLogoutDialog();
|
void openLogoutDialog();
|
||||||
void openRoomSettings(const QString &room_id);
|
void openRoomSettings(const QString &room_id);
|
||||||
void openMemberListDialog(const QString &room_id);
|
void openMemberListDialog(const QString &room_id);
|
||||||
void openUserProfile(const QString &user_id, const QString &room_id);
|
|
||||||
void openReadReceiptsDialog(const QString &event_id);
|
void openReadReceiptsDialog(const QString &event_id);
|
||||||
|
|
||||||
void hideOverlay();
|
void hideOverlay();
|
||||||
|
203
src/Olm.cpp
203
src/Olm.cpp
@ -1,9 +1,12 @@
|
|||||||
|
#include <QObject>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
#include "Olm.h"
|
#include "Olm.h"
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "Cache_p.h"
|
#include "Cache_p.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
|
#include "DeviceVerificationFlow.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
@ -28,7 +31,6 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
|
|||||||
{
|
{
|
||||||
if (msgs.empty())
|
if (msgs.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
nhlog::crypto()->info("received {} to_device messages", msgs.size());
|
nhlog::crypto()->info("received {} to_device messages", msgs.size());
|
||||||
nlohmann::json j_msg;
|
nlohmann::json j_msg;
|
||||||
|
|
||||||
@ -53,6 +55,10 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
|
|||||||
nhlog::crypto()->warn("validation error for olm message: {} {}",
|
nhlog::crypto()->warn("validation error for olm message: {} {}",
|
||||||
e.what(),
|
e.what(),
|
||||||
j_msg.dump(2));
|
j_msg.dump(2));
|
||||||
|
|
||||||
|
nhlog::crypto()->warn("validation error for olm message: {} {}",
|
||||||
|
e.what(),
|
||||||
|
j_msg.dump(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) {
|
} else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) {
|
||||||
@ -71,6 +77,43 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
|
|||||||
e.what(),
|
e.what(),
|
||||||
j_msg.dump(2));
|
j_msg.dump(2));
|
||||||
}
|
}
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
|
||||||
|
auto message = std::get<
|
||||||
|
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationAccept>>(msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationAccept(message.content);
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) {
|
||||||
|
auto message = std::get<
|
||||||
|
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationRequest>>(msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationRequest(message.content,
|
||||||
|
message.sender);
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) {
|
||||||
|
auto message = std::get<
|
||||||
|
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationCancel>>(msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationCancel(message.content);
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) {
|
||||||
|
auto message =
|
||||||
|
std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationKey>>(
|
||||||
|
msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationKey(message.content);
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) {
|
||||||
|
auto message =
|
||||||
|
std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationMac>>(
|
||||||
|
msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationMac(message.content);
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) {
|
||||||
|
auto message = std::get<
|
||||||
|
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationStart>>(msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationStart(message.content,
|
||||||
|
message.sender);
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) {
|
||||||
|
auto message = std::get<
|
||||||
|
mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationReady>>(msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationReady(message.content);
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) {
|
||||||
|
auto message =
|
||||||
|
std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>(
|
||||||
|
msg);
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationDone(message.content);
|
||||||
} else {
|
} else {
|
||||||
nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
|
nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
|
||||||
}
|
}
|
||||||
@ -95,23 +138,76 @@ handle_olm_message(const OlmMessage &msg)
|
|||||||
|
|
||||||
auto payload = try_olm_decryption(msg.sender_key, cipher.second);
|
auto payload = try_olm_decryption(msg.sender_key, cipher.second);
|
||||||
|
|
||||||
|
if (payload.is_null()) {
|
||||||
|
// Check for PRE_KEY message
|
||||||
|
if (cipher.second.type == 0) {
|
||||||
|
payload = handle_pre_key_olm_message(
|
||||||
|
msg.sender, msg.sender_key, cipher.second);
|
||||||
|
} else {
|
||||||
|
nhlog::crypto()->error("Undecryptable olm message!");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!payload.is_null()) {
|
if (!payload.is_null()) {
|
||||||
nhlog::crypto()->debug("decrypted olm payload: {}", payload.dump(2));
|
std::string msg_type = payload["type"];
|
||||||
create_inbound_megolm_session(msg.sender, msg.sender_key, payload);
|
|
||||||
|
if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationAccept(
|
||||||
|
payload["content"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::KeyVerificationRequest)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationRequest(
|
||||||
|
payload["content"], payload["sender"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::KeyVerificationCancel)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationCancel(
|
||||||
|
payload["content"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::KeyVerificationKey)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationKey(
|
||||||
|
payload["content"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::KeyVerificationMac)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationMac(
|
||||||
|
payload["content"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::KeyVerificationStart)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationStart(
|
||||||
|
payload["content"], payload["sender"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::KeyVerificationReady)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationReady(
|
||||||
|
payload["content"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::KeyVerificationDone)) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationDone(
|
||||||
|
payload["content"]);
|
||||||
|
return;
|
||||||
|
} else if (msg_type == to_string(mtx::events::EventType::RoomKey)) {
|
||||||
|
mtx::events::DeviceEvent<mtx::events::msg::RoomKey> roomKey =
|
||||||
|
payload;
|
||||||
|
create_inbound_megolm_session(roomKey, msg.sender_key);
|
||||||
|
return;
|
||||||
|
} else if (msg_type ==
|
||||||
|
to_string(mtx::events::EventType::ForwardedRoomKey)) {
|
||||||
|
mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey>
|
||||||
|
roomKey = payload;
|
||||||
|
import_inbound_megolm_session(roomKey);
|
||||||
return;
|
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
|
nlohmann::json
|
||||||
handle_pre_key_olm_message(const std::string &sender,
|
handle_pre_key_olm_message(const std::string &sender,
|
||||||
const std::string &sender_key,
|
const std::string &sender_key,
|
||||||
const mtx::events::msg::OlmCipherContent &content)
|
const mtx::events::msg::OlmCipherContent &content)
|
||||||
@ -129,14 +225,14 @@ handle_pre_key_olm_message(const std::string &sender,
|
|||||||
} catch (const mtx::crypto::olm_exception &e) {
|
} catch (const mtx::crypto::olm_exception &e) {
|
||||||
nhlog::crypto()->critical(
|
nhlog::crypto()->critical(
|
||||||
"failed to create inbound session with {}: {}", sender, e.what());
|
"failed to create inbound session with {}: {}", sender, e.what());
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mtx::crypto::matches_inbound_session_from(
|
if (!mtx::crypto::matches_inbound_session_from(
|
||||||
inbound_session.get(), sender_key, content.body)) {
|
inbound_session.get(), sender_key, content.body)) {
|
||||||
nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})",
|
nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})",
|
||||||
sender);
|
sender);
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
mtx::crypto::BinaryBuf output;
|
mtx::crypto::BinaryBuf output;
|
||||||
@ -146,7 +242,7 @@ handle_pre_key_olm_message(const std::string &sender,
|
|||||||
} catch (const mtx::crypto::olm_exception &e) {
|
} catch (const mtx::crypto::olm_exception &e) {
|
||||||
nhlog::crypto()->critical(
|
nhlog::crypto()->critical(
|
||||||
"failed to decrypt olm message {}: {}", content.body, e.what());
|
"failed to decrypt olm message {}: {}", content.body, e.what());
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
|
auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
|
||||||
@ -159,7 +255,7 @@ handle_pre_key_olm_message(const std::string &sender,
|
|||||||
"failed to save inbound olm session from {}: {}", sender, e.what());
|
"failed to save inbound olm session from {}: {}", sender, e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
create_inbound_megolm_session(sender, sender_key, plaintext);
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
mtx::events::msg::Encrypted
|
mtx::events::msg::Encrypted
|
||||||
@ -169,10 +265,15 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
|
|||||||
|
|
||||||
// relations shouldn't be encrypted...
|
// relations shouldn't be encrypted...
|
||||||
mtx::common::ReplyRelatesTo relation;
|
mtx::common::ReplyRelatesTo relation;
|
||||||
|
mtx::common::RelatesTo r_relation;
|
||||||
|
|
||||||
if (body["content"].contains("m.relates_to") &&
|
if (body["content"].contains("m.relates_to") &&
|
||||||
body["content"]["m.relates_to"].contains("m.in_reply_to")) {
|
body["content"]["m.relates_to"].contains("m.in_reply_to")) {
|
||||||
relation = body["content"]["m.relates_to"];
|
relation = body["content"]["m.relates_to"];
|
||||||
body["content"].erase("m.relates_to");
|
body["content"].erase("m.relates_to");
|
||||||
|
} else if (body["content"]["m.relates_to"].contains("event_id")) {
|
||||||
|
r_relation = body["content"]["m.relates_to"];
|
||||||
|
body["content"].erase("m.relates_to");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always check before for existence.
|
// Always check before for existence.
|
||||||
@ -187,6 +288,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
|
|||||||
data.device_id = device_id;
|
data.device_id = device_id;
|
||||||
data.algorithm = MEGOLM_ALGO;
|
data.algorithm = MEGOLM_ALGO;
|
||||||
data.relates_to = relation;
|
data.relates_to = relation;
|
||||||
|
data.r_relates_to = r_relation;
|
||||||
|
|
||||||
auto message_index = olm_outbound_group_session_message_index(res.session);
|
auto message_index = olm_outbound_group_session_message_index(res.session);
|
||||||
nhlog::crypto()->debug("next message_index {}", message_index);
|
nhlog::crypto()->debug("next message_index {}", message_index);
|
||||||
@ -229,10 +331,12 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return json::parse(std::string((char *)text.data(), text.size()));
|
return json::parse(std::string_view((char *)text.data(), text.size()));
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
nhlog::crypto()->critical("failed to parse the decrypted session msg: {}",
|
nhlog::crypto()->critical(
|
||||||
e.what());
|
"failed to parse the decrypted session msg: {} {}",
|
||||||
|
e.what(),
|
||||||
|
std::string_view((char *)text.data(), text.size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,29 +344,17 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
create_inbound_megolm_session(const std::string &sender,
|
create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
|
||||||
const std::string &sender_key,
|
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;
|
MegolmSessionIndex index;
|
||||||
index.room_id = room_id;
|
index.room_id = roomKey.content.room_id;
|
||||||
index.session_id = session_id;
|
index.session_id = roomKey.content.session_id;
|
||||||
index.sender_key = sender_key;
|
index.sender_key = sender_key;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto megolm_session = olm::client()->init_inbound_group_session(session_key);
|
auto megolm_session =
|
||||||
|
olm::client()->init_inbound_group_session(roomKey.content.session_key);
|
||||||
cache::saveInboundMegolmSession(index, std::move(megolm_session));
|
cache::saveInboundMegolmSession(index, std::move(megolm_session));
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
|
nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
|
||||||
@ -272,7 +364,34 @@ create_inbound_megolm_session(const std::string &sender,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender);
|
nhlog::crypto()->info(
|
||||||
|
"established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
import_inbound_megolm_session(
|
||||||
|
const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey)
|
||||||
|
{
|
||||||
|
MegolmSessionIndex index;
|
||||||
|
index.room_id = roomKey.content.room_id;
|
||||||
|
index.session_id = roomKey.content.session_id;
|
||||||
|
index.sender_key = roomKey.content.sender_key;
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto megolm_session =
|
||||||
|
olm::client()->import_inbound_group_session(roomKey.content.session_key);
|
||||||
|
cache::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 mtx::crypto::olm_exception &e) {
|
||||||
|
nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(Nico): Reload messages encrypted with this key.
|
||||||
|
nhlog::crypto()->info(
|
||||||
|
"established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -373,6 +492,10 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR
|
|||||||
if (!cache::outboundMegolmSessionExists(req.content.room_id)) {
|
if (!cache::outboundMegolmSessionExists(req.content.room_id)) {
|
||||||
nhlog::crypto()->warn("requested session not found in room: {}",
|
nhlog::crypto()->warn("requested session not found in room: {}",
|
||||||
req.content.room_id);
|
req.content.room_id);
|
||||||
|
|
||||||
|
nhlog::crypto()->warn("requested session not found in room: {}",
|
||||||
|
req.content.room_id);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,9 +600,11 @@ send_megolm_key_to_device(const std::string &user_id,
|
|||||||
->create_room_key_event(UserId(user_id), pks.ed25519, payload)
|
->create_room_key_event(UserId(user_id), pks.ed25519, payload)
|
||||||
.dump();
|
.dump();
|
||||||
|
|
||||||
|
mtx::requests::ClaimKeys claim_keys;
|
||||||
|
claim_keys.one_time_keys[user_id][device_id] = mtx::crypto::SIGNED_CURVE25519;
|
||||||
|
|
||||||
http::client()->claim_keys(
|
http::client()->claim_keys(
|
||||||
user_id,
|
claim_keys,
|
||||||
{device_id},
|
|
||||||
[room_key, user_id, device_id, pks](const mtx::responses::ClaimKeys &res,
|
[room_key, user_id, device_id, pks](const mtx::responses::ClaimKeys &res,
|
||||||
mtx::http::RequestErr err) {
|
mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
10
src/Olm.h
10
src/Olm.h
@ -71,11 +71,13 @@ handle_olm_message(const OlmMessage &msg);
|
|||||||
|
|
||||||
//! Establish a new inbound megolm session with the decrypted payload from olm.
|
//! Establish a new inbound megolm session with the decrypted payload from olm.
|
||||||
void
|
void
|
||||||
create_inbound_megolm_session(const std::string &sender,
|
create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
|
||||||
const std::string &sender_key,
|
const std::string &sender_key);
|
||||||
const nlohmann::json &payload);
|
|
||||||
|
|
||||||
void
|
void
|
||||||
|
import_inbound_megolm_session(
|
||||||
|
const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey);
|
||||||
|
|
||||||
|
nlohmann::json
|
||||||
handle_pre_key_olm_message(const std::string &sender,
|
handle_pre_key_olm_message(const std::string &sender,
|
||||||
const std::string &sender_key,
|
const std::string &sender_key,
|
||||||
const mtx::events::msg::OlmCipherContent &content);
|
const mtx::events::msg::OlmCipherContent &content);
|
||||||
|
@ -709,6 +709,8 @@ TextInputWidget::command(QString command, QString args)
|
|||||||
emit sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\");
|
emit sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\");
|
||||||
} else if (command == "clear-timeline") {
|
} else if (command == "clear-timeline") {
|
||||||
emit clearRoomTimeline();
|
emit clearRoomTimeline();
|
||||||
|
} else if (command == "rotate-megolm-session") {
|
||||||
|
emit rotateMegolmSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +186,7 @@ signals:
|
|||||||
void sendBanRoomRequest(const QString &userid, const QString &reason);
|
void sendBanRoomRequest(const QString &userid, const QString &reason);
|
||||||
void sendUnbanRoomRequest(const QString &userid, const QString &reason);
|
void sendUnbanRoomRequest(const QString &userid, const QString &reason);
|
||||||
void changeRoomNick(const QString &displayname);
|
void changeRoomNick(const QString &displayname);
|
||||||
|
void rotateMegolmSession();
|
||||||
|
|
||||||
void startedTyping();
|
void startedTyping();
|
||||||
void stoppedTyping();
|
void stoppedTyping();
|
||||||
|
@ -418,7 +418,6 @@ getPayloadType(const std::string &sdp, const std::string &name)
|
|||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -131,5 +131,4 @@ AcceptCall::AcceptCall(const QString &caller,
|
|||||||
emit close();
|
emit close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -33,5 +33,4 @@ private:
|
|||||||
QPushButton *rejectBtn_;
|
QPushButton *rejectBtn_;
|
||||||
std::vector<std::string> audioDevices_;
|
std::vector<std::string> audioDevices_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -100,5 +100,4 @@ PlaceCall::PlaceCall(const QString &callee,
|
|||||||
emit close();
|
emit close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -33,5 +33,4 @@ private:
|
|||||||
QPushButton *cancelBtn_;
|
QPushButton *cancelBtn_;
|
||||||
std::vector<std::string> audioDevices_;
|
std::vector<std::string> audioDevices_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -60,5 +60,4 @@ private:
|
|||||||
EmojiCategory category_ = EmojiCategory::Search;
|
EmojiCategory category_ = EmojiCategory::Search;
|
||||||
emoji::Provider emoji_provider_;
|
emoji::Provider emoji_provider_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
@ -33,5 +33,4 @@ private:
|
|||||||
return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower();
|
return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "Cache_p.h"
|
#include "Cache_p.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
#include "EventAccessors.h"
|
#include "EventAccessors.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
@ -53,9 +54,8 @@ EventStore::EventStore(std::string room_id, QObject *)
|
|||||||
&EventStore::oldMessagesRetrieved,
|
&EventStore::oldMessagesRetrieved,
|
||||||
this,
|
this,
|
||||||
[this](const mtx::responses::Messages &res) {
|
[this](const mtx::responses::Messages &res) {
|
||||||
//
|
|
||||||
uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
|
uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
|
||||||
if (newFirst == first && !res.chunk.empty())
|
if (newFirst == first)
|
||||||
fetchMore();
|
fetchMore();
|
||||||
else {
|
else {
|
||||||
emit beginInsertRows(toExternalIdx(newFirst),
|
emit beginInsertRows(toExternalIdx(newFirst),
|
||||||
@ -97,7 +97,7 @@ EventStore::EventStore(std::string room_id, QObject *)
|
|||||||
room_id_,
|
room_id_,
|
||||||
txn_id,
|
txn_id,
|
||||||
e.content,
|
e.content,
|
||||||
[this, txn_id](const mtx::responses::EventId &event_id,
|
[this, txn_id, e](const mtx::responses::EventId &event_id,
|
||||||
mtx::http::RequestErr err) {
|
mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
const int status_code =
|
const int status_code =
|
||||||
@ -110,7 +110,21 @@ EventStore::EventStore(std::string room_id, QObject *)
|
|||||||
emit messageFailed(txn_id);
|
emit messageFailed(txn_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit messageSent(txn_id, event_id.event_id.to_string());
|
emit messageSent(txn_id, event_id.event_id.to_string());
|
||||||
|
if constexpr (std::is_same_v<
|
||||||
|
decltype(e.content),
|
||||||
|
mtx::events::msg::Encrypted>) {
|
||||||
|
auto event =
|
||||||
|
decryptEvent({room_id_, e.event_id}, e);
|
||||||
|
if (auto dec =
|
||||||
|
std::get_if<mtx::events::RoomEvent<
|
||||||
|
mtx::events::msg::
|
||||||
|
KeyVerificationRequest>>(event)) {
|
||||||
|
emit updateFlowEventId(
|
||||||
|
event_id.event_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
event->data);
|
event->data);
|
||||||
@ -265,7 +279,76 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
|
|||||||
emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
|
emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decrypting and checking some encrypted messages
|
||||||
|
if (auto encrypted =
|
||||||
|
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
|
&event)) {
|
||||||
|
mtx::events::collections::TimelineEvents *d_event =
|
||||||
|
decryptEvent({room_id_, encrypted->event_id}, *encrypted);
|
||||||
|
if (std::visit(
|
||||||
|
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
|
||||||
|
*d_event)) {
|
||||||
|
handle_room_verification(*d_event);
|
||||||
}
|
}
|
||||||
|
// else {
|
||||||
|
// // only the key.verification.ready sent by localuser's other
|
||||||
|
// device
|
||||||
|
// // is of significance as it is used for detecting accepted request
|
||||||
|
// if (std::get_if<mtx::events::RoomEvent<
|
||||||
|
// mtx::events::msg::KeyVerificationReady>>(d_event)) {
|
||||||
|
// auto msg = std::get_if<mtx::events::RoomEvent<
|
||||||
|
// mtx::events::msg::KeyVerificationReady>>(d_event);
|
||||||
|
// ChatPage::instance()->receivedDeviceVerificationReady(
|
||||||
|
// msg->content);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
template<class... Ts>
|
||||||
|
struct overloaded : Ts...
|
||||||
|
{
|
||||||
|
using Ts::operator()...;
|
||||||
|
};
|
||||||
|
template<class... Ts>
|
||||||
|
overloaded(Ts...) -> overloaded<Ts...>;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
EventStore::handle_room_verification(mtx::events::collections::TimelineEvents event)
|
||||||
|
{
|
||||||
|
std::visit(
|
||||||
|
overloaded{
|
||||||
|
[this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) {
|
||||||
|
emit startDMVerification(msg);
|
||||||
|
},
|
||||||
|
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationCancel(msg.content);
|
||||||
|
},
|
||||||
|
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationAccept(msg.content);
|
||||||
|
},
|
||||||
|
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationKey(msg.content);
|
||||||
|
},
|
||||||
|
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationMac(msg.content);
|
||||||
|
},
|
||||||
|
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationReady(msg.content);
|
||||||
|
},
|
||||||
|
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationDone(msg.content);
|
||||||
|
},
|
||||||
|
[](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) {
|
||||||
|
ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender);
|
||||||
|
},
|
||||||
|
[](const auto &) {},
|
||||||
|
},
|
||||||
|
event);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList
|
QVariantList
|
||||||
@ -289,13 +372,14 @@ EventStore::reactions(const std::string &event_id)
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
|
if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
|
||||||
related_event)) {
|
related_event);
|
||||||
auto &agg = aggregation[reaction->content.relates_to.key];
|
reaction && reaction->content.relates_to.key) {
|
||||||
|
auto &agg = aggregation[reaction->content.relates_to.key.value()];
|
||||||
|
|
||||||
if (agg.count == 0) {
|
if (agg.count == 0) {
|
||||||
Reaction temp{};
|
Reaction temp{};
|
||||||
temp.key_ =
|
temp.key_ =
|
||||||
QString::fromStdString(reaction->content.relates_to.key);
|
QString::fromStdString(reaction->content.relates_to.key.value());
|
||||||
reactions.push_back(temp);
|
reactions.push_back(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,11 +491,12 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||||||
|
|
||||||
auto decryptionResult = olm::decryptEvent(index, e);
|
auto decryptionResult = olm::decryptEvent(index, e);
|
||||||
|
|
||||||
if (decryptionResult.error) {
|
|
||||||
mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
|
mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
|
||||||
dummy.origin_server_ts = e.origin_server_ts;
|
dummy.origin_server_ts = e.origin_server_ts;
|
||||||
dummy.event_id = e.event_id;
|
dummy.event_id = e.event_id;
|
||||||
dummy.sender = e.sender;
|
dummy.sender = e.sender;
|
||||||
|
|
||||||
|
if (decryptionResult.error) {
|
||||||
switch (*decryptionResult.error) {
|
switch (*decryptionResult.error) {
|
||||||
case olm::DecryptionErrorCode::MissingSession:
|
case olm::DecryptionErrorCode::MissingSession:
|
||||||
dummy.content.body =
|
dummy.content.body =
|
||||||
@ -484,6 +569,66 @@ EventStore::decryptEvent(const IdIndex &idx,
|
|||||||
return asCacheEntry(std::move(dummy));
|
return asCacheEntry(std::move(dummy));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string msg_str;
|
||||||
|
try {
|
||||||
|
auto session = cache::client()->getInboundMegolmSession(index);
|
||||||
|
auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext);
|
||||||
|
msg_str = std::string((char *)res.data.data(), res.data.size());
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
|
||||||
|
index.room_id,
|
||||||
|
index.session_id,
|
||||||
|
index.sender_key,
|
||||||
|
e.what());
|
||||||
|
dummy.content.body =
|
||||||
|
tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
|
||||||
|
"Placeholder, when the message can't be decrypted, because the DB "
|
||||||
|
"access "
|
||||||
|
"failed.")
|
||||||
|
.toStdString();
|
||||||
|
return asCacheEntry(std::move(dummy));
|
||||||
|
} catch (const mtx::crypto::olm_exception &e) {
|
||||||
|
nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
|
||||||
|
index.room_id,
|
||||||
|
index.session_id,
|
||||||
|
index.sender_key,
|
||||||
|
e.what());
|
||||||
|
dummy.content.body =
|
||||||
|
tr("-- Decryption Error (%1) --",
|
||||||
|
"Placeholder, when the message can't be decrypted. In this case, the "
|
||||||
|
"Olm "
|
||||||
|
"decrytion returned an error, which is passed as %1.")
|
||||||
|
.arg(e.what())
|
||||||
|
.toStdString();
|
||||||
|
return asCacheEntry(std::move(dummy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// relations are unencrypted in content...
|
||||||
|
if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
|
||||||
|
body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
|
||||||
|
|
||||||
|
json event_array = json::array();
|
||||||
|
event_array.push_back(body);
|
||||||
|
|
||||||
|
std::vector<mtx::events::collections::TimelineEvents> temp_events;
|
||||||
|
mtx::responses::utils::parse_timeline_events(event_array, temp_events);
|
||||||
|
|
||||||
|
if (temp_events.size() == 1) {
|
||||||
|
auto encInfo = mtx::accessors::file(temp_events[0]);
|
||||||
|
|
||||||
|
if (encInfo)
|
||||||
|
emit newEncryptedImage(encInfo.value());
|
||||||
|
|
||||||
|
return asCacheEntry(std::move(temp_events[0]));
|
||||||
|
}
|
||||||
|
|
||||||
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
|
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
|
||||||
if (encInfo)
|
if (encInfo)
|
||||||
emit newEncryptedImage(encInfo.value());
|
emit newEncryptedImage(encInfo.value());
|
||||||
@ -515,7 +660,8 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt)
|
|||||||
mtx::http::RequestErr err) {
|
mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
nhlog::net()->error(
|
nhlog::net()->error(
|
||||||
"Failed to retrieve event with id {}, which was "
|
"Failed to retrieve event with id {}, which "
|
||||||
|
"was "
|
||||||
"requested to show the replyTo for event {}",
|
"requested to show the replyTo for event {}",
|
||||||
relatedTo,
|
relatedTo,
|
||||||
id);
|
id);
|
||||||
|
@ -98,6 +98,9 @@ signals:
|
|||||||
void processPending();
|
void processPending();
|
||||||
void messageSent(std::string txn_id, std::string event_id);
|
void messageSent(std::string txn_id, std::string event_id);
|
||||||
void messageFailed(std::string txn_id);
|
void messageFailed(std::string txn_id);
|
||||||
|
void startDMVerification(
|
||||||
|
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg);
|
||||||
|
void updateFlowEventId(std::string event_id);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void addPending(mtx::events::collections::TimelineEvents event);
|
void addPending(mtx::events::collections::TimelineEvents event);
|
||||||
@ -107,6 +110,7 @@ private:
|
|||||||
mtx::events::collections::TimelineEvents *decryptEvent(
|
mtx::events::collections::TimelineEvents *decryptEvent(
|
||||||
const IdIndex &idx,
|
const IdIndex &idx,
|
||||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
|
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
|
||||||
|
void handle_room_verification(mtx::events::collections::TimelineEvents event);
|
||||||
|
|
||||||
std::string room_id_;
|
std::string room_id_;
|
||||||
|
|
||||||
|
@ -116,7 +116,46 @@ struct RoomEventType
|
|||||||
{
|
{
|
||||||
return qml_mtx_events::EventType::VideoMessage;
|
return qml_mtx_events::EventType::VideoMessage;
|
||||||
}
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationRequest;
|
||||||
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationStart;
|
||||||
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationMac;
|
||||||
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationAccept;
|
||||||
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationReady;
|
||||||
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationCancel;
|
||||||
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationKey;
|
||||||
|
}
|
||||||
|
qml_mtx_events::EventType operator()(
|
||||||
|
const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &)
|
||||||
|
{
|
||||||
|
return qml_mtx_events::EventType::KeyVerificationDone;
|
||||||
|
}
|
||||||
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
|
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
|
||||||
{
|
{
|
||||||
return qml_mtx_events::EventType::Redacted;
|
return qml_mtx_events::EventType::Redacted;
|
||||||
@ -211,6 +250,15 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
|
|||||||
connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
|
connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
|
||||||
connect(
|
connect(
|
||||||
&events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
|
&events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
|
||||||
|
connect(&events,
|
||||||
|
&EventStore::startDMVerification,
|
||||||
|
this,
|
||||||
|
[this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) {
|
||||||
|
ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this);
|
||||||
|
});
|
||||||
|
connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
|
||||||
|
this->updateFlowEventId(event_id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray>
|
QHash<int, QByteArray>
|
||||||
@ -743,9 +791,9 @@ TimelineModel::viewDecryptedRawMessage(QString id) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineModel::openUserProfile(QString userid) const
|
TimelineModel::openUserProfile(QString userid)
|
||||||
{
|
{
|
||||||
MainWindow::instance()->openUserProfile(userid, room_id_);
|
emit openProfile(new UserProfile(room_id_, userid, manager_, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -846,18 +894,18 @@ TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
void
|
void
|
||||||
TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType)
|
||||||
nlohmann::json content,
|
|
||||||
mtx::events::EventType eventType)
|
|
||||||
{
|
{
|
||||||
const auto room_id = room_id_.toStdString();
|
const auto room_id = room_id_.toStdString();
|
||||||
|
|
||||||
using namespace mtx::events;
|
using namespace mtx::events;
|
||||||
using namespace mtx::identifiers;
|
using namespace mtx::identifiers;
|
||||||
|
|
||||||
json doc = {
|
json doc = {{"type", mtx::events::to_string(eventType)},
|
||||||
{"type", mtx::events::to_string(eventType)}, {"content", content}, {"room_id", room_id}};
|
{"content", json(msg.content)},
|
||||||
|
{"room_id", room_id}};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if we have already an outbound megolm session then we can use.
|
// Check if we have already an outbound megolm session then we can use.
|
||||||
@ -865,7 +913,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
|
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
|
||||||
event.content =
|
event.content =
|
||||||
olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
|
olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
|
||||||
event.event_id = txn_id;
|
event.event_id = msg.event_id;
|
||||||
event.room_id = room_id;
|
event.room_id = room_id;
|
||||||
event.sender = http::client()->user_id().to_string();
|
event.sender = http::client()->user_id().to_string();
|
||||||
event.type = mtx::events::EventType::RoomEncrypted;
|
event.type = mtx::events::EventType::RoomEncrypted;
|
||||||
@ -893,14 +941,25 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
OutboundGroupSessionData session_data;
|
OutboundGroupSessionData session_data;
|
||||||
session_data.session_id = session_id;
|
session_data.session_id = session_id;
|
||||||
session_data.session_key = session_key;
|
session_data.session_key = session_key;
|
||||||
session_data.message_index = 0; // TODO Update me
|
session_data.message_index = 0;
|
||||||
cache::saveOutboundMegolmSession(
|
cache::saveOutboundMegolmSession(
|
||||||
room_id, session_data, std::move(outbound_session));
|
room_id, session_data, std::move(outbound_session));
|
||||||
|
|
||||||
|
{
|
||||||
|
MegolmSessionIndex index;
|
||||||
|
index.room_id = room_id;
|
||||||
|
index.session_id = session_id;
|
||||||
|
index.sender_key = olm::client()->identity_keys().curve25519;
|
||||||
|
auto megolm_session =
|
||||||
|
olm::client()->init_inbound_group_session(session_key);
|
||||||
|
cache::saveInboundMegolmSession(index, std::move(megolm_session));
|
||||||
|
}
|
||||||
|
|
||||||
const auto members = cache::roomMembers(room_id);
|
const auto members = cache::roomMembers(room_id);
|
||||||
nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
|
nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
|
||||||
|
|
||||||
auto keeper = std::make_shared<StateKeeper>([room_id, doc, txn_id, this]() {
|
auto keeper =
|
||||||
|
std::make_shared<StateKeeper>([room_id, doc, txn_id = msg.event_id, this]() {
|
||||||
try {
|
try {
|
||||||
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
|
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
|
||||||
event.content = olm::encrypt_group_message(
|
event.content = olm::encrypt_group_message(
|
||||||
@ -913,8 +972,8 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
|
|
||||||
emit this->addPendingMessageToStore(event);
|
emit this->addPendingMessageToStore(event);
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
nhlog::db()->critical("failed to save megolm outbound session: {}",
|
nhlog::db()->critical(
|
||||||
e.what());
|
"failed to save megolm outbound session: {}", e.what());
|
||||||
emit ChatPage::instance()->showNotification(
|
emit ChatPage::instance()->showNotification(
|
||||||
tr("Failed to encrypt event, sending aborted!"));
|
tr("Failed to encrypt event, sending aborted!"));
|
||||||
}
|
}
|
||||||
@ -926,7 +985,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
|
|
||||||
http::client()->query_keys(
|
http::client()->query_keys(
|
||||||
req,
|
req,
|
||||||
[keeper = std::move(keeper), megolm_payload, txn_id, this](
|
[keeper = std::move(keeper), megolm_payload, txn_id = msg.event_id, this](
|
||||||
const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
|
const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
nhlog::net()->warn("failed to query device keys: {} {}",
|
nhlog::net()->warn("failed to query device keys: {} {}",
|
||||||
@ -937,19 +996,23 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &user : res.device_keys) {
|
mtx::requests::ClaimKeys claim_keys;
|
||||||
// Mapping from a device_id with valid identity keys to the
|
|
||||||
|
// Mapping from user id to a device_id with valid identity keys to the
|
||||||
// generated room_key event used for sharing the megolm session.
|
// generated room_key event used for sharing the megolm session.
|
||||||
std::map<std::string, std::string> room_key_msgs;
|
std::map<std::string, std::map<std::string, std::string>> room_key_msgs;
|
||||||
std::map<std::string, DevicePublicKeys> deviceKeys;
|
std::map<std::string, std::map<std::string, DevicePublicKeys>> deviceKeys;
|
||||||
|
|
||||||
room_key_msgs.clear();
|
|
||||||
deviceKeys.clear();
|
|
||||||
|
|
||||||
|
for (const auto &user : res.device_keys) {
|
||||||
for (const auto &dev : user.second) {
|
for (const auto &dev : user.second) {
|
||||||
const auto user_id = ::UserId(dev.second.user_id);
|
const auto user_id = ::UserId(dev.second.user_id);
|
||||||
const auto device_id = DeviceId(dev.second.device_id);
|
const auto device_id = DeviceId(dev.second.device_id);
|
||||||
|
|
||||||
|
if (user_id.get() ==
|
||||||
|
http::client()->user_id().to_string() &&
|
||||||
|
device_id.get() == http::client()->device_id())
|
||||||
|
continue;
|
||||||
|
|
||||||
const auto device_keys = dev.second.keys;
|
const auto device_keys = dev.second.keys;
|
||||||
const auto curveKey = "curve25519:" + device_id.get();
|
const auto curveKey = "curve25519:" + device_id.get();
|
||||||
const auto edKey = "ed25519:" + device_id.get();
|
const auto edKey = "ed25519:" + device_id.get();
|
||||||
@ -968,7 +1031,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!mtx::crypto::verify_identity_signature(
|
if (!mtx::crypto::verify_identity_signature(
|
||||||
json(dev.second), device_id, user_id)) {
|
dev.second, device_id, user_id)) {
|
||||||
nhlog::crypto()->warn(
|
nhlog::crypto()->warn(
|
||||||
"failed to verify identity keys: {}",
|
"failed to verify identity keys: {}",
|
||||||
json(dev.second).dump(2));
|
json(dev.second).dump(2));
|
||||||
@ -991,42 +1054,25 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
user_id, pks.ed25519, megolm_payload)
|
user_id, pks.ed25519, megolm_payload)
|
||||||
.dump();
|
.dump();
|
||||||
|
|
||||||
room_key_msgs.emplace(device_id, room_key);
|
room_key_msgs[user_id].emplace(device_id, room_key);
|
||||||
deviceKeys.emplace(device_id, pks);
|
deviceKeys[user_id].emplace(device_id, pks);
|
||||||
|
claim_keys.one_time_keys[user.first][device_id] =
|
||||||
|
mtx::crypto::SIGNED_CURVE25519;
|
||||||
|
|
||||||
|
nhlog::net()->info("{}", device_id.get());
|
||||||
|
nhlog::net()->info(" curve25519 {}", pks.curve25519);
|
||||||
|
nhlog::net()->info(" ed25519 {}", pks.ed25519);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> valid_devices;
|
http::client()->claim_keys(claim_keys,
|
||||||
valid_devices.reserve(room_key_msgs.size());
|
|
||||||
for (auto const &d : room_key_msgs) {
|
|
||||||
valid_devices.push_back(d.first);
|
|
||||||
|
|
||||||
nhlog::net()->info("{}", d.first);
|
|
||||||
nhlog::net()->info(" curve25519 {}",
|
|
||||||
deviceKeys.at(d.first).curve25519);
|
|
||||||
nhlog::net()->info(" ed25519 {}",
|
|
||||||
deviceKeys.at(d.first).ed25519);
|
|
||||||
}
|
|
||||||
|
|
||||||
nhlog::net()->info(
|
|
||||||
"sending claim request for user {} with {} devices",
|
|
||||||
user.first,
|
|
||||||
valid_devices.size());
|
|
||||||
|
|
||||||
http::client()->claim_keys(
|
|
||||||
user.first,
|
|
||||||
valid_devices,
|
|
||||||
std::bind(&TimelineModel::handleClaimedKeys,
|
std::bind(&TimelineModel::handleClaimedKeys,
|
||||||
this,
|
this,
|
||||||
keeper,
|
keeper,
|
||||||
room_key_msgs,
|
room_key_msgs,
|
||||||
deviceKeys,
|
deviceKeys,
|
||||||
user.first,
|
|
||||||
std::placeholders::_1,
|
std::placeholders::_1,
|
||||||
std::placeholders::_2));
|
std::placeholders::_2));
|
||||||
|
|
||||||
// TODO: Wait before sending the next batch of requests.
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Let the user know about the errors.
|
// TODO: Let the user know about the errors.
|
||||||
@ -1044,10 +1090,10 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
|
TimelineModel::handleClaimedKeys(
|
||||||
const std::map<std::string, std::string> &room_keys,
|
std::shared_ptr<StateKeeper> keeper,
|
||||||
const std::map<std::string, DevicePublicKeys> &pks,
|
const std::map<std::string, std::map<std::string, std::string>> &room_keys,
|
||||||
const std::string &user_id,
|
const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks,
|
||||||
const mtx::responses::ClaimKeys &res,
|
const mtx::responses::ClaimKeys &res,
|
||||||
mtx::http::RequestErr err)
|
mtx::http::RequestErr err)
|
||||||
{
|
{
|
||||||
@ -1059,65 +1105,59 @@ TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nhlog::net()->debug("claimed keys for {}", user_id);
|
|
||||||
|
|
||||||
if (res.one_time_keys.size() == 0) {
|
|
||||||
nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
|
|
||||||
nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto retrieved_devices = res.one_time_keys.at(user_id);
|
|
||||||
|
|
||||||
// Payload with all the to_device message to be sent.
|
// Payload with all the to_device message to be sent.
|
||||||
json body;
|
nlohmann::json body;
|
||||||
body["messages"][user_id] = json::object();
|
|
||||||
|
for (const auto &[user_id, retrieved_devices] : res.one_time_keys) {
|
||||||
|
nhlog::net()->debug("claimed keys for {}", user_id);
|
||||||
|
if (retrieved_devices.size() == 0) {
|
||||||
|
nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto &rd : retrieved_devices) {
|
for (const auto &rd : retrieved_devices) {
|
||||||
const auto device_id = rd.first;
|
const auto device_id = rd.first;
|
||||||
|
|
||||||
nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
|
nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
|
||||||
|
|
||||||
|
if (rd.second.empty() || !rd.second.begin()->contains("key")) {
|
||||||
|
nhlog::net()->warn("Skipping device {} as it has no key.",
|
||||||
|
device_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Verify signatures
|
// TODO: Verify signatures
|
||||||
auto otk = rd.second.begin()->at("key");
|
auto otk = rd.second.begin()->at("key");
|
||||||
|
|
||||||
if (pks.find(device_id) == pks.end()) {
|
auto id_key = pks.at(user_id).at(device_id).curve25519;
|
||||||
nhlog::net()->critical("couldn't find public key for device: {}",
|
|
||||||
device_id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto id_key = pks.at(device_id).curve25519;
|
|
||||||
auto s = olm::client()->create_outbound_session(id_key, otk);
|
auto s = olm::client()->create_outbound_session(id_key, otk);
|
||||||
|
|
||||||
if (room_keys.find(device_id) == room_keys.end()) {
|
|
||||||
nhlog::net()->critical("couldn't find m.room_key for device: {}",
|
|
||||||
device_id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto device_msg = olm::client()->create_olm_encrypted_content(
|
auto device_msg = olm::client()->create_olm_encrypted_content(
|
||||||
s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
|
s.get(),
|
||||||
|
room_keys.at(user_id).at(device_id),
|
||||||
|
pks.at(user_id).at(device_id).curve25519);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cache::saveOlmSession(id_key, std::move(s));
|
cache::saveOlmSession(id_key, std::move(s));
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
|
nhlog::db()->critical("failed to save outbound olm session: {}",
|
||||||
} catch (const mtx::crypto::olm_exception &e) {
|
|
||||||
nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
|
|
||||||
e.what());
|
e.what());
|
||||||
|
} catch (const mtx::crypto::olm_exception &e) {
|
||||||
|
nhlog::crypto()->critical(
|
||||||
|
"failed to pickle outbound olm session: {}", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
body["messages"][user_id][device_id] = device_msg;
|
body["messages"][user_id][device_id] = device_msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
nhlog::net()->info("send_to_device: {}", user_id);
|
nhlog::net()->info("send_to_device: {}", user_id);
|
||||||
|
}
|
||||||
|
|
||||||
http::client()->send_to_device(
|
http::client()->send_to_device(
|
||||||
"m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
|
mtx::events::to_string(mtx::events::EventType::RoomEncrypted),
|
||||||
|
http::client()->generate_txn_id(),
|
||||||
|
body,
|
||||||
|
[keeper](mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
nhlog::net()->warn("failed to send "
|
nhlog::net()->warn("failed to send "
|
||||||
"send_to_device "
|
"send_to_device "
|
||||||
@ -1143,8 +1183,7 @@ struct SendMessageVisitor
|
|||||||
if (encInfo)
|
if (encInfo)
|
||||||
emit model_->newEncryptedImage(encInfo.value());
|
emit model_->newEncryptedImage(encInfo.value());
|
||||||
|
|
||||||
model_->sendEncryptedMessageEvent(
|
model_->sendEncryptedMessage(msg, Event);
|
||||||
msg.event_id, nlohmann::json(msg.content), Event);
|
|
||||||
} else {
|
} else {
|
||||||
msg.type = Event;
|
msg.type = Event;
|
||||||
emit model_->addPendingMessageToStore(msg);
|
emit model_->addPendingMessageToStore(msg);
|
||||||
@ -1199,6 +1238,54 @@ struct SendMessageVisitor
|
|||||||
event);
|
event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
|
||||||
|
mtx::events::EventType::RoomMessage>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationReady,
|
||||||
|
mtx::events::EventType::KeyVerificationReady>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationStart,
|
||||||
|
mtx::events::EventType::KeyVerificationStart>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationAccept,
|
||||||
|
mtx::events::EventType::KeyVerificationAccept>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationMac,
|
||||||
|
mtx::events::EventType::KeyVerificationMac>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationKey,
|
||||||
|
mtx::events::EventType::KeyVerificationKey>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationDone,
|
||||||
|
mtx::events::EventType::KeyVerificationDone>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg)
|
||||||
|
{
|
||||||
|
sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
|
||||||
|
mtx::events::EventType::KeyVerificationCancel>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
TimelineModel *model_;
|
TimelineModel *model_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "CacheCryptoStructs.h"
|
#include "CacheCryptoStructs.h"
|
||||||
#include "EventStore.h"
|
#include "EventStore.h"
|
||||||
|
#include "ui/UserProfile.h"
|
||||||
|
|
||||||
namespace mtx::http {
|
namespace mtx::http {
|
||||||
using RequestErr = const std::optional<mtx::http::ClientError> &;
|
using RequestErr = const std::optional<mtx::http::ClientError> &;
|
||||||
@ -87,6 +88,14 @@ enum EventType
|
|||||||
VideoMessage,
|
VideoMessage,
|
||||||
Redacted,
|
Redacted,
|
||||||
UnknownMessage,
|
UnknownMessage,
|
||||||
|
KeyVerificationRequest,
|
||||||
|
KeyVerificationStart,
|
||||||
|
KeyVerificationMac,
|
||||||
|
KeyVerificationAccept,
|
||||||
|
KeyVerificationCancel,
|
||||||
|
KeyVerificationKey,
|
||||||
|
KeyVerificationDone,
|
||||||
|
KeyVerificationReady
|
||||||
};
|
};
|
||||||
Q_ENUM_NS(EventType)
|
Q_ENUM_NS(EventType)
|
||||||
|
|
||||||
@ -199,7 +208,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void viewRawMessage(QString id) const;
|
Q_INVOKABLE void viewRawMessage(QString id) const;
|
||||||
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
|
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
|
||||||
Q_INVOKABLE void openUserProfile(QString userid) const;
|
Q_INVOKABLE void openUserProfile(QString userid);
|
||||||
Q_INVOKABLE void replyAction(QString id);
|
Q_INVOKABLE void replyAction(QString id);
|
||||||
Q_INVOKABLE void readReceiptsAction(QString id) const;
|
Q_INVOKABLE void readReceiptsAction(QString id) const;
|
||||||
Q_INVOKABLE void redactEvent(QString id);
|
Q_INVOKABLE void redactEvent(QString id);
|
||||||
@ -275,21 +284,23 @@ signals:
|
|||||||
void paginationInProgressChanged(const bool);
|
void paginationInProgressChanged(const bool);
|
||||||
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
||||||
|
|
||||||
|
void openProfile(UserProfile *profile);
|
||||||
|
|
||||||
void newMessageToSend(mtx::events::collections::TimelineEvents event);
|
void newMessageToSend(mtx::events::collections::TimelineEvents event);
|
||||||
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
|
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
|
||||||
|
void updateFlowEventId(std::string event_id);
|
||||||
|
|
||||||
void roomNameChanged();
|
void roomNameChanged();
|
||||||
void roomTopicChanged();
|
void roomTopicChanged();
|
||||||
void roomAvatarUrlChanged();
|
void roomAvatarUrlChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void sendEncryptedMessageEvent(const std::string &txn_id,
|
template<typename T>
|
||||||
nlohmann::json content,
|
void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType);
|
||||||
mtx::events::EventType);
|
void handleClaimedKeys(
|
||||||
void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
|
std::shared_ptr<StateKeeper> keeper,
|
||||||
const std::map<std::string, std::string> &room_key,
|
const std::map<std::string, std::map<std::string, std::string>> &room_keys,
|
||||||
const std::map<std::string, DevicePublicKeys> &pks,
|
const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks,
|
||||||
const std::string &user_id,
|
|
||||||
const mtx::responses::ClaimKeys &res,
|
const mtx::responses::ClaimKeys &res,
|
||||||
mtx::http::RequestErr err);
|
mtx::http::RequestErr err);
|
||||||
void readEvent(const std::string &id);
|
void readEvent(const std::string &id);
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#include <QMetaType>
|
#include <QMetaType>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
|
#include <QQmlEngine>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "BlurhashProvider.h"
|
#include "BlurhashProvider.h"
|
||||||
@ -19,7 +20,12 @@
|
|||||||
#include "emoji/EmojiModel.h"
|
#include "emoji/EmojiModel.h"
|
||||||
#include "emoji/Provider.h"
|
#include "emoji/Provider.h"
|
||||||
|
|
||||||
|
#include <iostream> //only for debugging
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
|
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
|
||||||
|
Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
|
||||||
|
|
||||||
|
namespace msgs = mtx::events::msg;
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineViewManager::updateEncryptedDescriptions()
|
TimelineViewManager::updateEncryptedDescriptions()
|
||||||
@ -65,9 +71,13 @@ TimelineViewManager::userColor(QString id, QColor background)
|
|||||||
QString
|
QString
|
||||||
TimelineViewManager::userPresence(QString id) const
|
TimelineViewManager::userPresence(QString id) const
|
||||||
{
|
{
|
||||||
|
if (id.isEmpty())
|
||||||
|
return "";
|
||||||
|
else
|
||||||
return QString::fromStdString(
|
return QString::fromStdString(
|
||||||
mtx::presence::to_string(cache::presenceState(id.toStdString())));
|
mtx::presence::to_string(cache::presenceState(id.toStdString())));
|
||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
TimelineViewManager::userStatus(QString id) const
|
TimelineViewManager::userStatus(QString id) const
|
||||||
{
|
{
|
||||||
@ -83,15 +93,52 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
|||||||
, callManager_(callManager)
|
, callManager_(callManager)
|
||||||
, settings(userSettings)
|
, settings(userSettings)
|
||||||
{
|
{
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
|
||||||
|
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
|
||||||
|
|
||||||
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
|
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
|
||||||
"im.nheko",
|
"im.nheko",
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
"MtxEvent",
|
"MtxEvent",
|
||||||
"Can't instantiate enum!");
|
"Can't instantiate enum!");
|
||||||
|
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
|
||||||
|
"im.nheko",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"VerificationStatus",
|
||||||
|
"Can't instantiate enum!");
|
||||||
|
|
||||||
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
|
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
|
||||||
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
|
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
|
||||||
|
qmlRegisterUncreatableType<DeviceVerificationFlow>(
|
||||||
|
"im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
|
||||||
|
qmlRegisterUncreatableType<UserProfile>(
|
||||||
|
"im.nheko",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"UserProfileModel",
|
||||||
|
"UserProfile needs to be instantiated on the C++ side");
|
||||||
|
|
||||||
|
static auto self = this;
|
||||||
|
qmlRegisterSingletonType<TimelineViewManager>(
|
||||||
|
"im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
|
||||||
|
return self;
|
||||||
|
});
|
||||||
|
qmlRegisterSingletonType<UserSettings>(
|
||||||
|
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
|
||||||
|
return self->settings.data();
|
||||||
|
});
|
||||||
|
|
||||||
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
|
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
|
||||||
|
qRegisterMetaType<std::vector<DeviceInfo>>();
|
||||||
|
|
||||||
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
|
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
|
||||||
qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
|
qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
|
||||||
qmlRegisterUncreatableType<QAbstractItemModel>(
|
qmlRegisterUncreatableType<QAbstractItemModel>(
|
||||||
@ -123,8 +170,6 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
|||||||
});
|
});
|
||||||
#endif
|
#endif
|
||||||
container->setMinimumSize(200, 200);
|
container->setMinimumSize(200, 200);
|
||||||
view->rootContext()->setContextProperty("timelineManager", this);
|
|
||||||
view->rootContext()->setContextProperty("settings", settings.data());
|
|
||||||
updateColorPalette();
|
updateColorPalette();
|
||||||
view->engine()->addImageProvider("MxcImage", imgProvider);
|
view->engine()->addImageProvider("MxcImage", imgProvider);
|
||||||
view->engine()->addImageProvider("colorimage", colorImgProvider);
|
view->engine()->addImageProvider("colorimage", colorImgProvider);
|
||||||
@ -136,6 +181,57 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
|||||||
&ChatPage::decryptSidebarChanged,
|
&ChatPage::decryptSidebarChanged,
|
||||||
this,
|
this,
|
||||||
&TimelineViewManager::updateEncryptedDescriptions);
|
&TimelineViewManager::updateEncryptedDescriptions);
|
||||||
|
connect(
|
||||||
|
dynamic_cast<ChatPage *>(parent),
|
||||||
|
&ChatPage::receivedRoomDeviceVerificationRequest,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
|
||||||
|
TimelineModel *model) {
|
||||||
|
auto event_id = QString::fromStdString(message.event_id);
|
||||||
|
if (!this->dvList.contains(event_id)) {
|
||||||
|
if (auto flow = DeviceVerificationFlow::NewInRoomVerification(
|
||||||
|
this,
|
||||||
|
model,
|
||||||
|
message.content,
|
||||||
|
QString::fromStdString(message.sender),
|
||||||
|
event_id)) {
|
||||||
|
dvList[event_id] = flow;
|
||||||
|
emit newDeviceVerificationRequest(flow.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(dynamic_cast<ChatPage *>(parent),
|
||||||
|
&ChatPage::receivedDeviceVerificationRequest,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationRequest &msg, std::string sender) {
|
||||||
|
if (!msg.transaction_id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto txnid = QString::fromStdString(msg.transaction_id.value());
|
||||||
|
if (!this->dvList.contains(txnid)) {
|
||||||
|
if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
|
||||||
|
this, msg, QString::fromStdString(sender), txnid)) {
|
||||||
|
dvList[txnid] = flow;
|
||||||
|
emit newDeviceVerificationRequest(flow.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(dynamic_cast<ChatPage *>(parent),
|
||||||
|
&ChatPage::receivedDeviceVerificationStart,
|
||||||
|
this,
|
||||||
|
[this](const mtx::events::msg::KeyVerificationStart &msg, std::string sender) {
|
||||||
|
if (!msg.transaction_id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto txnid = QString::fromStdString(msg.transaction_id.value());
|
||||||
|
if (!this->dvList.contains(txnid)) {
|
||||||
|
if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
|
||||||
|
this, msg, QString::fromStdString(sender), txnid)) {
|
||||||
|
dvList[txnid] = flow;
|
||||||
|
emit newDeviceVerificationRequest(flow.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
connect(parent, &ChatPage::loggedOut, this, [this]() {
|
connect(parent, &ChatPage::loggedOut, this, [this]() {
|
||||||
isInitialSync_ = true;
|
isInitialSync_ = true;
|
||||||
emit initialSyncChanged(true);
|
emit initialSyncChanged(true);
|
||||||
@ -284,6 +380,58 @@ TimelineViewManager::openRoomSettings() const
|
|||||||
MainWindow::instance()->openRoomSettings(timeline_->roomId());
|
MainWindow::instance()->openRoomSettings(timeline_->roomId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TimelineViewManager::verifyUser(QString userid)
|
||||||
|
{
|
||||||
|
auto joined_rooms = cache::joinedRooms();
|
||||||
|
auto room_infos = cache::getRoomInfo(joined_rooms);
|
||||||
|
|
||||||
|
for (std::string room_id : joined_rooms) {
|
||||||
|
if ((room_infos[QString::fromStdString(room_id)].member_count == 2) &&
|
||||||
|
cache::isRoomEncrypted(room_id)) {
|
||||||
|
auto room_members = cache::roomMembers(room_id);
|
||||||
|
if (std::find(room_members.begin(),
|
||||||
|
room_members.end(),
|
||||||
|
(userid).toStdString()) != room_members.end()) {
|
||||||
|
auto model = models.value(QString::fromStdString(room_id));
|
||||||
|
auto flow = DeviceVerificationFlow::InitiateUserVerification(
|
||||||
|
this, model.data(), userid);
|
||||||
|
connect(model.data(),
|
||||||
|
&TimelineModel::updateFlowEventId,
|
||||||
|
this,
|
||||||
|
[this, flow](std::string eventId) {
|
||||||
|
dvList[QString::fromStdString(eventId)] = flow;
|
||||||
|
});
|
||||||
|
emit newDeviceVerificationRequest(flow.data());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit ChatPage::instance()->showNotification(
|
||||||
|
tr("No share room with this user found. Create an "
|
||||||
|
"encrypted room with this user and try again."));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TimelineViewManager::removeVerificationFlow(DeviceVerificationFlow *flow)
|
||||||
|
{
|
||||||
|
for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) {
|
||||||
|
if ((*it).second == flow) {
|
||||||
|
dvList.remove((*it).first);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TimelineViewManager::verifyDevice(QString userid, QString deviceid)
|
||||||
|
{
|
||||||
|
auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid);
|
||||||
|
this->dvList[flow->transactionId()] = flow;
|
||||||
|
emit newDeviceVerificationRequest(flow.data());
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineViewManager::updateReadReceipts(const QString &room_id,
|
TimelineViewManager::updateReadReceipts(const QString &room_id,
|
||||||
const std::vector<QString> &event_ids)
|
const std::vector<QString> &event_ids)
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "CallManager.h"
|
#include "CallManager.h"
|
||||||
|
#include "DeviceVerificationFlow.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "TimelineModel.h"
|
#include "TimelineModel.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
@ -71,6 +72,10 @@ public:
|
|||||||
Q_INVOKABLE void openMemberListDialog() const;
|
Q_INVOKABLE void openMemberListDialog() const;
|
||||||
Q_INVOKABLE void openLeaveRoomDialog() const;
|
Q_INVOKABLE void openLeaveRoomDialog() const;
|
||||||
Q_INVOKABLE void openRoomSettings() const;
|
Q_INVOKABLE void openRoomSettings() const;
|
||||||
|
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
|
||||||
|
|
||||||
|
void verifyUser(QString userid);
|
||||||
|
void verifyDevice(QString userid, QString deviceid);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void clearRoomMessageCount(QString roomid);
|
void clearRoomMessageCount(QString roomid);
|
||||||
@ -79,6 +84,7 @@ signals:
|
|||||||
void initialSyncChanged(bool isInitialSync);
|
void initialSyncChanged(bool isInitialSync);
|
||||||
void replyingEventChanged(QString replyingEvent);
|
void replyingEventChanged(QString replyingEvent);
|
||||||
void replyClosed();
|
void replyClosed();
|
||||||
|
void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
|
||||||
void inviteUsers(QStringList users);
|
void inviteUsers(QStringList users);
|
||||||
void showRoomList();
|
void showRoomList();
|
||||||
void narrowViewChanged();
|
void narrowViewChanged();
|
||||||
@ -172,4 +178,14 @@ private:
|
|||||||
|
|
||||||
QSharedPointer<UserSettings> settings;
|
QSharedPointer<UserSettings> settings;
|
||||||
QHash<QString, QColor> userColors;
|
QHash<QString, QColor> userColors;
|
||||||
|
|
||||||
|
QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;
|
||||||
};
|
};
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationAccept)
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationCancel)
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationDone)
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationKey)
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationMac)
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationReady)
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationRequest)
|
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationStart)
|
||||||
|
227
src/ui/UserProfile.cpp
Normal file
227
src/ui/UserProfile.cpp
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
#include "UserProfile.h"
|
||||||
|
#include "Cache_p.h"
|
||||||
|
#include "ChatPage.h"
|
||||||
|
#include "DeviceVerificationFlow.h"
|
||||||
|
#include "Logging.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include "mtx/responses/crypto.hpp"
|
||||||
|
#include "timeline/TimelineModel.h"
|
||||||
|
#include "timeline/TimelineViewManager.h"
|
||||||
|
|
||||||
|
UserProfile::UserProfile(QString roomid,
|
||||||
|
QString userid,
|
||||||
|
TimelineViewManager *manager_,
|
||||||
|
TimelineModel *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, roomid_(roomid)
|
||||||
|
, userid_(userid)
|
||||||
|
, manager(manager_)
|
||||||
|
, model(parent)
|
||||||
|
{
|
||||||
|
fetchDeviceList(this->userid_);
|
||||||
|
|
||||||
|
connect(cache::client(),
|
||||||
|
&Cache::verificationStatusChanged,
|
||||||
|
this,
|
||||||
|
[this](const std::string &user_id) {
|
||||||
|
if (user_id != this->userid_.toStdString())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto status = cache::verificationStatus(user_id);
|
||||||
|
if (!status)
|
||||||
|
return;
|
||||||
|
this->isUserVerified = status->user_verified;
|
||||||
|
emit userStatusChanged();
|
||||||
|
|
||||||
|
for (auto &deviceInfo : deviceList_.deviceList_) {
|
||||||
|
deviceInfo.verification_status =
|
||||||
|
std::find(status->verified_devices.begin(),
|
||||||
|
status->verified_devices.end(),
|
||||||
|
deviceInfo.device_id.toStdString()) ==
|
||||||
|
status->verified_devices.end()
|
||||||
|
? verification::UNVERIFIED
|
||||||
|
: verification::VERIFIED;
|
||||||
|
}
|
||||||
|
deviceList_.reset(deviceList_.deviceList_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray>
|
||||||
|
DeviceInfoModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{DeviceId, "deviceId"},
|
||||||
|
{DeviceName, "deviceName"},
|
||||||
|
{VerificationStatus, "verificationStatus"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant
|
||||||
|
DeviceInfoModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case DeviceId:
|
||||||
|
return deviceList_[index.row()].device_id;
|
||||||
|
case DeviceName:
|
||||||
|
return deviceList_[index.row()].display_name;
|
||||||
|
case VerificationStatus:
|
||||||
|
return QVariant::fromValue(deviceList_[index.row()].verification_status);
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
this->deviceList_ = std::move(deviceList);
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceInfoModel *
|
||||||
|
UserProfile::deviceList()
|
||||||
|
{
|
||||||
|
return &this->deviceList_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
UserProfile::userid()
|
||||||
|
{
|
||||||
|
return this->userid_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
UserProfile::displayName()
|
||||||
|
{
|
||||||
|
return cache::displayName(roomid_, userid_);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
UserProfile::avatarUrl()
|
||||||
|
{
|
||||||
|
return cache::avatarUrl(roomid_, userid_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
UserProfile::getUserStatus()
|
||||||
|
{
|
||||||
|
return isUserVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserProfile::fetchDeviceList(const QString &userID)
|
||||||
|
{
|
||||||
|
auto localUser = utils::localUser();
|
||||||
|
|
||||||
|
ChatPage::instance()->query_keys(
|
||||||
|
userID.toStdString(),
|
||||||
|
[other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||||
|
err->matrix_error.errcode,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finding if the User is Verified or not based on the Signatures
|
||||||
|
ChatPage::instance()->query_keys(
|
||||||
|
utils::localUser().toStdString(),
|
||||||
|
[other_user_id, other_user_keys, this](const UserKeyCache &res,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
using namespace mtx;
|
||||||
|
std::string local_user_id = utils::localUser().toStdString();
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to query device keys: {},{}",
|
||||||
|
err->matrix_error.errcode,
|
||||||
|
static_cast<int>(err->status_code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.device_keys.empty()) {
|
||||||
|
nhlog::net()->warn("no devices retrieved {}", local_user_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DeviceInfo> deviceInfo;
|
||||||
|
auto devices = other_user_keys.device_keys;
|
||||||
|
auto verificationStatus =
|
||||||
|
cache::client()->verificationStatus(other_user_id);
|
||||||
|
|
||||||
|
isUserVerified = verificationStatus.user_verified;
|
||||||
|
emit userStatusChanged();
|
||||||
|
|
||||||
|
for (const auto &d : devices) {
|
||||||
|
auto device = d.second;
|
||||||
|
verification::Status verified =
|
||||||
|
verification::Status::UNVERIFIED;
|
||||||
|
|
||||||
|
if (std::find(verificationStatus.verified_devices.begin(),
|
||||||
|
verificationStatus.verified_devices.end(),
|
||||||
|
device.device_id) !=
|
||||||
|
verificationStatus.verified_devices.end() &&
|
||||||
|
mtx::crypto::verify_identity_signature(
|
||||||
|
device,
|
||||||
|
DeviceId(device.device_id),
|
||||||
|
UserId(other_user_id)))
|
||||||
|
verified = verification::Status::VERIFIED;
|
||||||
|
|
||||||
|
deviceInfo.push_back(
|
||||||
|
{QString::fromStdString(d.first),
|
||||||
|
QString::fromStdString(
|
||||||
|
device.unsigned_info.device_display_name),
|
||||||
|
verified});
|
||||||
|
}
|
||||||
|
|
||||||
|
this->deviceList_.queueReset(std::move(deviceInfo));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserProfile::banUser()
|
||||||
|
{
|
||||||
|
ChatPage::instance()->banUser(this->userid_, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// void ignoreUser(){
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
void
|
||||||
|
UserProfile::kickUser()
|
||||||
|
{
|
||||||
|
ChatPage::instance()->kickUser(this->userid_, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserProfile::startChat()
|
||||||
|
{
|
||||||
|
mtx::requests::CreateRoom req;
|
||||||
|
req.preset = mtx::requests::Preset::PrivateChat;
|
||||||
|
req.visibility = mtx::requests::Visibility::Private;
|
||||||
|
if (utils::localUser() != this->userid_)
|
||||||
|
req.invite = {this->userid_.toStdString()};
|
||||||
|
emit ChatPage::instance()->createRoom(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserProfile::verify(QString device)
|
||||||
|
{
|
||||||
|
if (!device.isEmpty())
|
||||||
|
manager->verifyDevice(userid_, device);
|
||||||
|
else {
|
||||||
|
manager->verifyUser(userid_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserProfile::unverify(QString device)
|
||||||
|
{
|
||||||
|
cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
|
||||||
|
}
|
123
src/ui/UserProfile.h
Normal file
123
src/ui/UserProfile.h
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include "MatrixClient.h"
|
||||||
|
|
||||||
|
namespace verification {
|
||||||
|
Q_NAMESPACE
|
||||||
|
|
||||||
|
enum Status
|
||||||
|
{
|
||||||
|
VERIFIED,
|
||||||
|
UNVERIFIED,
|
||||||
|
BLOCKED
|
||||||
|
};
|
||||||
|
Q_ENUM_NS(Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeviceVerificationFlow;
|
||||||
|
class TimelineModel;
|
||||||
|
class TimelineViewManager;
|
||||||
|
|
||||||
|
class DeviceInfo
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeviceInfo(const QString deviceID,
|
||||||
|
const QString displayName,
|
||||||
|
verification::Status verification_status_)
|
||||||
|
: device_id(deviceID)
|
||||||
|
, display_name(displayName)
|
||||||
|
, verification_status(verification_status_)
|
||||||
|
{}
|
||||||
|
DeviceInfo()
|
||||||
|
: verification_status(verification::UNVERIFIED)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QString device_id;
|
||||||
|
QString display_name;
|
||||||
|
|
||||||
|
verification::Status verification_status;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeviceInfoModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum Roles
|
||||||
|
{
|
||||||
|
DeviceId,
|
||||||
|
DeviceName,
|
||||||
|
VerificationStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit DeviceInfoModel(QObject *parent = nullptr)
|
||||||
|
{
|
||||||
|
(void)parent;
|
||||||
|
connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset);
|
||||||
|
};
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
||||||
|
{
|
||||||
|
(void)parent;
|
||||||
|
return (int)deviceList_.size();
|
||||||
|
}
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void queueReset(const std::vector<DeviceInfo> &deviceList);
|
||||||
|
public slots:
|
||||||
|
void reset(const std::vector<DeviceInfo> &deviceList);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<DeviceInfo> deviceList_;
|
||||||
|
|
||||||
|
friend class UserProfile;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UserProfile : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QString displayName READ displayName CONSTANT)
|
||||||
|
Q_PROPERTY(QString userid READ userid CONSTANT)
|
||||||
|
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
|
||||||
|
Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT)
|
||||||
|
Q_PROPERTY(bool isUserVerified READ getUserStatus NOTIFY userStatusChanged)
|
||||||
|
public:
|
||||||
|
UserProfile(QString roomid,
|
||||||
|
QString userid,
|
||||||
|
TimelineViewManager *manager_,
|
||||||
|
TimelineModel *parent = nullptr);
|
||||||
|
|
||||||
|
DeviceInfoModel *deviceList();
|
||||||
|
|
||||||
|
QString userid();
|
||||||
|
QString displayName();
|
||||||
|
QString avatarUrl();
|
||||||
|
bool getUserStatus();
|
||||||
|
|
||||||
|
Q_INVOKABLE void verify(QString device = "");
|
||||||
|
Q_INVOKABLE void unverify(QString device = "");
|
||||||
|
Q_INVOKABLE void fetchDeviceList(const QString &userID);
|
||||||
|
Q_INVOKABLE void banUser();
|
||||||
|
// Q_INVOKABLE void ignoreUser();
|
||||||
|
Q_INVOKABLE void kickUser();
|
||||||
|
Q_INVOKABLE void startChat();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void userStatusChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString roomid_, userid_;
|
||||||
|
DeviceInfoModel deviceList_;
|
||||||
|
bool isUserVerified = false;
|
||||||
|
TimelineViewManager *manager;
|
||||||
|
TimelineModel *model;
|
||||||
|
|
||||||
|
void callback_fn(const mtx::responses::QueryKeys &res,
|
||||||
|
mtx::http::RequestErr err,
|
||||||
|
std::string user_id);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user