Browse Source

verification: Add qr-code scanning for verification

This also moves the error handling to the `IdentityVerification`
merge-requests/1327/merge
Julian Sparber 4 years ago
parent
commit
ff2a3ca741
  1. 860
      Cargo.lock
  2. 5
      Cargo.toml
  3. 7
      build-aux/org.gnome.FractalNext.Devel.json
  4. 1
      data/resources/resources.gresource.xml
  5. 13
      data/resources/ui/qr-code-scanner.ui
  6. 171
      data/resources/ui/session-verification.ui
  7. 2
      src/contrib/mod.rs
  8. 461
      src/contrib/qr_code_scanner/camera_paintable.rs
  9. 199
      src/contrib/qr_code_scanner/mod.rs
  10. 140
      src/contrib/qr_code_scanner/qr_code_detector.rs
  11. 14
      src/contrib/qr_code_scanner/screenshot.rs
  12. 1
      src/main.rs
  13. 4
      src/meson.build
  14. 338
      src/session/verification/identity_verification.rs
  15. 144
      src/session/verification/session_verification.rs

860
Cargo.lock generated

File diff suppressed because it is too large Load Diff

5
Cargo.toml

@ -28,6 +28,11 @@ futures = "0.3"
rand = "0.8"
indexmap = "1.6.2"
qrcode = "0.12.0"
ashpd = {git = "https://github.com/bilelmoussaoui/ashpd", rev="66d4dc0020181a7174451150ecc711344082b5ce", features=["feature_gtk4", "feature_pipewire", "log"]}
gst = {version = "0.17", package = "gstreamer"}
gst_base = {version = "0.17", package = "gstreamer-base"}
gst_video = {version = "0.17", package = "gstreamer-video"}
image = {version = "0.23", default-features = false, features=["png"]}
[dependencies.sourceview]
package = "sourceview5"

7
build-aux/org.gnome.FractalNext.Devel.json

@ -4,13 +4,15 @@
"runtime-version" : "master",
"sdk" : "org.gnome.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable"
"org.freedesktop.Sdk.Extension.rust-stable",
"org.freedesktop.Sdk.Extension.llvm12"
],
"command" : "fractal",
"finish-args" : [
"--socket=fallback-x11",
"--socket=wayland",
"--share=network",
"--share=ipc",
"--device=dri",
"--talk-name=org.a11y.Bus",
"--talk-name=org.freedesktop.secrets",
@ -19,7 +21,8 @@
"--env=RUST_BACKTRACE=1"
],
"build-options" : {
"append-path" : "/usr/lib/sdk/rust-stable/bin",
"append-ld-library-path": "/usr/lib/sdk/llvm12/lib",
"append-path" : "/usr/lib/sdk/llvm12/bin:/usr/lib/sdk/rust-stable/bin",
"build-args" : [
"--share=network"
],

1
data/resources/resources.gresource.xml

@ -44,6 +44,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="room-creation.ui">ui/room-creation.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="session-verification.ui">ui/session-verification.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="verification-emoji.ui">ui/verification-emoji.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="qr-code-scanner.ui">ui/qr-code-scanner.ui</file>
<file compressed="true">style.css</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>

13
data/resources/ui/qr-code-scanner.ui

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="QrCodeScanner" parent="AdwBin">
<property name="child">
<object class="GtkPicture" id="picture">
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="keep-aspect-ratio">False</property>
<property name="height-request">400</property>
</object>
</property>
</template>
</interface>

171
data/resources/ui/session-verification.ui

@ -140,6 +140,177 @@
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">scan-qr-code</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="tightening-threshold">300</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<property name="valign">center</property>
<property name="halign">center</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Verify Session</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
<style>
<class name="title-1"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Scan the Qr code with this session from another session logged into this account</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
</object>
</child>
<child>
<object class="QrCodeScanner" id="qr_code_scanner">
<property name="margin-top">24</property>
<property name="margin-bottom">24</property>
<property name="vexpand">True</property>
<property name="hexpand">True</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Can't scan QR code?</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
</object>
</child>
<child>
<object class="SpinnerButton" id="start_emoji_btn2">
<property name="label" translatable="yes">Compare Emoji</property>
<property name="halign">center</property>
<style>
<class name="pill"/>
</style>
</object>
</child>
<child>
<object class="SpinnerButton" id="take_screenshot_btn2">
<property name="label" translatable="yes">Take a Screenshot of a Qr Code</property>
<property name="halign">center</property>
<style>
<class name="pill"/>
</style>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">qr-code-scanned</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="tightening-threshold">300</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<property name="valign">center</property>
<property name="halign">center</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Scan Complete</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
<style>
<class name="title-1"/>
</style>
</object>
</child>
<child>
<object class="GtkPicture">
<property name="file">resource:///org/gnome/FractalNext/icons/scalable/status/setup-complete.svg</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">You scanned to qr code successfully. You may need to confirm the verification in the other session.</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">no-camera</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="tightening-threshold">300</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<property name="valign">center</property>
<property name="halign">center</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Verify Session</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
<style>
<class name="title-1"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Select an option to verify the new session.</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
</object>
</child>
<child>
<object class="SpinnerButton" id="start_emoji_btn3">
<property name="label" translatable="yes">Compare Emoji</property>
<property name="halign">center</property>
<style>
<class name="pill"/>
</style>
</object>
</child>
<child>
<object class="SpinnerButton" id="take_screenshot_btn3">
<property name="label" translatable="yes">Take a Screenshot of a Qr Code</property>
<property name="halign">center</property>
<style>
<class name="pill"/>
</style>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">qrcode</property>

2
src/contrib/mod.rs

@ -1,3 +1,5 @@
mod qr_code;
mod qr_code_scanner;
pub use self::qr_code::{QRCode, QRCodeExt};
pub use self::qr_code_scanner::{screenshot, QrCodeScanner};

461
src/contrib/qr_code_scanner/camera_paintable.rs

@ -0,0 +1,461 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Fancy Camera with QR code detection
//
// Pipeline:
// queue -- videoconvert -- QrCodeDetector sink
// /
// pipewiresrc -- tee
// \
// queue -- videoconvert -- our fancy sink
use glib::{clone, Receiver, Sender};
use gst::prelude::*;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use once_cell::sync::Lazy;
use std::os::unix::io::AsRawFd;
use std::sync::{Arc, Mutex};
use crate::contrib::qr_code_scanner::qr_code_detector::QrCodeDetector;
use crate::contrib::qr_code_scanner::QrVerificationDataBoxed;
use gtk::{gdk, graphene};
use matrix_sdk::encryption::verification::QrVerificationData;
pub enum Action {
FrameChanged,
QrCodeDetected(QrVerificationData),
}
mod camera_sink {
use std::convert::AsRef;
#[derive(Debug)]
pub struct Frame(pub gst_video::VideoFrame<gst_video::video_frame::Readable>);
impl AsRef<[u8]> for Frame {
fn as_ref(&self) -> &[u8] {
self.0.plane_data(0).unwrap()
}
}
impl From<Frame> for gdk::Paintable {
fn from(f: Frame) -> gdk::Paintable {
let format = match f.0.format() {
gst_video::VideoFormat::Bgra => gdk::MemoryFormat::B8g8r8a8,
gst_video::VideoFormat::Argb => gdk::MemoryFormat::A8r8g8b8,
gst_video::VideoFormat::Rgba => gdk::MemoryFormat::R8g8b8a8,
gst_video::VideoFormat::Abgr => gdk::MemoryFormat::A8b8g8r8,
gst_video::VideoFormat::Rgb => gdk::MemoryFormat::R8g8b8,
gst_video::VideoFormat::Bgr => gdk::MemoryFormat::B8g8r8,
_ => unreachable!(),
};
let width = f.0.width() as i32;
let height = f.0.height() as i32;
let rowstride = f.0.plane_stride()[0] as usize;
gdk::MemoryTexture::new(
width,
height,
format,
&glib::Bytes::from_owned(f),
rowstride,
)
.upcast()
}
}
impl Frame {
pub fn new(buffer: &gst::Buffer, info: &gst_video::VideoInfo) -> Self {
let video_frame =
gst_video::VideoFrame::from_buffer_readable(buffer.clone(), &info).unwrap();
Self(video_frame)
}
pub fn width(&self) -> u32 {
self.0.width()
}
pub fn height(&self) -> u32 {
self.0.height()
}
}
use super::*;
mod imp {
use std::sync::Mutex;
use gst::subclass::prelude::*;
use gst_base::subclass::prelude::*;
use gst_video::subclass::prelude::*;
use once_cell::sync::Lazy;
use super::*;
#[derive(Default)]
pub struct CameraSink {
pub info: Mutex<Option<gst_video::VideoInfo>>,
pub sender: Mutex<Option<Sender<Action>>>,
pub pending_frame: Mutex<Option<Frame>>,
}
#[glib::object_subclass]
impl ObjectSubclass for CameraSink {
const NAME: &'static str = "CameraSink";
type Type = super::CameraSink;
type ParentType = gst_video::VideoSink;
}
impl ObjectImpl for CameraSink {}
impl ElementImpl for CameraSink {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"GTK Camera Sink",
"Sink/Camera/Video",
"A GTK Camera sink",
"Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let caps = gst_video::video_make_raw_caps(&[
gst_video::VideoFormat::Bgra,
gst_video::VideoFormat::Argb,
gst_video::VideoFormat::Rgba,
gst_video::VideoFormat::Abgr,
gst_video::VideoFormat::Rgb,
gst_video::VideoFormat::Bgr,
])
.any_features()
.build();
vec![gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&caps,
)
.unwrap()]
});
PAD_TEMPLATES.as_ref()
}
}
impl BaseSinkImpl for CameraSink {
fn set_caps(
&self,
_element: &Self::Type,
caps: &gst::Caps,
) -> Result<(), gst::LoggableError> {
let video_info = gst_video::VideoInfo::from_caps(caps).unwrap();
let mut info = self.info.lock().unwrap();
info.replace(video_info);
Ok(())
}
}
impl VideoSinkImpl for CameraSink {
fn show_frame(
&self,
_element: &Self::Type,
buffer: &gst::Buffer,
) -> Result<gst::FlowSuccess, gst::FlowError> {
if let Some(info) = &*self.info.lock().unwrap() {
let frame = Frame::new(buffer, info);
let mut last_frame = self.pending_frame.lock().unwrap();
last_frame.replace(frame);
let sender = self.sender.lock().unwrap();
sender.as_ref().unwrap().send(Action::FrameChanged).unwrap();
}
Ok(gst::FlowSuccess::Ok)
}
}
}
glib::wrapper! {
pub struct CameraSink(ObjectSubclass<imp::CameraSink>) @extends gst_video::VideoSink, gst_base::BaseSink, gst::Element, gst::Object;
}
unsafe impl Send for CameraSink {}
unsafe impl Sync for CameraSink {}
impl CameraSink {
pub fn new(sender: Sender<Action>) -> Self {
let sink = glib::Object::new(&[]).expect("Failed to create a CameraSink");
let priv_ = imp::CameraSink::from_instance(&sink);
priv_.sender.lock().unwrap().replace(sender);
sink
}
pub fn pending_frame(&self) -> Option<Frame> {
let self_ = imp::CameraSink::from_instance(self);
self_.pending_frame.lock().unwrap().take()
}
}
}
mod imp {
use glib::subclass;
use std::cell::RefCell;
use super::*;
pub struct CameraPaintable {
pub sink: camera_sink::CameraSink,
pub detector: QrCodeDetector,
pub pipeline: RefCell<Option<gst::Pipeline>>,
pub sender: Sender<Action>,
pub image: RefCell<Option<gdk::Paintable>>,
pub size: RefCell<Option<(u32, u32)>>,
pub receiver: RefCell<Option<Receiver<Action>>>,
}
impl Default for CameraPaintable {
fn default() -> Self {
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let receiver = RefCell::new(Some(r));
Self {
pipeline: RefCell::default(),
sink: camera_sink::CameraSink::new(sender.clone()),
detector: QrCodeDetector::new(sender.clone()),
image: RefCell::new(None),
sender,
receiver,
size: RefCell::new(None),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for CameraPaintable {
const NAME: &'static str = "CameraPaintable";
type Type = super::CameraPaintable;
type ParentType = glib::Object;
type Interfaces = (gdk::Paintable,);
}
impl ObjectImpl for CameraPaintable {
fn constructed(&self, obj: &Self::Type) {
obj.init_widgets();
self.parent_constructed(obj);
}
fn dispose(&self, paintable: &Self::Type) {
paintable.close_pipeline();
}
fn signals() -> &'static [subclass::Signal] {
static SIGNALS: Lazy<Vec<subclass::Signal>> = Lazy::new(|| {
vec![subclass::Signal::builder(
"code-detected",
&[QrVerificationDataBoxed::static_type().into()],
glib::Type::UNIT.into(),
)
.flags(glib::SignalFlags::RUN_FIRST)
.build()]
});
SIGNALS.as_ref()
}
}
impl PaintableImpl for CameraPaintable {
fn intrinsic_height(&self, _paintable: &Self::Type) -> i32 {
if let Some((_, height)) = *self.size.borrow() {
height as i32
} else {
0
}
}
fn intrinsic_width(&self, _paintable: &Self::Type) -> i32 {
if let Some((width, _)) = *self.size.borrow() {
width as i32
} else {
0
}
}
fn snapshot(
&self,
_paintable: &Self::Type,
snapshot: &gdk::Snapshot,
width: f64,
height: f64,
) {
let snapshot = snapshot.downcast_ref::<gtk::Snapshot>().unwrap();
if let Some(ref image) = *self.image.borrow() {
// Transformation to avoid stretching the camera. We translate and scale the image
// under a clip.
let clip = graphene::Rect::new(0.0, 0.0, width as f32, height as f32);
let aspect = width / height.max(std::f64::EPSILON); // Do not divide by zero.
let image_aspect = image.intrinsic_aspect_ratio();
snapshot.push_clip(&clip);
if image_aspect == 0.0 {
image.snapshot(snapshot.upcast_ref(), width, height);
return;
};
let (new_width, new_height) = match aspect <= image_aspect {
true => (height * image_aspect, height), // Mobile view
false => (width, width / image_aspect), // Landscape
};
let p = graphene::Point::new(
((width - new_width) / 2.0) as f32,
((height - new_height) / 2.0) as f32,
);
snapshot.translate(&p);
image.snapshot(snapshot.upcast_ref(), new_width, new_height);
snapshot.pop();
} else {
snapshot.append_color(
&gdk::RGBA::black(),
&graphene::Rect::new(0f32, 0f32, width as f32, height as f32),
);
}
}
}
}
glib::wrapper! {
pub struct CameraPaintable(ObjectSubclass<imp::CameraPaintable>) @implements gdk::Paintable;
}
impl Default for CameraPaintable {
fn default() -> Self {
glib::Object::new(&[]).expect("Failed to create a CameraPaintable")
}
}
impl CameraPaintable {
pub fn set_pipewire_fd<F: AsRawFd>(&self, fd: F, node_id: u32) {
let pipewire_element = gst::ElementFactory::make("pipewiresrc", None).unwrap();
pipewire_element
.set_property("fd", &fd.as_raw_fd())
.unwrap();
pipewire_element
.set_property("path", &node_id.to_string())
.unwrap();
self.init_pipeline(pipewire_element);
}
fn init_pipeline(&self, pipewire_src: gst::Element) {
let self_ = imp::CameraPaintable::from_instance(self);
let pipeline = gst::Pipeline::new(None);
let tee = gst::ElementFactory::make("tee", None).unwrap();
let queue = gst::ElementFactory::make("queue", None).unwrap();
let videoconvert1 = gst::ElementFactory::make("videoconvert", None).unwrap();
let videoconvert2 = gst::ElementFactory::make("videoconvert", None).unwrap();
let src_pad = queue.static_pad("src").unwrap();
// Reduce the number of frames we use to get the qrcode from
let start = Arc::new(Mutex::new(std::time::Instant::now()));
src_pad.add_probe(gst::PadProbeType::BUFFER, move |_, _| {
let mut start = start.lock().unwrap();
if start.elapsed() < std::time::Duration::from_millis(500) {
gst::PadProbeReturn::Drop
} else {
*start = std::time::Instant::now();
gst::PadProbeReturn::Ok
}
});
let queue2 = gst::ElementFactory::make("queue", None).unwrap();
pipeline
.add_many(&[
&pipewire_src,
&tee,
&queue,
&videoconvert1,
self_.detector.upcast_ref(),
&queue2,
&videoconvert2,
self_.sink.upcast_ref(),
])
.unwrap();
gst::Element::link_many(&[
&pipewire_src,
&tee,
&queue,
&videoconvert1,
self_.detector.upcast_ref(),
])
.unwrap();
tee.link_pads(None, &queue2, None).unwrap();
gst::Element::link_many(&[&queue2, &videoconvert2, self_.sink.upcast_ref()]).unwrap();
let bus = pipeline.bus().unwrap();
bus.add_watch_local(
clone!(@weak self as paintable => @default-return glib::Continue(false), move |_, msg| {
match msg.view() {
gst::MessageView::Error(err) => {
log::error!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
},
_ => (),
}
glib::Continue(true)
}),
)
.expect("Failed to add bus watch");
pipeline.set_state(gst::State::Playing).ok();
self_.pipeline.replace(Some(pipeline));
}
pub fn close_pipeline(&self) {
let self_ = imp::CameraPaintable::from_instance(self);
if let Some(pipeline) = self_.pipeline.borrow_mut().take() {
pipeline.set_state(gst::State::Null).unwrap();
}
}
pub fn init_widgets(&self) {
let self_ = imp::CameraPaintable::from_instance(self);
let receiver = self_.receiver.borrow_mut().take().unwrap();
receiver.attach(
None,
glib::clone!(@weak self as paintable => @default-return glib::Continue(false), move |action| paintable.do_action(action)),
);
}
fn do_action(&self, action: Action) -> glib::Continue {
let self_ = imp::CameraPaintable::from_instance(self);
match action {
Action::FrameChanged => {
if let Some(frame) = self_.sink.pending_frame() {
let (width, height) = (frame.width(), frame.height());
self_.size.replace(Some((width, height)));
self_.image.replace(Some(frame.into()));
self.invalidate_contents();
}
}
Action::QrCodeDetected(code) => {
self.emit_by_name("code-detected", &[&QrVerificationDataBoxed(code)])
.unwrap();
}
}
glib::Continue(true)
}
}

199
src/contrib/qr_code_scanner/mod.rs

@ -0,0 +1,199 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::spawn;
use ashpd::{desktop::camera, zbus};
use glib::clone;
use glib::subclass;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use matrix_sdk::encryption::verification::QrVerificationData;
use std::os::unix::prelude::RawFd;
mod camera_paintable;
mod qr_code_detector;
pub mod screenshot;
use camera_paintable::CameraPaintable;
mod imp {
use adw::subclass::prelude::*;
use gtk::CompositeTemplate;
use once_cell::sync::Lazy;
use std::cell::Cell;
use super::*;
#[derive(Debug, CompositeTemplate, Default)]
#[template(resource = "/org/gnome/FractalNext/qr-code-scanner.ui")]
pub struct QrCodeScanner {
pub paintable: CameraPaintable,
#[template_child]
pub picture: TemplateChild<gtk::Picture>,
pub has_camera: Cell<bool>,
pub is_started: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for QrCodeScanner {
const NAME: &'static str = "QrCodeScanner";
type Type = super::QrCodeScanner;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for QrCodeScanner {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpec::new_boolean(
"has-camera",
"Has Camera",
"Whether we have a working camera",
false,
glib::ParamFlags::READABLE,
)]
});
PROPERTIES.as_ref()
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"has-camera" => obj.has_camera().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.picture.set_paintable(Some(&self.paintable));
let callback = glib::clone!(@weak obj => @default-return None, move |args: &[glib::Value]| {
let code = args.get(1).unwrap().get::<QrVerificationDataBoxed>().unwrap();
obj.emit_by_name("code-detected", &[&code]).unwrap();
None
});
self.paintable
.connect_local("code-detected", false, callback)
.unwrap();
obj.init_has_camera();
}
fn signals() -> &'static [subclass::Signal] {
static SIGNALS: Lazy<Vec<subclass::Signal>> = Lazy::new(|| {
vec![subclass::Signal::builder(
"code-detected",
&[QrVerificationDataBoxed::static_type().into()],
glib::Type::UNIT.into(),
)
.flags(glib::SignalFlags::RUN_FIRST)
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for QrCodeScanner {
fn unmap(&self, widget: &Self::Type) {
self.parent_unmap(widget);
widget.stop();
}
}
impl BinImpl for QrCodeScanner {}
}
glib::wrapper! {
pub struct QrCodeScanner(ObjectSubclass<imp::QrCodeScanner>) @extends gtk::Widget, adw::Bin;
}
impl QrCodeScanner {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create a QrCodeScanner")
}
pub fn stop(&self) {
let self_ = imp::QrCodeScanner::from_instance(self);
self_.paintable.close_pipeline();
}
async fn start_internal(&self) -> bool {
let self_ = imp::QrCodeScanner::from_instance(self);
if let Ok(Some(stream_fd)) = stream().await {
if let Ok(node_id) = camera::pipewire_node_id(stream_fd).await {
self_.paintable.set_pipewire_fd(stream_fd, node_id);
self_.has_camera.set(true);
self.notify("has-camera");
return true;
}
}
self_.has_camera.set(false);
self.notify("has-camera");
false
}
pub async fn start(&self) -> bool {
let priv_ = imp::QrCodeScanner::from_instance(self);
let is_started = self.start_internal().await;
priv_.is_started.set(is_started);
is_started
}
fn init_has_camera(&self) {
spawn!(clone!(@weak self as obj => async move {
let priv_ = imp::QrCodeScanner::from_instance(&obj);
let has_camera = if obj.start_internal().await {
if !priv_.is_started.get() {
obj.stop();
}
true
} else {
false
};
priv_.has_camera.set(has_camera);
obj.notify("has-camera");
}));
}
pub fn has_camera(&self) -> bool {
let priv_ = imp::QrCodeScanner::from_instance(self);
priv_.has_camera.get()
}
/// Connects the prepared signals to the function f given in input
pub fn connect_code_detected<F: Fn(&Self, QrVerificationData) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("code-detected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let data = values[1].get::<QrVerificationDataBoxed>().unwrap();
f(&obj, data.0);
None
})
.unwrap()
}
}
async fn stream() -> Result<Option<RawFd>, ashpd::Error> {
let connection = zbus::Connection::session().await?;
let proxy = camera::CameraProxy::new(&connection).await?;
if proxy.is_camera_present().await? {
proxy.access_camera().await?;
Ok(Some(proxy.open_pipe_wire_remote().await?))
} else {
Ok(None)
}
}
#[derive(Clone, Debug, PartialEq, glib::GBoxed)]
#[gboxed(type_name = "QrVerificationDataBoxed")]
struct QrVerificationDataBoxed(QrVerificationData);

140
src/contrib/qr_code_scanner/qr_code_detector.rs

@ -0,0 +1,140 @@
use crate::contrib::qr_code_scanner::camera_paintable::Action;
use matrix_sdk::encryption::verification::QrVerificationData;
use glib::Sender;
use gst_video::{video_frame::VideoFrameRef, VideoInfo};
use log::debug;
use std::convert::AsRef;
use super::*;
mod imp {
use std::sync::Mutex;
use gst::subclass::prelude::*;
use gst_base::subclass::prelude::*;
use gst_video::subclass::prelude::*;
use once_cell::sync::Lazy;
use super::*;
#[derive(Default)]
pub struct QrCodeDetector {
pub info: Mutex<Option<VideoInfo>>,
pub sender: Mutex<Option<Sender<Action>>>,
pub code: Mutex<Option<QrVerificationData>>,
}
#[glib::object_subclass]
impl ObjectSubclass for QrCodeDetector {
const NAME: &'static str = "QrCodeDetector";
type Type = super::QrCodeDetector;
type ParentType = gst_video::VideoSink;
}
impl ObjectImpl for QrCodeDetector {}
impl ElementImpl for QrCodeDetector {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"Matrix Qr Code detector Sink",
"Sink/Video/QrCode/Matrix",
"A Qr code detector for Matrix",
"Julian Sparber <julian@sparber.net>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let caps = gst_video::video_make_raw_caps(&[gst_video::VideoFormat::Gray8])
.any_features()
.build();
vec![gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&caps,
)
.unwrap()]
});
PAD_TEMPLATES.as_ref()
}
}
impl BaseSinkImpl for QrCodeDetector {
fn set_caps(
&self,
_element: &Self::Type,
caps: &gst::Caps,
) -> Result<(), gst::LoggableError> {
let video_info = gst_video::VideoInfo::from_caps(caps).unwrap();
let mut info = self.info.lock().unwrap();
info.replace(video_info);
Ok(())
}
}
impl VideoSinkImpl for QrCodeDetector {
fn show_frame(
&self,
_element: &Self::Type,
buffer: &gst::Buffer,
) -> Result<gst::FlowSuccess, gst::FlowError> {
let now = std::time::Instant::now();
if let Some(info) = &*self.info.lock().unwrap() {
let frame = VideoFrameRef::from_buffer_ref_readable(buffer, info).unwrap();
let mut samples = image::FlatSamples::<Vec<u8>> {
samples: frame.plane_data(0).unwrap().to_vec(),
layout: image::flat::SampleLayout {
channels: 1,
channel_stride: 1,
width: frame.width(),
width_stride: 1,
height: frame.height(),
height_stride: frame.plane_stride()[0] as usize,
},
color_hint: Some(image::ColorType::L8),
};
let image = samples.as_view_mut::<image::Luma<u8>>().unwrap();
if let Ok(code) = QrVerificationData::from_luma(image) {
let mut previous_code = self.code.lock().unwrap();
if previous_code.as_ref() != Some(&code) {
previous_code.replace(code.clone());
let sender = self.sender.lock().unwrap();
sender
.as_ref()
.unwrap()
.send(Action::QrCodeDetected(code))
.unwrap();
}
}
}
debug!("Spend {}ms to detect qr code", now.elapsed().as_millis());
Ok(gst::FlowSuccess::Ok)
}
}
}
glib::wrapper! {
pub struct QrCodeDetector(ObjectSubclass<imp::QrCodeDetector>) @extends gst_video::VideoSink, gst_base::BaseSink, gst::Element, gst::Object;
}
unsafe impl Send for QrCodeDetector {}
unsafe impl Sync for QrCodeDetector {}
impl QrCodeDetector {
pub fn new(sender: Sender<Action>) -> Self {
let sink = glib::Object::new(&[]).expect("Failed to create a QrCodeDetector");
let priv_ = imp::QrCodeDetector::from_instance(&sink);
priv_.sender.lock().unwrap().replace(sender);
sink
}
}

14
src/contrib/qr_code_scanner/screenshot.rs

@ -0,0 +1,14 @@
use ashpd::desktop::screenshot;
use gtk::gio;
use gtk::prelude::*;
use matrix_sdk::encryption::verification::QrVerificationData;
pub async fn capture(root: &gtk::Root) -> Option<QrVerificationData> {
let identifier = ashpd::WindowIdentifier::from_native(root).await;
let uri = screenshot::take(&identifier, true, true).await.ok()?;
let screenshot = gio::File::for_uri(&uri);
let (data, _) = screenshot.load_contents(gio::NONE_CANCELLABLE).ok()?;
let image = image::load_from_memory(&data).ok()?;
QrVerificationData::from_image(image).ok()
}

1
src/main.rs

@ -48,6 +48,7 @@ fn main() {
gtk::init().expect("Unable to start GTK4");
adw::init();
gst::init().expect("Failed to initalize gst");
let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file");
gio::resources_register(&res);

4
src/meson.build

@ -21,6 +21,10 @@ run_command(
sources = files(
'application.rs',
'contrib/qr_code.rs',
'contrib/qr_code_scanner/camera_paintable.rs',
'contrib/qr_code_scanner/mod.rs',
'contrib/qr_code_scanner/screenshot.rs',
'contrib/qr_code_scanner/qr_code_detector.rs',
'components/avatar.rs',
'components/auth_dialog.rs',
'components/context_menu_bin.rs',

338
src/session/verification/identity_verification.rs

@ -1,18 +1,25 @@
use crate::session::user::UserExt;
use crate::session::User;
use crate::spawn;
use crate::spawn_tokio;
use crate::Error;
use gettextrs::gettext;
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
use log::error;
use log::warn;
use matrix_sdk::{
encryption::{
identities::RequestVerificationError,
verification::{
CancelInfo, Emoji, QrVerification, SasVerification, Verification as MatrixVerification,
VerificationRequest,
CancelInfo, Emoji, QrVerification, QrVerificationData, SasVerification,
Verification as MatrixVerification, VerificationRequest,
},
},
ruma::{
api::client::r0::sync::sync_events::ToDevice, events::AnyToDeviceEvent, identifiers::UserId,
api::client::r0::sync::sync_events::ToDevice,
events::key::verification::{cancel::CancelCode, VerificationMethod},
events::AnyToDeviceEvent,
identifiers::UserId,
},
Client, Error as MatrixError,
};
@ -62,26 +69,30 @@ pub enum Mode {
Unavailable,
Requested,
SasV1,
QrV1,
QrV1Show,
QrV1Scan,
Completed,
Cancelled,
Dismissed,
Error,
}
impl Default for Mode {
fn default() -> Self {
Self::Unavailable
Self::Requested
}
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq, Clone)]
pub enum UserAction {
Match,
NotMatch,
Cancel,
StartSas,
Scanned(QrVerificationData),
}
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, PartialEq)]
pub enum Message {
UserAction(UserAction),
Sync((String, State)),
@ -99,9 +110,10 @@ mod imp {
pub user: OnceCell<WeakRef<User>>,
pub mode: Cell<Mode>,
pub sync_sender: RefCell<Option<mpsc::Sender<Message>>>,
pub main_sender: OnceCell<glib::SyncSender<Verification>>,
pub main_sender: OnceCell<glib::SyncSender<(Verification, Mode)>>,
pub request: RefCell<Option<Verification>>,
pub source_id: RefCell<Option<SourceId>>,
pub flow_id: RefCell<Option<String>>,
}
#[glib::object_subclass]
@ -137,6 +149,13 @@ mod imp {
None,
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_string(
"flow-id",
"Flow Id",
"The flow id of this verification request",
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
@ -152,6 +171,7 @@ mod imp {
) {
match pspec.name() {
"user" => obj.set_user(value.get().unwrap()),
"flow-id" => obj.set_flow_id(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -161,6 +181,7 @@ mod imp {
"user" => obj.user().to_value(),
"mode" => obj.mode().to_value(),
"display-name" => obj.display_name().to_value(),
"flow-id" => obj.flow_id().to_value(),
_ => unimplemented!(),
}
}
@ -169,16 +190,11 @@ mod imp {
self.parent_constructed(obj);
let (main_sender, main_receiver) =
glib::MainContext::sync_channel::<Verification>(Default::default(), 100);
glib::MainContext::sync_channel::<(Verification, Mode)>(Default::default(), 100);
let source_id = main_receiver.attach(
None,
clone!(@weak obj => @default-return glib::Continue(false), move |verification| {
let mode = match verification {
Verification::QrV1(_) => Mode::QrV1,
Verification::SasV1(_) => Mode::SasV1,
Verification::Request(_) => Mode::Requested,
};
clone!(@weak obj => @default-return glib::Continue(false), move |(verification, mode)| {
obj.set_request(Some(verification));
obj.set_mode(mode);
@ -208,6 +224,38 @@ impl IdentityVerification {
glib::Object::new(&[("user", user)]).expect("Failed to create IdentityVerification")
}
pub fn accept_incoming(&self) {
let priv_ = imp::IdentityVerification::from_instance(self);
let user = self.user();
let client = user.session().client();
let user_id = user.user_id().clone();
let main_sender = priv_.main_sender.get().unwrap().clone();
let flow_id = self.flow_id().clone().unwrap();
self.set_request(None);
let (sync_sender, sync_receiver) = mpsc::channel(100);
priv_.sync_sender.replace(Some(sync_sender));
// TODO add timeout
let handle = spawn_tokio!(async move {
start(client, user_id, flow_id, main_sender, sync_receiver).await
});
let weak_obj = self.downgrade();
spawn!(async move {
let result = handle.await.unwrap();
if let Some(obj) = weak_obj.upgrade() {
let priv_ = imp::IdentityVerification::from_instance(&obj);
match result {
Ok(result) => obj.set_mode(result),
Err(error) => error!("Verification failed: {}", error),
}
priv_.sync_sender.take();
}
});
}
pub fn user(&self) -> User {
let priv_ = imp::IdentityVerification::from_instance(self);
priv_.user.get().unwrap().upgrade().unwrap()
@ -220,7 +268,7 @@ impl IdentityVerification {
/// Start an interactive identity verification
/// Already in progress verifications are cancelled before starting a new one
pub async fn start(&self) -> Result<(), RequestVerificationError> {
pub fn start(&self) {
let priv_ = imp::IdentityVerification::from_instance(self);
let user = self.user();
let client = user.session().client();
@ -235,15 +283,41 @@ impl IdentityVerification {
// TODO add timeout
let result =
spawn_tokio!(async move { start(client, user_id, main_sender, sync_receiver).await })
.await
.unwrap()?;
priv_.sync_sender.take();
let handle = spawn_tokio!(async move {
let identity = if let Some(identity) =
client.get_user_identity(&user_id).await.map_err(|error| {
RequestVerificationError::Sdk(MatrixError::CryptoStoreError(error))
})? {
identity
} else {
return Ok(Mode::IdentityNotFound);
};
self.set_mode(result);
Ok(())
let request = identity
.request_verification_with_methods(vec![
VerificationMethod::SasV1,
VerificationMethod::QrCodeScanV1,
VerificationMethod::QrCodeShowV1,
VerificationMethod::ReciprocateV1,
])
.await?;
let flow_id = request.flow_id().to_owned();
start(client, user_id, flow_id, main_sender, sync_receiver).await
});
let weak_obj = self.downgrade();
spawn!(async move {
let result = handle.await.unwrap();
if let Some(obj) = weak_obj.upgrade() {
let priv_ = imp::IdentityVerification::from_instance(&obj);
match result {
Ok(result) => obj.set_mode(result),
Err(error) => error!("Verification failed: {}", error),
}
priv_.sync_sender.take();
}
});
}
pub fn emoji_match(&self) {
@ -281,6 +355,19 @@ impl IdentityVerification {
return;
}
match mode {
Mode::SasV1 => {
if self.emoji().is_none() {
warn!("Failed to get emoji for SasVerification");
self.show_error();
}
}
Mode::Unavailable | Mode::Cancelled => {
self.show_error();
}
_ => {}
}
priv_.mode.set(mode);
self.notify("mode");
}
@ -291,11 +378,60 @@ impl IdentityVerification {
priv_.request.replace(request);
}
fn show_error(&self) {
self.set_mode(Mode::Error);
let error_message = if let Some(info) = self.cancel_info() {
match info.cancel_code() {
CancelCode::User => Some(gettext("You cancelled the verificaiton process.")),
CancelCode::Timeout => Some(gettext(
"The verification process failed because it reached a timeout.",
)),
CancelCode::Accepted => {
Some(gettext("You accepted the request from an other session."))
}
_ => match info.cancel_code().as_str() {
"m.mismatched_sas" => Some(gettext("The emoji did not match.")),
_ => None,
},
}
} else {
None
};
let error_message = error_message.unwrap_or_else(|| {
gettext("An unknown error occured during the verification process.")
});
let error = Error::new(move |_| {
let error_label = gtk::LabelBuilder::new()
.label(&error_message)
.wrap(true)
.build();
Some(error_label.upcast())
});
if let Some(window) = self.user().session().parent_window() {
window.append_error(&error);
}
}
pub fn display_name(&self) -> String {
// TODO: give this request a name based on the device
"Request".to_string()
}
pub fn flow_id(&self) -> Option<String> {
let priv_ = imp::IdentityVerification::from_instance(self);
priv_.flow_id.borrow().clone()
}
pub fn set_flow_id(&self, flow_id: Option<String>) {
let priv_ = imp::IdentityVerification::from_instance(self);
priv_.flow_id.replace(flow_id);
self.notify("flow-id");
}
/// Get the QrCode for this verification request
///
/// This is only set once the request reached the `State::Ready`
@ -334,6 +470,18 @@ impl IdentityVerification {
}
}
pub fn scanned_qr_code(&self, data: QrVerificationData) {
let priv_ = imp::IdentityVerification::from_instance(self);
if let Some(sync_sender) = &*priv_.sync_sender.borrow() {
let result = sync_sender.try_send(Message::UserAction(UserAction::Scanned(data)));
if let Err(error) = result {
error!("Failed to send message to tokio runtime: {}", error);
}
}
}
pub fn cancel(&self) {
let priv_ = imp::IdentityVerification::from_instance(self);
if let Some(sync_sender) = &*priv_.sync_sender.borrow() {
@ -344,6 +492,10 @@ impl IdentityVerification {
}
}
pub fn dismiss(&self) {
self.set_mode(Mode::Dismissed);
}
/// Get information about why the request was cancelled
pub fn cancel_info(&self) -> Option<CancelInfo> {
let priv_ = imp::IdentityVerification::from_instance(self);
@ -394,62 +546,115 @@ impl IdentityVerification {
async fn start(
client: Client,
user_id: UserId,
main_sender: glib::SyncSender<Verification>,
flow_id: String,
main_sender: glib::SyncSender<(Verification, Mode)>,
mut sync_receiver: mpsc::Receiver<Message>,
) -> Result<Mode, RequestVerificationError> {
let identity = if let Some(identity) = client
.get_user_identity(&user_id)
.await
.map_err(|error| RequestVerificationError::Sdk(MatrixError::CryptoStoreError(error)))?
{
identity
} else {
return Ok(Mode::IdentityNotFound);
};
let request = identity.request_verification().await?;
let flow_id = request.flow_id();
let request =
if let Some(verification) = client.get_verification_request(&user_id, &flow_id).await {
verification
} else {
return Ok(Mode::Unavailable);
};
let result = main_sender.send(Verification::Request(request.clone()));
let result = main_sender.send((Verification::Request(request.clone()), Mode::Requested));
if let Err(error) = result {
error!("Failed to send message to the main context: {}", error);
}
if wait_for_state(flow_id, State::Ready, &mut sync_receiver).await {
request.cancel().await?;
return Ok(Mode::Cancelled);
if !request.we_started() {
request
.accept_with_methods(vec![
VerificationMethod::SasV1,
VerificationMethod::QrCodeScanV1,
VerificationMethod::QrCodeShowV1,
VerificationMethod::ReciprocateV1,
])
.await?;
} else {
if wait_for_state(&flow_id, State::Ready, &mut sync_receiver).await {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
}
let qr_verification = request
.generate_qr_code()
.await
.map_err(|error| RequestVerificationError::Sdk(error))?;
let supports_qr_code_scanning = request.their_supported_methods().map_or(false, |methods| {
methods
.iter()
.any(|method| method == &VerificationMethod::QrCodeScanV1)
});
let qr_verification = if supports_qr_code_scanning {
request
.generate_qr_code()
.await
.map_err(|error| RequestVerificationError::Sdk(error))?
} else {
None
};
let start_sas = if let Some(qr_verification) = qr_verification {
let result = main_sender.send(Verification::QrV1(qr_verification));
let result = main_sender.send((Verification::QrV1(qr_verification), Mode::QrV1Show));
if let Err(error) = result {
error!("Failed to send message to the main context: {}", error);
}
let (start_sas, cancel) = loop {
loop {
match sync_receiver.recv().await.unwrap() {
Message::Sync((id, State::Start)) if flow_id == &id => break (false, false),
Message::Sync((id, State::Cancel)) if flow_id == &id => break (false, true),
Message::UserAction(UserAction::Cancel) => break (false, true),
Message::UserAction(UserAction::StartSas) => break (true, false),
Message::Sync((id, State::Start)) if flow_id == id => break false,
Message::Sync((id, State::Cancel)) if flow_id == id => {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
Message::UserAction(UserAction::Cancel) => {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
Message::UserAction(UserAction::StartSas) => break true,
Message::UserAction(UserAction::Scanned(data)) => {
request.scan_qr_code(data).await?;
break false;
}
_ => {}
}
};
if cancel {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
start_sas
} else {
true
let supports_qr_code_showing = request.their_supported_methods().map_or(false, |methods| {
methods
.iter()
.any(|method| method == &VerificationMethod::QrCodeShowV1)
});
if supports_qr_code_showing {
let result = main_sender.send((Verification::Request(request.clone()), Mode::QrV1Scan));
if let Err(error) = result {
error!("Failed to send message to the main context: {}", error);
}
loop {
match sync_receiver.recv().await.unwrap() {
Message::Sync((id, State::Start)) if flow_id == id => break false,
Message::Sync((id, State::Cancel)) if flow_id == id => {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
Message::UserAction(UserAction::Cancel) => {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
Message::UserAction(UserAction::StartSas) => break true,
Message::UserAction(UserAction::Scanned(data)) => {
request.scan_qr_code(data).await?;
break false;
}
_ => {}
}
}
} else {
true
}
};
if start_sas {
@ -461,9 +666,9 @@ async fn start(
{
let cancel = loop {
match sync_receiver.recv().await {
Some(Message::Sync((id, State::Start))) if flow_id == &id => break false,
Some(Message::Sync((id, State::Accept))) if flow_id == &id => break false,
Some(Message::Sync((id, State::Cancel))) if flow_id == &id => break true,
Some(Message::Sync((id, State::Start))) if flow_id == id => break false,
Some(Message::Sync((id, State::Accept))) if flow_id == id => break false,
Some(Message::Sync((id, State::Cancel))) if flow_id == id => break true,
Some(Message::UserAction(UserAction::Cancel)) => break true,
None => break true,
_ => {}
@ -491,7 +696,7 @@ async fn start(
MatrixVerification::QrV1(qr_verification) => {
qr_verification.confirm().await?;
if wait_for_state(flow_id, State::Done, &mut sync_receiver).await {
if wait_for_state(&flow_id, State::Done, &mut sync_receiver).await {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
@ -499,25 +704,26 @@ async fn start(
MatrixVerification::SasV1(sas_verification) => {
sas_verification.accept().await?;
if wait_for_state(flow_id, State::Key, &mut sync_receiver).await {
if wait_for_state(&flow_id, State::Key, &mut sync_receiver).await {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
let result = main_sender.send(Verification::SasV1(sas_verification.clone()));
let result =
main_sender.send((Verification::SasV1(sas_verification.clone()), Mode::SasV1));
if let Err(error) = result {
error!("Failed to send message to the main context: {}", error);
}
if wait_for_match_action(flow_id, &mut sync_receiver).await {
if wait_for_match_action(&flow_id, &mut sync_receiver).await {
request.cancel().await?;
return Ok(Mode::Cancelled);
}
sas_verification.confirm().await?;
if wait_for_state(flow_id, State::Done, &mut sync_receiver).await {
if wait_for_state(&flow_id, State::Done, &mut sync_receiver).await {
request.cancel().await?;
return Ok(Mode::Cancelled);
}

144
src/session/verification/session_verification.rs

@ -4,14 +4,16 @@ use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate
use log::{debug, error, warn};
use crate::components::{AuthDialog, SpinnerButton};
use crate::contrib::screenshot;
use crate::contrib::QRCode;
use crate::contrib::QRCodeExt;
use crate::contrib::QrCodeScanner;
use crate::session::verification::{Emoji, IdentityVerification, VerificationMode};
use crate::session::Session;
use crate::spawn;
use crate::Error;
use crate::Window;
use matrix_sdk::ruma::events::key::verification::cancel::CancelCode;
use matrix_sdk::encryption::verification::QrVerificationData;
mod imp {
use super::*;
@ -24,7 +26,7 @@ mod imp {
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/session-verification.ui")]
pub struct SessionVerification {
pub request: OnceCell<WeakRef<IdentityVerification>>,
pub request: OnceCell<IdentityVerification>,
pub session: OnceCell<WeakRef<Session>>,
#[template_child]
pub bootstrap_button: TemplateChild<SpinnerButton>,
@ -41,7 +43,17 @@ mod imp {
#[template_child]
pub start_emoji_btn: TemplateChild<SpinnerButton>,
#[template_child]
pub start_emoji_btn2: TemplateChild<SpinnerButton>,
#[template_child]
pub start_emoji_btn3: TemplateChild<SpinnerButton>,
#[template_child]
pub take_screenshot_btn2: TemplateChild<SpinnerButton>,
#[template_child]
pub take_screenshot_btn3: TemplateChild<SpinnerButton>,
#[template_child]
pub main_stack: TemplateChild<gtk::Stack>,
#[template_child]
pub qr_code_scanner: TemplateChild<QrCodeScanner>,
pub mode_handler: RefCell<Option<SignalHandlerId>>,
}
@ -145,11 +157,42 @@ mod imp {
obj.request().start_sas();
}));
self.start_emoji_btn2
.connect_clicked(clone!(@weak obj => move |button| {
let priv_ = imp::SessionVerification::from_instance(&obj);
button.set_loading(true);
priv_.take_screenshot_btn2.set_sensitive(false);
obj.request().start_sas();
}));
self.start_emoji_btn3
.connect_clicked(clone!(@weak obj => move |button| {
let priv_ = imp::SessionVerification::from_instance(&obj);
button.set_loading(true);
priv_.take_screenshot_btn3.set_sensitive(false);
obj.request().start_sas();
}));
self.bootstrap_button
.connect_clicked(clone!(@weak obj => move |button| {
button.set_loading(true);
obj.bootstrap_cross_signing();
}));
self.take_screenshot_btn2
.connect_clicked(clone!(@weak obj => move |button| {
let priv_ = imp::SessionVerification::from_instance(&obj);
button.set_loading(true);
priv_.start_emoji_btn2.set_sensitive(false);
obj.take_screenshot();
}));
self.take_screenshot_btn3
.connect_clicked(clone!(@weak obj => move |button| {
let priv_ = imp::SessionVerification::from_instance(&obj);
button.set_loading(true);
priv_.start_emoji_btn3.set_sensitive(false);
obj.take_screenshot();
}));
}
fn dispose(&self, obj: &Self::Type) {
@ -172,15 +215,15 @@ impl SessionVerification {
.expect("Failed to create SessionVerification")
}
pub fn request(&self) -> IdentityVerification {
pub fn request(&self) -> &IdentityVerification {
let priv_ = imp::SessionVerification::from_instance(self);
priv_.request.get().unwrap().upgrade().unwrap()
priv_.request.get().unwrap()
}
fn set_request(&self, request: IdentityVerification) {
let priv_ = imp::SessionVerification::from_instance(self);
priv_.request.set(request.downgrade()).unwrap()
priv_.request.set(request).unwrap();
}
pub fn session(&self) -> Session {
@ -208,6 +251,12 @@ impl SessionVerification {
priv_.start_emoji_btn.set_loading(false);
priv_.start_emoji_btn.set_sensitive(true);
priv_.bootstrap_button.set_loading(false);
priv_.start_emoji_btn2.set_loading(false);
priv_.start_emoji_btn2.set_sensitive(true);
priv_.take_screenshot_btn2.set_loading(false);
priv_.take_screenshot_btn2.set_sensitive(true);
priv_.take_screenshot_btn3.set_loading(false);
priv_.take_screenshot_btn3.set_sensitive(true);
while let Some(child) = priv_.emoji_row_1.first_child() {
priv_.emoji_row_1.remove(&child);
@ -233,15 +282,7 @@ impl SessionVerification {
priv_.mode_handler.replace(Some(handler));
let weak_obj = self.downgrade();
spawn!(clone!(@weak request => async move {
if let Err(error) = request.start().await {
if let Some(obj) = weak_obj.upgrade() {
obj.show_error();
}
error!("Verification failed: {}", error);
}
}));
request.start();
}
/// Cancel the verification request without telling the user about it
@ -267,7 +308,7 @@ impl SessionVerification {
VerificationMode::Requested => {
priv_.main_stack.set_visible_child_name("wait-for-device");
}
VerificationMode::QrV1 => {
VerificationMode::QrV1Show => {
if let Some(qrcode) = request.qr_code() {
priv_.qrcode.set_qrcode(qrcode);
priv_.main_stack.set_visible_child_name("qrcode");
@ -276,6 +317,9 @@ impl SessionVerification {
request.start_sas();
}
}
VerificationMode::QrV1Scan => {
self.start_scanning();
}
VerificationMode::SasV1 => {
// TODO: implement sas fallback when emojis arn't supported
if let Some(emoji) = request.emoji() {
@ -287,56 +331,44 @@ impl SessionVerification {
}
}
priv_.main_stack.set_visible_child_name("emoji");
} else {
warn!("Failed to get emoji for SasVerification");
self.show_error();
}
}
VerificationMode::Unavailable => {
self.show_error();
}
VerificationMode::Completed => {
priv_.main_stack.set_visible_child_name("completed");
}
VerificationMode::Cancelled => {
self.show_error();
_ => {
warn!("Try to show a dismissed verification");
}
}
}
fn show_error(&self) {
let error_message = if let Some(info) = self.request().cancel_info() {
match info.cancel_code() {
CancelCode::User => Some(gettext("You cancelled the verificaiton process.")),
CancelCode::Timeout => Some(gettext(
"The verification process failed because it reached a timeout.",
)),
_ => match info.cancel_code().as_str() {
"m.mismatched_sas" => Some(gettext("The emoji did not match.")),
_ => None,
},
fn start_scanning(&self) {
spawn!(clone!(@weak self as obj => async move {
let priv_ = imp::SessionVerification::from_instance(&obj);
if priv_.qr_code_scanner.start().await {
priv_.main_stack.set_visible_child_name("scan-qr-code");
} else {
priv_.main_stack.set_visible_child_name("no-camera");
}
} else {
None
};
let error_message = error_message.unwrap_or_else(|| {
gettext("An unknown error occured during the verification process.")
});
let error = Error::new(move |_| {
let error_label = gtk::LabelBuilder::new()
.label(&error_message)
.wrap(true)
.build();
Some(error_label.upcast())
});
if let Some(window) = self.session().parent_window() {
window.append_error(&error);
}
self.silent_cancel();
self.start_verification();
}));
}
fn take_screenshot(&self) {
spawn!(clone!(@weak self as obj => async move {
let root = obj.root().unwrap();
if let Some(code) = screenshot::capture(&root).await {
obj.finish_scanning(code);
} else {
obj.reset();
}
}));
}
fn finish_scanning(&self, data: QrVerificationData) {
let priv_ = imp::SessionVerification::from_instance(self);
priv_.qr_code_scanner.stop();
self.request().scanned_qr_code(data);
priv_.main_stack.set_visible_child_name("qr-code-scanned");
}
fn show_recovery(&self) {
@ -362,7 +394,7 @@ impl SessionVerification {
self.silent_cancel();
self.activate_action("session.logout", None);
}
"emoji" | "qrcode" => {
"emoji" | "qrcode" | "scan-qr-code" | "no-camera" => {
self.silent_cancel();
self.start_verification();
}

Loading…
Cancel
Save