Skip to main content

fractal/session/
user.rs

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