fractal/session/model/
user.rs

1use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
2use matrix_sdk::encryption::identities::UserIdentity;
3use ruma::{MatrixToUri, OwnedMxcUri, OwnedUserId};
4use tracing::{debug, error};
5
6use super::{IdentityVerification, Room, Session};
7use crate::{
8    components::{AvatarImage, AvatarUriSource, PillSource},
9    prelude::*,
10    spawn, spawn_tokio,
11};
12
13#[glib::flags(name = "UserActions")]
14pub enum UserActions {
15    VERIFY = 0b0000_0001,
16}
17
18impl Default for UserActions {
19    fn default() -> Self {
20        Self::empty()
21    }
22}
23
24mod imp {
25    use std::{
26        cell::{Cell, OnceCell, RefCell},
27        marker::PhantomData,
28    };
29
30    use super::*;
31
32    #[derive(Debug, Default, glib::Properties)]
33    #[properties(wrapper_type = super::User)]
34    pub struct User {
35        /// The ID of this user.
36        user_id: OnceCell<OwnedUserId>,
37        /// The ID of this user, as a string.
38        #[property(get = Self::user_id_string)]
39        user_id_string: PhantomData<String>,
40        /// The current session.
41        #[property(get, construct_only)]
42        session: OnceCell<Session>,
43        /// Whether this user is the same as the session's user.
44        #[property(get)]
45        is_own_user: Cell<bool>,
46        /// Whether this user has been verified.
47        #[property(get)]
48        is_verified: Cell<bool>,
49        /// Whether this user is currently ignored.
50        #[property(get)]
51        is_ignored: Cell<bool>,
52        ignored_handler: RefCell<Option<glib::SignalHandlerId>>,
53    }
54
55    #[glib::object_subclass]
56    impl ObjectSubclass for User {
57        const NAME: &'static str = "User";
58        type Type = super::User;
59        type ParentType = PillSource;
60    }
61
62    #[glib::derived_properties]
63    impl ObjectImpl for User {
64        fn constructed(&self) {
65            self.parent_constructed();
66            let obj = self.obj();
67
68            let avatar_image = AvatarImage::new(&obj.session(), AvatarUriSource::User, None, None);
69            obj.avatar_data().set_image(Some(avatar_image));
70        }
71
72        fn dispose(&self) {
73            if let Some(session) = self.session.get() {
74                if let Some(handler) = self.ignored_handler.take() {
75                    session.ignored_users().disconnect(handler);
76                }
77            }
78        }
79    }
80
81    impl PillSourceImpl for User {
82        fn identifier(&self) -> String {
83            self.user_id_string()
84        }
85    }
86
87    impl User {
88        /// The ID of this user.
89        pub(super) fn user_id(&self) -> &OwnedUserId {
90            self.user_id.get().expect("user ID should be initialized")
91        }
92
93        /// The ID of this user, as a string.
94        fn user_id_string(&self) -> String {
95            self.user_id().to_string()
96        }
97
98        /// The current session.
99        fn session(&self) -> &Session {
100            self.session.get().expect("session should be initialized")
101        }
102
103        /// Set the ID of this user.
104        pub(crate) fn set_user_id(&self, user_id: OwnedUserId) {
105            let user_id = self.user_id.get_or_init(|| user_id);
106
107            let obj = self.obj();
108            obj.set_name(None);
109            obj.bind_property("display-name", &obj.avatar_data(), "display-name")
110                .sync_create()
111                .build();
112
113            let session = self.session();
114            self.is_own_user.set(session.user_id() == user_id);
115
116            let ignored_users = session.ignored_users();
117            let ignored_handler = ignored_users.connect_items_changed(clone!(
118                #[weak(rename_to = imp)]
119                self,
120                move |ignored_users, _, _, _| {
121                    let user_id = imp.user_id.get().expect("user ID is initialized");
122                    let is_ignored = ignored_users.contains(user_id);
123
124                    if imp.is_ignored.get() != is_ignored {
125                        imp.is_ignored.set(is_ignored);
126                        imp.obj().notify_is_ignored();
127                    }
128                }
129            ));
130            self.is_ignored.set(ignored_users.contains(user_id));
131            self.ignored_handler.replace(Some(ignored_handler));
132
133            spawn!(clone!(
134                #[weak(rename_to = imp)]
135                self,
136                async move {
137                    imp.init_is_verified().await;
138                }
139            ));
140        }
141
142        /// Get the local cryptographic identity (aka cross-signing identity) of
143        /// this user.
144        ///
145        /// Locally, we should always have the crypto identity of our own user
146        /// and of users with whom we share an encrypted room.
147        pub(super) async fn local_crypto_identity(&self) -> Option<UserIdentity> {
148            let encryption = self.session().client().encryption();
149            let user_id = self.user_id().clone();
150            let handle = spawn_tokio!(async move { encryption.get_user_identity(&user_id).await });
151
152            match handle.await.expect("task was not aborted") {
153                Ok(identity) => identity,
154                Err(error) => {
155                    error!("Could not get local crypto identity: {error}");
156                    None
157                }
158            }
159        }
160
161        /// Load whether this user is verified.
162        async fn init_is_verified(&self) {
163            // If a user is verified, we should have their crypto identity locally.
164            let is_verified = self
165                .local_crypto_identity()
166                .await
167                .is_some_and(|i| i.is_verified());
168
169            if self.is_verified.get() == is_verified {
170                return;
171            }
172
173            self.is_verified.set(is_verified);
174            self.obj().notify_is_verified();
175        }
176
177        /// Create an encrypted direct chat with this user.
178        pub(super) async fn create_direct_chat(&self) -> Result<Room, matrix_sdk::Error> {
179            let user_id = self.user_id().clone();
180            let client = self.session().client();
181            let handle = spawn_tokio!(async move { client.create_dm(&user_id).await });
182
183            match handle.await.expect("task was not aborted") {
184                Ok(matrix_room) => {
185                    let room = self
186                        .session()
187                        .room_list()
188                        .get_wait(matrix_room.room_id(), None)
189                        .await
190                        .expect("The newly created room was not found");
191                    Ok(room)
192                }
193                Err(error) => {
194                    error!("Could not create direct chat: {error}");
195                    Err(error)
196                }
197            }
198        }
199    }
200}
201
202glib::wrapper! {
203    /// `glib::Object` representation of a Matrix user.
204    pub struct User(ObjectSubclass<imp::User>) @extends PillSource;
205}
206
207impl User {
208    /// Constructs a new user with the given user ID for the given session.
209    pub fn new(session: &Session, user_id: OwnedUserId) -> Self {
210        let obj = glib::Object::builder::<Self>()
211            .property("session", session)
212            .build();
213
214        obj.imp().set_user_id(user_id);
215        obj
216    }
217
218    /// Get the cryptographic identity (aka cross-signing identity) of this
219    /// user.
220    ///
221    /// First, we try to get the local crypto identity if we are sure that it is
222    /// up-to-date. If we do not have the crypto identity locally, we request it
223    /// from the homeserver.
224    pub(crate) async fn ensure_crypto_identity(&self) -> Option<UserIdentity> {
225        let session = self.session();
226        let encryption = session.client().encryption();
227        let user_id = self.user_id();
228
229        // First, see if we should have an updated crypto identity for the user locally.
230        // When we get the remote crypto identity of a user manually, it is cached
231        // locally but it is not kept up-to-date unless the user is tracked. That's why
232        // it's important to only use the local crypto identity if the user is tracked.
233        let should_have_local = if user_id == session.user_id() {
234            true
235        } else {
236            // We should have the updated user identity locally for tracked users.
237            let encryption_clone = encryption.clone();
238            let handle = spawn_tokio!(async move { encryption_clone.tracked_users().await });
239
240            match handle.await.expect("task was not aborted") {
241                Ok(tracked_users) => tracked_users.contains(user_id),
242                Err(error) => {
243                    error!("Could not get tracked users: {error}");
244                    // We are not sure, but let us try to get the local user identity first.
245                    true
246                }
247            }
248        };
249
250        // Try to get the local crypto identity.
251        if should_have_local {
252            if let Some(identity) = self.imp().local_crypto_identity().await {
253                return Some(identity);
254            }
255        }
256
257        // Now, try to request the crypto identity from the homeserver.
258        let user_id_clone = user_id.clone();
259        let handle =
260            spawn_tokio!(async move { encryption.request_user_identity(&user_id_clone).await });
261
262        match handle.await.expect("task was not aborted") {
263            Ok(identity) => identity,
264            Err(error) => {
265                error!("Could not request remote crypto identity: {error}");
266                None
267            }
268        }
269    }
270
271    /// Start a verification of the identity of this user.
272    pub(crate) async fn verify_identity(&self) -> Result<IdentityVerification, ()> {
273        self.session()
274            .verification_list()
275            .create(Some(self.clone()))
276            .await
277    }
278
279    /// The existing direct chat with this user, if any.
280    ///
281    /// A direct chat is a joined room marked as direct, with only our own user
282    /// and the other user in it.
283    pub(crate) fn direct_chat(&self) -> Option<Room> {
284        self.session().room_list().direct_chat(self.user_id())
285    }
286
287    /// Get or create a direct chat with this user.
288    ///
289    /// If there is no existing direct chat, a new one is created.
290    pub(crate) async fn get_or_create_direct_chat(&self) -> Result<Room, ()> {
291        let user_id = self.user_id();
292
293        if let Some(room) = self.direct_chat() {
294            debug!("Using existing direct chat with {user_id}…");
295            return Ok(room);
296        }
297
298        debug!("Creating direct chat with {user_id}…");
299        self.imp().create_direct_chat().await.map_err(|_| ())
300    }
301
302    /// Ignore this user.
303    pub(crate) async fn ignore(&self) -> Result<(), ()> {
304        self.session().ignored_users().add(self.user_id()).await
305    }
306
307    /// Stop ignoring this user.
308    pub(crate) async fn stop_ignoring(&self) -> Result<(), ()> {
309        self.session().ignored_users().remove(self.user_id()).await
310    }
311}
312
313pub trait UserExt: IsA<User> {
314    /// The current session.
315    fn session(&self) -> Session {
316        self.upcast_ref().session()
317    }
318
319    /// The ID of this user.
320    fn user_id(&self) -> &OwnedUserId {
321        self.upcast_ref().imp().user_id()
322    }
323
324    /// Whether this user is the same as the session's user.
325    fn is_own_user(&self) -> bool {
326        self.upcast_ref().is_own_user()
327    }
328
329    /// Set the name of this user.
330    fn set_name(&self, name: Option<String>) {
331        let user = self.upcast_ref();
332
333        let display_name = if let Some(name) = name.filter(|n| !n.is_empty()) {
334            name
335        } else {
336            user.user_id().localpart().to_owned()
337        };
338
339        user.set_display_name(display_name);
340    }
341
342    /// Set the avatar URL of this user.
343    fn set_avatar_url(&self, uri: Option<OwnedMxcUri>) {
344        self.upcast_ref()
345            .avatar_data()
346            .image()
347            .expect("avatar data should have an image")
348            // User avatars never have information.
349            .set_uri_and_info(uri, None);
350    }
351
352    /// Get the `matrix.to` URI representation for this `User`.
353    fn matrix_to_uri(&self) -> MatrixToUri {
354        self.user_id().matrix_to_uri()
355    }
356
357    /// Load the user profile from the homeserver.
358    ///
359    /// This overwrites the already loaded display name and avatar.
360    async fn load_profile(&self) -> Result<(), ()> {
361        let user_id = self.user_id();
362
363        let client = self.session().client();
364        let user_id_clone = user_id.clone();
365        let handle =
366            spawn_tokio!(
367                async move { client.account().fetch_user_profile_of(&user_id_clone).await }
368            );
369
370        match handle.await.expect("task was not aborted") {
371            Ok(response) => {
372                let user = self.upcast_ref::<User>();
373
374                user.set_name(response.displayname);
375                user.set_avatar_url(response.avatar_url);
376                Ok(())
377            }
378            Err(error) => {
379                error!("Could not load user profile for `{user_id}`: {error}");
380                Err(())
381            }
382        }
383    }
384
385    /// Whether this user is currently ignored.
386    fn is_ignored(&self) -> bool {
387        self.upcast_ref().is_ignored()
388    }
389
390    /// Connect to the signal emitted when the `is-ignored` property changes.
391    fn connect_is_ignored_notify<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
392        self.upcast_ref().connect_is_ignored_notify(move |user| {
393            f(user
394                .downcast_ref()
395                .expect("downcasting to own type should succeed"));
396        })
397    }
398}
399
400impl<T: IsA<PillSource> + IsA<User>> UserExt for T {}
401
402unsafe impl<T> IsSubclassable<T> for User
403where
404    T: PillSourceImpl,
405    T::Type: IsA<PillSource>,
406{
407    fn class_init(class: &mut glib::Class<Self>) {
408        <glib::Object as IsSubclassable<T>>::class_init(class.upcast_ref_mut());
409    }
410
411    fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
412        <glib::Object as IsSubclassable<T>>::instance_init(instance);
413    }
414}