diff --git a/src/session/content/room_history/message_row/mod.rs b/src/session/content/room_history/message_row/mod.rs index 74f72bb3..864a97e7 100644 --- a/src/session/content/room_history/message_row/mod.rs +++ b/src/session/content/room_history/message_row/mod.rs @@ -412,7 +412,7 @@ fn build_content(parent: &adw::Bin, event: &Event, compact: bool) { parent.set_child(Some(&child)); child }; - child.text(gettext("Fractal couldn’t decrypt this message.")); + child.text(gettext("Fractal couldn’t decrypt this message, but will retry once the keys are available.")); } Some(AnyMessageLikeEventContent::RoomRedaction(_)) => { let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::()) diff --git a/src/session/mod.rs b/src/session/mod.rs index 8de11952..0f748fe6 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -26,6 +26,7 @@ use log::{debug, error, warn}; use matrix_sdk::{ config::{RequestConfig, SyncSettings}, deserialized_responses::SyncResponse, + room::Room as MatrixRoom, ruma::{ api::{ client::{ @@ -36,7 +37,10 @@ use matrix_sdk::{ error::{FromHttpResponseError, ServerError}, }, assign, - events::{direct::DirectEventContent, GlobalAccountDataEvent}, + events::{ + direct::DirectEventContent, room::encryption::SyncRoomEncryptionEvent, + GlobalAccountDataEvent, + }, RoomId, }, store::{make_store_config, OpenStoreError}, @@ -476,6 +480,7 @@ impl Session { self.room_list().load(); self.setup_direct_room_handler(); + self.setup_room_encrypted_changes(); self.sync(); @@ -881,6 +886,32 @@ impl Session { }) ); } + + fn setup_room_encrypted_changes(&self) { + let session_weak = glib::SendWeakRef::from(self.downgrade()); + let client = self.client(); + spawn_tokio!(async move { + client + .register_event_handler( + move |_: SyncRoomEncryptionEvent, matrix_room: MatrixRoom| { + let session_weak = session_weak.clone(); + async move { + let ctx = glib::MainContext::default(); + ctx.spawn(async move { + if let Some(session) = session_weak.upgrade() { + if let Some(room) = + session.room_list().get(matrix_room.room_id()) + { + room.set_is_encrypted(true); + } + } + }); + } + }, + ) + .await; + }); + } } impl Default for Session { diff --git a/src/session/room/event.rs b/src/session/room/event.rs index 8f46970b..79919570 100644 --- a/src/session/room/event.rs +++ b/src/session/room/event.rs @@ -11,11 +11,13 @@ use matrix_sdk::{ ruma::{ events::{ room::{ + encrypted::RoomEncryptedEventContent, message::{MessageType, Relation}, redaction::SyncRoomRedactionEvent, }, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncRoomEvent, - AnySyncStateEvent, MessageLikeUnsigned, SyncMessageLikeEvent, SyncStateEvent, + AnySyncStateEvent, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, + SyncMessageLikeEvent, SyncStateEvent, }, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, }, @@ -27,7 +29,7 @@ use super::{ Member, ReactionList, Room, }; use crate::{ - spawn_tokio, + spawn, spawn_tokio, utils::{filename_for_mime, media_type_uid}, }; @@ -54,6 +56,7 @@ mod imp { pub replacing_events: RefCell>, pub reactions: ReactionList, pub source_changed_handler: RefCell>, + pub keys_handle: RefCell>, pub room: OnceCell>, } @@ -221,6 +224,16 @@ impl Event { let priv_ = self.imp(); if let Ok(deserialized) = event.event.deserialize() { + if let AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted( + SyncMessageLikeEvent::Original(ref encrypted), + )) = deserialized + { + let encrypted = encrypted.to_owned(); + spawn!(clone!(@weak self as obj => async move { + obj.try_to_decrypt(encrypted).await; + })); + } + priv_.event.replace(Some(deserialized)); } else { warn!("Failed to deserialize event: {:?}", event); @@ -230,6 +243,35 @@ impl Event { self.notify("event"); self.notify("activatable"); + self.notify("source"); + } + + async fn try_to_decrypt(&self, event: OriginalSyncMessageLikeEvent) { + let priv_ = self.imp(); + let room = self.room().matrix_room(); + let handle = spawn_tokio!(async move { room.decrypt_event(&event).await }); + + match handle.await.unwrap() { + Ok(decrypted) => { + if let Some(keys_handle) = priv_.keys_handle.take() { + self.room().disconnect(keys_handle); + } + self.set_matrix_pure_event(decrypted.into()); + } + Err(error) => { + warn!("Failed to decrypt event: {}", error); + if priv_.keys_handle.borrow().is_none() { + let handle = self.room().connect_new_encryption_keys( + clone!(@weak self as obj => move |_| { + // Try to decrypt the event again + obj.set_matrix_pure_event(obj.matrix_pure_event()); + }), + ); + + priv_.keys_handle.replace(Some(handle)); + } + } + } } pub fn matrix_sender(&self) -> OwnedUserId { diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs index 9db979fe..b6175895 100644 --- a/src/session/room/mod.rs +++ b/src/session/room/mod.rs @@ -31,11 +31,12 @@ use matrix_sdk::{ redaction::{OriginalSyncRoomRedactionEvent, RoomRedactionEventContent}, topic::RoomTopicEventContent, }, + room_key::ToDeviceRoomKeyEventContent, tag::{TagInfo, TagName}, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncMessageLikeEvent, AnySyncRoomEvent, AnySyncStateEvent, EventContent, MessageLikeEventType, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, StateEventType, - SyncMessageLikeEvent, SyncStateEvent, + SyncMessageLikeEvent, SyncStateEvent, ToDeviceEvent, }, receipt::ReceiptType, serde::Raw, @@ -110,6 +111,8 @@ mod imp { pub successor: OnceCell, /// The most recent verification request event. pub verification: RefCell>, + /// Whether this room is encrypted + pub is_encrypted: Cell, } #[glib::object_subclass] @@ -241,6 +244,13 @@ mod imp { IdentityVerification::static_type(), glib::ParamFlags::READWRITE, ), + glib::ParamSpecBoolean::new( + "encrypted", + "Encrypted", + "Whether this room is encrypted", + false, + glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, + ), ] }); @@ -276,6 +286,7 @@ mod imp { obj.store_topic(topic); } "verification" => obj.set_verification(value.get().unwrap()), + "encrypted" => obj.set_is_encrypted(value.get().unwrap()), _ => unimplemented!(), } } @@ -310,6 +321,7 @@ mod imp { |id| id.as_ref().to_value(), ), "verification" => obj.verification().to_value(), + "encrypted" => obj.is_encrypted().to_value(), _ => unimplemented!(), } } @@ -319,6 +331,7 @@ mod imp { vec![ Signal::builder("order-changed", &[], <()>::static_type().into()).build(), Signal::builder("room-forgotten", &[], <()>::static_type().into()).build(), + Signal::builder("new-encryption-keys", &[], <()>::static_type().into()).build(), ] }); SIGNALS.as_ref() @@ -338,6 +351,7 @@ mod imp { .unwrap(); obj.load_power_levels(); + obj.setup_is_encrypted(); obj.bind_property("display-name", obj.avatar(), "display-name") .flags(glib::BindingFlags::SYNC_CREATE) @@ -1637,6 +1651,75 @@ impl Room { self.set_latest_unread(latest_unread); } + + pub fn is_encrypted(&self) -> bool { + self.imp().is_encrypted.get() + } + + pub fn set_is_encrypted(&self, is_encrypted: bool) { + let was_encrypted = self.is_encrypted(); + if was_encrypted == is_encrypted { + return; + } + + if was_encrypted && !is_encrypted { + error!("Encryption for a room can't be disabled"); + return; + } + + if self.matrix_room().is_encrypted() != is_encrypted { + // TODO: enable encryption if it isn't enabled yet + } + + self.setup_is_encrypted(); + } + + fn setup_is_encrypted(&self) { + if !self.matrix_room().is_encrypted() { + return; + } + self.setup_new_encryption_keys_handler(); + self.imp().is_encrypted.set(true); + self.notify("encrypted"); + } + + fn setup_new_encryption_keys_handler(&self) { + spawn!( + glib::PRIORITY_DEFAULT_IDLE, + clone!(@weak self as obj => async move { + let obj_weak = glib::SendWeakRef::from(obj.downgrade()); + obj.session().client().register_event_handler( + move |event: ToDeviceEvent| { + let obj_weak = obj_weak.clone(); + async move { + let ctx = glib::MainContext::default(); + ctx.spawn(async move { + if let Some(room) = obj_weak.upgrade() { + if room.room_id() == event.content.room_id { + room.emit_by_name::<()>("new-encryption-keys", &[]); + } + } + }); + } + }, + ) + .await; + }) + ); + } + + pub fn connect_new_encryption_keys( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_local("new-encryption-keys", true, move |values| { + let obj = values[0].get::().unwrap(); + + f(&obj); + + None + }) + } } /// Whether the given event can count as an unread message.