fractal/session/model/room/
aliases.rs

1use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*};
2use matrix_sdk::{deserialized_responses::RawSyncOrStrippedState, reqwest::StatusCode};
3use ruma::{
4    OwnedRoomAliasId,
5    api::client::{
6        alias::{create_alias, delete_alias},
7        room,
8    },
9    events::{SyncStateEvent, room::canonical_alias::RoomCanonicalAliasEventContent},
10};
11use tracing::error;
12
13use super::Room;
14use crate::spawn_tokio;
15
16mod imp {
17    use std::{cell::RefCell, marker::PhantomData, sync::LazyLock};
18
19    use glib::subclass::Signal;
20
21    use super::*;
22
23    #[derive(Debug, Default, glib::Properties)]
24    #[properties(wrapper_type = super::RoomAliases)]
25    pub struct RoomAliases {
26        /// The room these aliases belong to.
27        #[property(get)]
28        room: glib::WeakRef<Room>,
29        /// The canonical alias.
30        pub(super) canonical_alias: RefCell<Option<OwnedRoomAliasId>>,
31        /// The canonical alias, as a string.
32        #[property(get = Self::canonical_alias_string)]
33        canonical_alias_string: PhantomData<Option<String>>,
34        /// The other aliases.
35        pub(super) alt_aliases: RefCell<Vec<OwnedRoomAliasId>>,
36        /// The other aliases, as a `GtkStringList`.
37        #[property(get)]
38        alt_aliases_model: gtk::StringList,
39        /// The alias, as a string.
40        ///
41        /// If the canonical alias is not set, it can be an alt alias.
42        #[property(get = Self::alias_string)]
43        alias_string: PhantomData<Option<String>>,
44    }
45
46    #[glib::object_subclass]
47    impl ObjectSubclass for RoomAliases {
48        const NAME: &'static str = "RoomAliases";
49        type Type = super::RoomAliases;
50    }
51
52    #[glib::derived_properties]
53    impl ObjectImpl for RoomAliases {
54        fn signals() -> &'static [Signal] {
55            static SIGNALS: LazyLock<Vec<Signal>> =
56                LazyLock::new(|| vec![Signal::builder("changed").build()]);
57            SIGNALS.as_ref()
58        }
59    }
60
61    impl RoomAliases {
62        /// Set the room these aliases belong to.
63        pub(super) fn set_room(&self, room: &Room) {
64            self.room.set(Some(room));
65        }
66
67        /// Set the canonical alias.
68        ///
69        /// Returns `true` if the alias changed.
70        fn set_canonical_alias(&self, canonical_alias: Option<OwnedRoomAliasId>) -> bool {
71            if *self.canonical_alias.borrow() == canonical_alias {
72                return false;
73            }
74
75            self.canonical_alias.replace(canonical_alias);
76
77            let obj = self.obj();
78            obj.notify_canonical_alias_string();
79            obj.notify_alias_string();
80            true
81        }
82
83        /// The canonical alias, as a string.
84        fn canonical_alias_string(&self) -> Option<String> {
85            self.canonical_alias
86                .borrow()
87                .as_ref()
88                .map(ToString::to_string)
89        }
90
91        /// Set the alt aliases.
92        ///
93        /// Returns `true` if the aliases changed.
94        fn set_alt_aliases(&self, alt_aliases: Vec<OwnedRoomAliasId>) -> bool {
95            // Check quickly if there are any changes first.
96            if *self.alt_aliases.borrow() == alt_aliases {
97                return false;
98            }
99
100            let (pos, removed) = {
101                let old_aliases = &*self.alt_aliases.borrow();
102                let mut pos = None;
103
104                // Check if aliases were changed in the current list.
105                for (i, old_alias) in old_aliases.iter().enumerate() {
106                    if alt_aliases.get(i).is_none_or(|alias| alias != old_alias) {
107                        pos = Some(i);
108                        break;
109                    }
110                }
111
112                // Check if aliases were added.
113                let old_len = old_aliases.len();
114                if pos.is_none() {
115                    let new_len = alt_aliases.len();
116
117                    if old_len < new_len {
118                        pos = Some(old_len);
119                    }
120                }
121
122                let Some(pos) = pos else {
123                    return false;
124                };
125
126                let removed = old_len.saturating_sub(pos);
127
128                (pos, removed)
129            };
130
131            let additions = alt_aliases.get(pos..).unwrap_or_default().to_owned();
132            let additions_str = additions
133                .iter()
134                .map(|alias| alias.as_str())
135                .collect::<Vec<_>>();
136
137            let Ok(pos) = u32::try_from(pos) else {
138                return false;
139            };
140            let Ok(removed) = u32::try_from(removed) else {
141                return false;
142            };
143
144            self.alt_aliases.replace(alt_aliases);
145            self.alt_aliases_model.splice(pos, removed, &additions_str);
146
147            self.obj().notify_alias_string();
148            true
149        }
150
151        /// The alias, as a string.
152        fn alias_string(&self) -> Option<String> {
153            self.canonical_alias_string()
154                .or_else(|| self.alt_aliases_model.string(0).map(Into::into))
155        }
156
157        /// Update the aliases with the SDK data.
158        pub(super) fn update(&self) {
159            let Some(room) = self.room.upgrade() else {
160                return;
161            };
162
163            let obj = self.obj();
164            let _guard = obj.freeze_notify();
165
166            let matrix_room = room.matrix_room();
167            let mut changed = self.set_canonical_alias(matrix_room.canonical_alias());
168            changed |= self.set_alt_aliases(matrix_room.alt_aliases());
169
170            if changed {
171                obj.emit_by_name::<()>("changed", &[]);
172            }
173        }
174    }
175}
176
177glib::wrapper! {
178    /// Aliases of a room.
179    pub struct RoomAliases(ObjectSubclass<imp::RoomAliases>);
180}
181
182impl RoomAliases {
183    pub fn new() -> Self {
184        glib::Object::new()
185    }
186
187    /// Initialize these aliases with the given room.
188    pub(crate) fn init(&self, room: &Room) {
189        self.imp().set_room(room);
190    }
191
192    /// Update the aliases with the SDK data.
193    pub(crate) fn update(&self) {
194        self.imp().update();
195    }
196
197    /// Get the content of the canonical alias event from the store.
198    async fn canonical_alias_event_content(
199        &self,
200    ) -> Result<Option<RoomCanonicalAliasEventContent>, ()> {
201        let Some(room) = self.room() else {
202            return Err(());
203        };
204
205        let matrix_room = room.matrix_room().clone();
206        let handle = spawn_tokio!(async move {
207            matrix_room
208                .get_state_event_static::<RoomCanonicalAliasEventContent>()
209                .await
210        });
211
212        let raw_event = match handle.await.unwrap() {
213            Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => raw_event,
214            // We shouldn't need to load this is an invited room.
215            Ok(_) => return Ok(None),
216            Err(error) => {
217                error!("Could not get canonical alias event: {error}");
218                return Err(());
219            }
220        };
221
222        match raw_event.deserialize() {
223            Ok(SyncStateEvent::Original(event)) => Ok(Some(event.content)),
224            // The redacted event doesn't have a content.
225            Ok(_) => Ok(None),
226            Err(error) => {
227                error!("Could not deserialize canonical alias event: {error}");
228                Err(())
229            }
230        }
231    }
232
233    /// The canonical alias.
234    pub(crate) fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
235        self.imp().canonical_alias.borrow().clone()
236    }
237
238    /// Remove the given canonical alias.
239    ///
240    /// Checks that the canonical alias is the correct one before proceeding.
241    pub(crate) async fn remove_canonical_alias(&self, alias: &OwnedRoomAliasId) -> Result<(), ()> {
242        let mut event_content = self
243            .canonical_alias_event_content()
244            .await?
245            .unwrap_or_default();
246
247        // Remove the canonical alias, if it is there.
248        if event_content.alias.take().is_none_or(|a| a != *alias) {
249            // Nothing to do.
250            return Err(());
251        }
252
253        let Some(room) = self.room() else {
254            return Err(());
255        };
256
257        let matrix_room = room.matrix_room().clone();
258        let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await });
259
260        match handle.await.unwrap() {
261            Ok(_) => Ok(()),
262            Err(error) => {
263                error!("Could not remove canonical alias: {error}");
264                Err(())
265            }
266        }
267    }
268
269    /// Set the given alias to be the canonical alias.
270    ///
271    /// Removes the given alias from the alt aliases if it is in the list.
272    pub(crate) async fn set_canonical_alias(&self, alias: OwnedRoomAliasId) -> Result<(), ()> {
273        let mut event_content = self
274            .canonical_alias_event_content()
275            .await?
276            .unwrap_or_default();
277
278        if event_content.alias.as_ref().is_some_and(|a| *a == alias) {
279            // Nothing to do.
280            return Err(());
281        }
282
283        let Some(room) = self.room() else {
284            return Err(());
285        };
286
287        // Remove from the alt aliases, if it is there.
288        let alt_alias_pos = event_content.alt_aliases.iter().position(|a| *a == alias);
289        if let Some(pos) = alt_alias_pos {
290            event_content.alt_aliases.remove(pos);
291        }
292
293        // Set as canonical alias.
294        if let Some(old_canonical) = event_content.alias.replace(alias) {
295            // Move the old canonical alias to the alt aliases, if it is not there already.
296            let has_old_canonical = event_content.alt_aliases.contains(&old_canonical);
297
298            if !has_old_canonical {
299                event_content.alt_aliases.push(old_canonical);
300            }
301        }
302
303        let matrix_room = room.matrix_room().clone();
304        let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await });
305
306        match handle.await.unwrap() {
307            Ok(_) => Ok(()),
308            Err(error) => {
309                error!("Could not set canonical alias: {error}");
310                Err(())
311            }
312        }
313    }
314
315    /// The other public aliases.
316    pub(crate) fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
317        self.imp().alt_aliases.borrow().clone()
318    }
319
320    /// Remove the given alt alias.
321    ///
322    /// Checks that is in the list of alt aliases before proceeding.
323    pub(crate) async fn remove_alt_alias(&self, alias: &OwnedRoomAliasId) -> Result<(), ()> {
324        let mut event_content = self
325            .canonical_alias_event_content()
326            .await?
327            .unwrap_or_default();
328
329        // Remove from the alt aliases, if it is there.
330        let alt_alias_pos = event_content.alt_aliases.iter().position(|a| a == alias);
331        if let Some(pos) = alt_alias_pos {
332            event_content.alt_aliases.remove(pos);
333        } else {
334            // Nothing to do.
335            return Err(());
336        }
337
338        let Some(room) = self.room() else {
339            return Err(());
340        };
341
342        let matrix_room = room.matrix_room().clone();
343        let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await });
344
345        match handle.await.unwrap() {
346            Ok(_) => Ok(()),
347            Err(error) => {
348                error!("Could not remove alt alias: {error}");
349                Err(())
350            }
351        }
352    }
353
354    /// Set the given alias to be an alt alias.
355    ///
356    /// Removes the given alias from the alt aliases if it is in the list.
357    pub(crate) async fn add_alt_alias(
358        &self,
359        alias: OwnedRoomAliasId,
360    ) -> Result<(), AddAltAliasError> {
361        let Ok(event_content) = self.canonical_alias_event_content().await else {
362            return Err(AddAltAliasError::Other);
363        };
364
365        let mut event_content = event_content.unwrap_or_default();
366
367        // Do nothing if it is already present.
368        if event_content.alias.as_ref().is_some_and(|a| *a == alias)
369            || event_content.alt_aliases.contains(&alias)
370        {
371            error!("Cannot add alias already listed");
372            return Err(AddAltAliasError::Other);
373        }
374
375        let Some(room) = self.room() else {
376            return Err(AddAltAliasError::Other);
377        };
378
379        let matrix_room = room.matrix_room().clone();
380
381        // Check that the alias exists and points to the proper room.
382        let client = matrix_room.client();
383        let alias_clone = alias.clone();
384        let handle = spawn_tokio!(async move { client.resolve_room_alias(&alias_clone).await });
385
386        match handle.await.unwrap() {
387            Ok(response) => {
388                if response.room_id != matrix_room.room_id() {
389                    error!("Cannot add alias that points to other room");
390                    return Err(AddAltAliasError::InvalidRoomId);
391                }
392            }
393            Err(error) => {
394                error!("Could not check room alias: {error}");
395                if error
396                    .as_client_api_error()
397                    .is_some_and(|e| e.status_code == StatusCode::NOT_FOUND)
398                {
399                    return Err(AddAltAliasError::NotRegistered);
400                }
401
402                return Err(AddAltAliasError::Other);
403            }
404        }
405
406        // Add as alt alias.
407        event_content.alt_aliases.push(alias);
408        let handle = spawn_tokio!(async move { matrix_room.send_state_event(event_content).await });
409
410        match handle.await.unwrap() {
411            Ok(_) => Ok(()),
412            Err(error) => {
413                error!("Could not add alt alias: {error}");
414                Err(AddAltAliasError::Other)
415            }
416        }
417    }
418
419    /// The main alias.
420    ///
421    /// This is the canonical alias if there is one, of the first of the alt
422    /// aliases.
423    pub(crate) fn alias(&self) -> Option<OwnedRoomAliasId> {
424        self.canonical_alias()
425            .or_else(|| self.imp().alt_aliases.borrow().first().cloned())
426    }
427
428    /// Get the local aliases registered on the homeserver.
429    pub(crate) async fn local_aliases(&self) -> Result<Vec<OwnedRoomAliasId>, ()> {
430        let Some(room) = self.room() else {
431            return Err(());
432        };
433
434        let matrix_room = room.matrix_room();
435        let client = matrix_room.client();
436        let room_id = matrix_room.room_id().to_owned();
437
438        let handle =
439            spawn_tokio!(
440                async move { client.send(room::aliases::v3::Request::new(room_id)).await }
441            );
442
443        match handle.await.unwrap() {
444            Ok(response) => Ok(response.aliases),
445            Err(error) => {
446                error!("Could not fetch local room aliases: {error}");
447                Err(())
448            }
449        }
450    }
451
452    /// Unregister the given local alias.
453    pub(crate) async fn unregister_local_alias(&self, alias: OwnedRoomAliasId) -> Result<(), ()> {
454        let Some(room) = self.room() else {
455            return Err(());
456        };
457
458        // Check that the alias exists and points to the proper room.
459        let matrix_room = room.matrix_room();
460        let client = matrix_room.client();
461
462        let request = delete_alias::v3::Request::new(alias);
463        let handle = spawn_tokio!(async move { client.send(request).await });
464
465        match handle.await.unwrap() {
466            Ok(_) => Ok(()),
467            Err(error) => {
468                error!("Could not unregister local alias: {error}");
469                Err(())
470            }
471        }
472    }
473
474    /// Register the given local alias.
475    pub(crate) async fn register_local_alias(
476        &self,
477        alias: OwnedRoomAliasId,
478    ) -> Result<(), RegisterLocalAliasError> {
479        let Some(room) = self.room() else {
480            return Err(RegisterLocalAliasError::Other);
481        };
482
483        // Check that the alias exists and points to the proper room.
484        let matrix_room = room.matrix_room();
485        let client = matrix_room.client();
486        let room_id = matrix_room.room_id().to_owned();
487
488        let request = create_alias::v3::Request::new(alias, room_id);
489        let handle = spawn_tokio!(async move { client.send(request).await });
490
491        match handle.await.unwrap() {
492            Ok(_) => Ok(()),
493            Err(error) => {
494                error!("Could not register local alias: {error}");
495
496                if error
497                    .as_client_api_error()
498                    .is_some_and(|e| e.status_code == StatusCode::CONFLICT)
499                {
500                    Err(RegisterLocalAliasError::AlreadyInUse)
501                } else {
502                    Err(RegisterLocalAliasError::Other)
503                }
504            }
505        }
506    }
507
508    /// Connect to the signal emitted when the aliases changed.
509    pub(crate) fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
510        self.connect_closure(
511            "changed",
512            true,
513            closure_local!(move |obj: Self| {
514                f(&obj);
515            }),
516        )
517    }
518}
519
520impl Default for RoomAliases {
521    fn default() -> Self {
522        Self::new()
523    }
524}
525
526/// All high-level errors that can happen when trying to add an alt alias.
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528pub(crate) enum AddAltAliasError {
529    /// The alias is not registered.
530    NotRegistered,
531    /// The alias is not registered to this room.
532    InvalidRoomId,
533    /// An other error occurred.
534    Other,
535}
536
537/// All high-level errors that can happen when trying to register a local alias.
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub(crate) enum RegisterLocalAliasError {
540    /// The alias is already registered.
541    AlreadyInUse,
542    /// An other error occurred.
543    Other,
544}