fractal/session/model/verification/
verification_list.rs

1use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
2use matrix_sdk::{
3    encryption::verification::VerificationRequest, Client as MatrixClient, Room as MatrixRoom,
4};
5use ruma::{
6    events::{
7        key::verification::request::ToDeviceKeyVerificationRequestEvent,
8        room::message::{MessageType, OriginalSyncRoomMessageEvent},
9    },
10    RoomId,
11};
12use tracing::{debug, error};
13
14use super::{load_supported_verification_methods, VerificationKey, VerificationState};
15use crate::{
16    session::model::{IdentityVerification, Member, Membership, Session, User},
17    spawn, spawn_tokio,
18};
19
20mod imp {
21    use std::{cell::RefCell, sync::LazyLock};
22
23    use glib::subclass::Signal;
24    use indexmap::IndexMap;
25
26    use super::*;
27
28    #[derive(Debug, Default, glib::Properties)]
29    #[properties(wrapper_type = super::VerificationList)]
30    pub struct VerificationList {
31        /// The ongoing verification requests.
32        pub(super) list: RefCell<IndexMap<VerificationKey, IdentityVerification>>,
33        /// The current session.
34        #[property(get, construct_only)]
35        session: glib::WeakRef<Session>,
36    }
37
38    #[glib::object_subclass]
39    impl ObjectSubclass for VerificationList {
40        const NAME: &'static str = "VerificationList";
41        type Type = super::VerificationList;
42        type Interfaces = (gio::ListModel,);
43    }
44
45    #[glib::derived_properties]
46    impl ObjectImpl for VerificationList {
47        fn signals() -> &'static [Signal] {
48            static SIGNALS: LazyLock<Vec<Signal>> =
49                LazyLock::new(|| vec![Signal::builder("secret-received").build()]);
50            SIGNALS.as_ref()
51        }
52    }
53
54    impl ListModelImpl for VerificationList {
55        fn item_type(&self) -> glib::Type {
56            IdentityVerification::static_type()
57        }
58
59        fn n_items(&self) -> u32 {
60            self.list.borrow().len() as u32
61        }
62
63        fn item(&self, position: u32) -> Option<glib::Object> {
64            self.list
65                .borrow()
66                .get_index(position as usize)
67                .map(|(_, item)| item.clone().upcast())
68        }
69    }
70
71    impl VerificationList {
72        /// Add a verification received via a to-device event.
73        pub(super) async fn add_to_device_request(&self, request: VerificationRequest) {
74            if request.is_done() || request.is_cancelled() || request.is_passive() {
75                // Ignore requests that are already finished.
76                return;
77            }
78
79            let Some(session) = self.session.upgrade() else {
80                return;
81            };
82
83            let verification = IdentityVerification::new(request, &session.user(), None).await;
84            self.add(verification.clone());
85
86            if verification.state() == VerificationState::Requested {
87                session
88                    .notifications()
89                    .show_to_device_identity_verification(&verification)
90                    .await;
91            }
92        }
93
94        /// Add a verification received via an in-room event.
95        pub(super) async fn add_in_room_request(
96            &self,
97            request: VerificationRequest,
98            room_id: &RoomId,
99        ) {
100            if request.is_done() || request.is_cancelled() || request.is_passive() {
101                // Ignore requests that are already finished.
102                return;
103            }
104
105            let Some(session) = self.session.upgrade() else {
106                return;
107            };
108            let Some(room) = session.room_list().get(room_id) else {
109                error!(
110                    "Room for verification request `({}, {})` not found",
111                    request.other_user_id(),
112                    request.flow_id()
113                );
114                return;
115            };
116
117            if matches!(
118                room.own_member().membership(),
119                Membership::Leave | Membership::Ban
120            ) {
121                // Ignore requests where the user is not in the room anymore.
122                return;
123            }
124
125            let other_user_id = request.other_user_id().to_owned();
126            let member = room.members().map_or_else(
127                || Member::new(&room, other_user_id.clone()),
128                |l| l.get_or_create(other_user_id.clone()),
129            );
130
131            // Ensure the member is up-to-date.
132            let matrix_room = room.matrix_room().clone();
133            let handle =
134                spawn_tokio!(async move { matrix_room.get_member_no_sync(&other_user_id).await });
135            match handle.await.expect("task was not aborted") {
136                Ok(Some(matrix_member)) => member.update_from_room_member(&matrix_member),
137                Ok(None) => {
138                    error!(
139                        "Room member for verification request `({}, {})` not found",
140                        request.other_user_id(),
141                        request.flow_id()
142                    );
143                    return;
144                }
145                Err(error) => {
146                    error!(
147                        "Could not get room member for verification request `({}, {})`: {error}",
148                        request.other_user_id(),
149                        request.flow_id()
150                    );
151                    return;
152                }
153            }
154
155            let verification =
156                IdentityVerification::new(request, member.upcast_ref(), Some(&room)).await;
157
158            room.set_verification(Some(&verification));
159
160            self.add(verification.clone());
161
162            if verification.state() == VerificationState::Requested {
163                session
164                    .notifications()
165                    .show_in_room_identity_verification(&verification)
166                    .await;
167            }
168        }
169
170        /// Add the given verification to the list.
171        pub(super) fn add(&self, verification: IdentityVerification) {
172            let key = verification.key();
173
174            // Don't add request that already exists.
175            if self.list.borrow().contains_key(&key) {
176                return;
177            }
178
179            let obj = self.obj();
180            verification.connect_remove_from_list(clone!(
181                #[weak]
182                obj,
183                move |verification| {
184                    obj.remove(&verification.key());
185                }
186            ));
187
188            let (pos, _) = self.list.borrow_mut().insert_full(key, verification);
189
190            obj.items_changed(pos as u32, 0, 1);
191        }
192    }
193}
194
195glib::wrapper! {
196    /// The list of ongoing verification requests.
197    pub struct VerificationList(ObjectSubclass<imp::VerificationList>)
198        @implements gio::ListModel;
199}
200
201impl VerificationList {
202    /// Construct a new `VerificationList` with the given session.
203    pub fn new(session: &Session) -> Self {
204        glib::Object::builder().property("session", session).build()
205    }
206
207    /// Initialize this list to listen to new verification requests.
208    pub(crate) fn init(&self) {
209        let Some(session) = self.session() else {
210            return;
211        };
212
213        let client = session.client();
214        let obj_weak = glib::SendWeakRef::from(self.downgrade());
215
216        let obj_weak_clone = obj_weak.clone();
217        client.add_event_handler(
218            move |ev: ToDeviceKeyVerificationRequestEvent, client: MatrixClient| {
219                let obj_weak = obj_weak_clone.clone();
220                async move {
221                    let Some(request) = client
222                        .encryption()
223                        .get_verification_request(&ev.sender, &ev.content.transaction_id)
224                        .await
225                    else {
226                        // This might be normal if the request has already timed out.
227                        debug!(
228                            "To-device verification request `({}, {})` not found in the SDK",
229                            ev.sender, ev.content.transaction_id
230                        );
231                        return;
232                    };
233
234                    if !request.is_self_verification() {
235                        // We only support in-room verifications for other users.
236                        debug!(
237                            "To-device verification request `({}, {})` for other users is not supported",
238                            ev.sender, ev.content.transaction_id
239                        );
240                        return;
241                    }
242
243                    let ctx = glib::MainContext::default();
244                    ctx.spawn(async move {
245                        spawn!(async move {
246                            if let Some(obj) = obj_weak.upgrade() {
247                                obj.imp().add_to_device_request(request).await;
248                            }
249                        });
250                    });
251                }
252            },
253        );
254
255        client.add_event_handler(
256            move |ev: OriginalSyncRoomMessageEvent, room: MatrixRoom, client: MatrixClient| {
257                let obj_weak = obj_weak.clone();
258                async move {
259                    let MessageType::VerificationRequest(_) = &ev.content.msgtype else {
260                        return;
261                    };
262                    let Some(request) = client
263                        .encryption()
264                        .get_verification_request(&ev.sender, &ev.event_id)
265                        .await
266                    else {
267                        // This might be normal if the request has already timed out.
268                        debug!(
269                            "To-device verification request `({}, {})` not found in the SDK",
270                            ev.sender, ev.event_id
271                        );
272                        return;
273                    };
274                    let room_id = room.room_id().to_owned();
275
276                    let ctx = glib::MainContext::default();
277                    ctx.spawn(async move {
278                        spawn!(async move {
279                            if let Some(obj) = obj_weak.upgrade() {
280                                obj.imp().add_in_room_request(request, &room_id).await;
281                            }
282                        });
283                    });
284                }
285            },
286        );
287    }
288
289    /// Remove the verification with the given key.
290    pub(crate) fn remove(&self, key: &VerificationKey) {
291        let Some((pos, ..)) = self.imp().list.borrow_mut().shift_remove_full(key) else {
292            return;
293        };
294
295        self.items_changed(pos as u32, 1, 0);
296
297        if let Some(session) = self.session() {
298            session.notifications().withdraw_identity_verification(key);
299        }
300    }
301
302    /// Get the verification with the given key.
303    pub(crate) fn get(&self, key: &VerificationKey) -> Option<IdentityVerification> {
304        self.imp().list.borrow().get(key).cloned()
305    }
306
307    // Returns the ongoing session verification, if any.
308    pub(crate) fn ongoing_session_verification(&self) -> Option<IdentityVerification> {
309        let list = self.imp().list.borrow();
310        list.values()
311            .find(|v| v.is_self_verification() && !v.is_finished())
312            .cloned()
313    }
314
315    // Returns the ongoing verification in the given room, if any.
316    pub(crate) fn ongoing_room_verification(
317        &self,
318        room_id: &RoomId,
319    ) -> Option<IdentityVerification> {
320        let list = self.imp().list.borrow();
321        list.values()
322            .find(|v| v.room().is_some_and(|room| room.room_id() == room_id) && !v.is_finished())
323            .cloned()
324    }
325
326    /// Create and send a new verification request.
327    ///
328    /// If `user` is `None`, a new session verification is started for our own
329    /// user and sent to other devices.
330    pub(crate) async fn create(&self, user: Option<User>) -> Result<IdentityVerification, ()> {
331        let Some(session) = self.session() else {
332            error!("Could not create identity verification: failed to upgrade session");
333            return Err(());
334        };
335
336        let user = user.unwrap_or_else(|| session.user());
337
338        let supported_methods = load_supported_verification_methods().await;
339
340        let Some(identity) = user.ensure_crypto_identity().await else {
341            error!("Could not create identity verification: cryptographic identity not found");
342            return Err(());
343        };
344
345        let handle = spawn_tokio!(async move {
346            identity
347                .request_verification_with_methods(supported_methods)
348                .await
349        });
350
351        match handle.await.expect("task was not aborted") {
352            Ok(request) => {
353                let room = if let Some(room_id) = request.room_id() {
354                    let Some(room) = session.room_list().get(room_id) else {
355                        error!(
356                            "Room for verification request `({}, {})` not found",
357                            request.other_user_id(),
358                            request.flow_id()
359                        );
360                        return Err(());
361                    };
362                    Some(room)
363                } else {
364                    None
365                };
366
367                let verification = IdentityVerification::new(request, &user, room.as_ref()).await;
368                self.imp().add(verification.clone());
369
370                Ok(verification)
371            }
372            Err(error) => {
373                error!("Could not create identity verification: {error}");
374                Err(())
375            }
376        }
377    }
378}