Skip to main content

matrix_ui_serializable/
lib.rs

1#![recursion_limit = "256"]
2use std::{path::PathBuf, sync::Arc};
3
4use futures::StreamExt;
5use serde::{Serialize, ser::Serializer};
6use tokio::{
7    runtime::Handle,
8    sync::{broadcast, mpsc::unbounded_channel},
9};
10use tracing::{error, info};
11use url::Url;
12
13use crate::{
14    init::{
15        FrontendAuthTypeResponse, check_homeserver_auth_type,
16        session::{setup_token_background_save, try_restore_session_to_state},
17        singletons::{
18            APP_DATA_DIR, CURRENT_USER_ID, EVENT_BRIDGE, REQUEST_SENDER,
19            VERIFICATION_RESPONSE_RECEIVER,
20        },
21        workers::{async_main_loop, async_worker},
22    },
23    models::{
24        event_bridge::EventBridge,
25        events::{
26            EmitEvent, MatrixLoginPayload, MatrixUpdateCurrentActiveRoom,
27            MatrixVerificationResponse, ToastNotificationRequest, ToastNotificationVariant,
28        },
29        state_updater::StateUpdater,
30    },
31    room::{
32        notifications::enqueue_toast_notification,
33        rooms_list::{RoomsCollectionStatus, RoomsListUpdate, enqueue_rooms_list_update},
34    },
35};
36
37pub(crate) mod account;
38pub mod commands;
39pub(crate) mod events;
40pub(crate) mod init;
41pub mod models;
42pub(crate) mod room;
43pub(crate) mod stores;
44pub(crate) mod user;
45pub(crate) mod utils;
46
47pub type Result<T> = std::result::Result<T, Error>;
48
49/// matrix-ui-serializable Error enum
50#[derive(Debug, thiserror::Error)]
51pub enum Error {
52    #[error(transparent)]
53    Io(#[from] std::io::Error),
54    #[error(transparent)]
55    Anyhow(#[from] anyhow::Error),
56    #[error(transparent)]
57    MatrixSdk(#[from] matrix_sdk::Error),
58}
59
60impl Serialize for Error {
61    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
62    where
63        S: Serializer,
64    {
65        serializer.serialize_str(self.to_string().as_ref())
66    }
67}
68
69/// Required `mpsc:Receiver`s to listen to incoming events
70pub struct EventReceivers {
71    // Event based
72    verification_response_receiver: mpsc::Receiver<MatrixVerificationResponse>,
73    room_update_receiver: mpsc::Receiver<MatrixUpdateCurrentActiveRoom>,
74    // Command based
75    matrix_login_receiver: mpsc::Receiver<MatrixLoginPayload>,
76    oauth_deeplink_receiver: mpsc::Receiver<Url>,
77}
78
79impl EventReceivers {
80    pub fn new(
81        verification_response_receiver: mpsc::Receiver<MatrixVerificationResponse>,
82        room_update_receiver: mpsc::Receiver<MatrixUpdateCurrentActiveRoom>,
83        matrix_login_receiver: mpsc::Receiver<MatrixLoginPayload>,
84        oauth_deeplink_receiver: mpsc::Receiver<Url>,
85    ) -> Self {
86        Self {
87            verification_response_receiver,
88            room_update_receiver,
89            matrix_login_receiver,
90            oauth_deeplink_receiver,
91        }
92    }
93}
94
95/// The required configuration for this lib. Adapters must implement updaters and event_receivers.
96pub struct LibConfig {
97    /// The functions that will be in charge of updating the frontend states / stores
98    /// from the backend state
99    updaters: Box<dyn StateUpdater>,
100    /// To listen to events coming from the frontend, we use channels.
101    event_receivers: EventReceivers,
102    /// The session option stored (or not) by the adapter. It is serialized.
103    session_option: Option<String>,
104    /// A PathBuf to the application data directory
105    app_data_dir: PathBuf,
106    /// The client's URL (for oauth metadata)
107    oauth_client_uri: Url,
108    /// A callback URL that will handle the redirection to your app when logging with the Oauth flow
109    oauth_redirect_uri: Url,
110}
111
112impl LibConfig {
113    pub fn new(
114        updaters: Box<dyn StateUpdater>,
115        event_receivers: EventReceivers,
116        session_option: Option<String>,
117        app_data_dir: PathBuf,
118        oauth_client_uri: Url,
119        oauth_redirect_uri: Url,
120    ) -> Self {
121        Self {
122            updaters,
123            event_receivers,
124            session_option,
125            app_data_dir,
126            oauth_client_uri,
127            oauth_redirect_uri,
128        }
129    }
130}
131
132/// Function to be called once your app is starting to init this lib.
133/// This will start the workers and return a `Receiver` to forward outgoing events.
134pub fn init(mut config: LibConfig) -> broadcast::Receiver<EmitEvent> {
135    APP_DATA_DIR
136        .set(config.app_data_dir)
137        .expect("Couldn't set app data dir");
138
139    // Lib -> adapter events
140    let (event_bridge, broadcast_receiver) = EventBridge::new();
141    EVENT_BRIDGE
142        .set(event_bridge)
143        .expect("Couldn't set the event bridge");
144
145    let basic_init_handle = Handle::current().spawn(async move {
146        // Adapter -> lib events
147
148        VERIFICATION_RESPONSE_RECEIVER
149            .set(tokio::sync::Mutex::new(
150                config.event_receivers.verification_response_receiver,
151            ))
152            .expect("Couldn't set the verification response receiver");
153
154        // Create a channel to be used between UI thread(s) and the async worker thread.
155        init::singletons::init_broadcaster(16).expect("Couldn't init the UI broadcaster");
156
157        let (matrix_request_sender, matrix_request_receiver) = unbounded_channel::<MatrixRequest>();
158        REQUEST_SENDER
159            .set(matrix_request_sender)
160            .expect("BUG: REQUEST_SENDER already set!");
161
162        matrix_request_receiver
163    });
164
165    let _monitor = Handle::current().spawn(async move {
166        let updaters_arc = Arc::new(config.updaters);
167        let inner_updaters = updaters_arc.clone();
168
169        // Setup the token refresher thread before first sync or client build.
170        setup_token_background_save(inner_updaters.clone());
171        let matrix_request_receiver = basic_init_handle.await.expect("couldn't do basic init");
172
173        let client_opt = match try_restore_session_to_state(config.session_option).await {
174            Ok(opt) => opt,
175            Err(e) => {
176                enqueue_toast_notification(ToastNotificationRequest::new(
177                    format!("Failed to restore session, falling back on login. Error: {e}"),
178                    None,
179                    ToastNotificationVariant::Error,
180                ));
181                None
182            }
183        };
184
185        let (client, _has_been_restored) = match client_opt {
186            Some(restored) => {
187                if let Err(e) = &inner_updaters.update_login_state(
188                    LoginState::Restored,
189                    restored.user_id().map(|v| v.to_string()),
190                ) {
191                    enqueue_toast_notification(ToastNotificationRequest::new(
192                        format!("Cannot update login state. Error: {e}"),
193                        None,
194                        ToastNotificationVariant::Error,
195                    ))
196                }
197                (restored, true)
198            }
199            None => {
200                LOGIN_STORE_READY.wait();
201                if let Err(e) =
202                    &inner_updaters.update_login_state(LoginState::AwaitingForHomeserver, None)
203                {
204                    enqueue_toast_notification(ToastNotificationRequest::new(
205                        format!("Cannot update login state. Error: {e}"),
206                        None,
207                        ToastNotificationVariant::Error,
208                    ))
209                }
210                info!("Waiting for homeserver selection...");
211
212                let client = if let Ok((auth_type, client)) = check_homeserver_auth_type().await {
213                    let serialized_session = match auth_type {
214                        FrontendAuthTypeResponse::Oauth => init::oauth::register_and_login_oauth(
215                            &client,
216                            config.event_receivers.oauth_deeplink_receiver,
217                            &config.oauth_client_uri,
218                            &config.oauth_redirect_uri,
219                        )
220                        .await
221                        .expect("Failed to login with OAuth"),
222                        FrontendAuthTypeResponse::Matrix => {
223                            // wait for frontend payload
224                            let login_payload = config
225                                .event_receivers
226                                .matrix_login_receiver
227                                .recv()
228                                .await
229                                .expect("no login sender to listen to");
230                            init::login::login_and_persist_matrix_session(
231                                &client,
232                                login_payload.username.clone(),
233                                login_payload.password.clone(),
234                                login_payload.client_name.clone(),
235                            )
236                            .await
237                            .expect("Failed to login with Matrix Auth")
238                        }
239                        FrontendAuthTypeResponse::WrongUrl => {
240                            enqueue_toast_notification(ToastNotificationRequest::new(
241                                "The homeserver URL is incorrect".to_owned(),
242                                Some("Please restart the app and try another one.".to_owned()),
243                                ToastNotificationVariant::Error,
244                            ));
245                            "".to_owned()
246                        }
247                    };
248
249                    if let Err(e) = &inner_updaters
250                        .persist_login_session(serialized_session)
251                        .await
252                    {
253                        enqueue_toast_notification(ToastNotificationRequest::new(
254                            format!("Failed to persist login session. Error: {e}"),
255                            None,
256                            ToastNotificationVariant::Error,
257                        ));
258                    }
259
260                    client
261                } else {
262                    panic!("Unknown homeserver auth type")
263                };
264
265                (client, false)
266            }
267        };
268
269        CURRENT_USER_ID
270            .set(client.user_id().unwrap().to_owned())
271            .expect("Couldn't set CURRENT_USER_ID singleton");
272
273        let user_avatar = client.account().get_avatar_url().await.map_or(None, |a| a);
274
275        let user_display_name = client
276            .account()
277            .get_display_name()
278            .await
279            .map_or(None, |n| n);
280
281        let device_name = client
282            .encryption()
283            .get_own_device()
284            .await
285            .ok()
286            .flatten()
287            .and_then(|d| d.display_name().map(|s| s.to_owned()));
288
289        if let Err(e) = inner_updaters.update_current_user_info(
290            Some(CURRENT_USER_ID.get().unwrap().to_owned()),
291            user_avatar,
292            user_display_name,
293            device_name,
294        ) {
295            enqueue_toast_notification(ToastNotificationRequest::new(
296                format!("Cannot update current user info. Error: {e}"),
297                None,
298                ToastNotificationVariant::Error,
299            ))
300        }
301
302        // Update frontend login state
303        if let Err(e) = &inner_updaters.update_login_state(
304            LoginState::LoggedIn,
305            CURRENT_USER_ID.get().map(|u| u.to_string()),
306        ) {
307            error!("Cannot update frontend login store. {e}");
308            enqueue_toast_notification(ToastNotificationRequest::new(
309                format!("Cannot update login state. Error: {e}"),
310                None,
311                ToastNotificationVariant::Error,
312            ))
313        }
314
315        let mut verification_subscriber = client.encryption().verification_state();
316
317        let verification_state_updaters = inner_updaters.clone();
318        tokio::task::spawn(async move {
319            while let Some(state) = verification_subscriber.next().await {
320                if let Err(e) = verification_state_updaters
321                    .update_verification_state(FrontendVerificationState::new(state))
322                {
323                    enqueue_toast_notification(ToastNotificationRequest::new(
324                        format!("Cannot update verification store. Error: {e}"),
325                        None,
326                        ToastNotificationVariant::Error,
327                    ))
328                }
329            }
330        });
331
332        let mut state_stream = client.encryption().recovery().state_stream();
333        let recovery_state_updaters = inner_updaters.clone();
334        tokio::task::spawn(async move {
335            while let Some(update) = state_stream.next().await {
336                recovery_state_updaters
337                    .update_recovery_state(update)
338                    .expect("couldn't update frontend recovery state")
339            }
340        });
341
342        let mut ui_event_receiver =
343            init::singletons::subscribe_to_events().expect("Couldn't get UI event receiver"); // subscribe to events so the sender(s) never fail
344
345        // Spawn the actual async worker thread.
346        let mut worker_join_handle = Handle::current().spawn(async_worker(matrix_request_receiver));
347
348        // // Start the main loop that drives the Matrix client SDK.
349        let mut main_loop_join_handle = Handle::current().spawn(async_main_loop(
350            client,
351            updaters_arc,
352            config.event_receivers.room_update_receiver,
353        ));
354
355        LOGIN_STORE_READY.wait();
356
357        #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples.
358        loop {
359            tokio::select! {
360                result = &mut main_loop_join_handle => {
361                    match result {
362                        Ok(Ok(())) => {
363                            error!("BUG: main async loop task ended unexpectedly!");
364                        }
365                        Ok(Err(e)) => {
366                            error!("Error: main async loop task ended:\n\t{e:?}");
367                            enqueue_rooms_list_update(RoomsListUpdate::Status {
368                                status: RoomsCollectionStatus::Error(e.to_string()),
369                            });
370                            enqueue_toast_notification(ToastNotificationRequest::new(
371                                format!("Rooms list update error: {e}"),
372                                None,
373                                ToastNotificationVariant::Error,
374                            ));
375                        },
376                        Err(e) => {
377                            error!("BUG: failed to join main async loop task: {e:?}");
378                        }
379                    }
380                    break;
381                }
382                result = &mut worker_join_handle => {
383                    match result {
384                        Ok(Ok(())) => {
385                            error!("BUG: async worker task ended unexpectedly!");
386                        }
387                        Ok(Err(e)) => {
388                            error!("Error: async worker task ended:\n\t{e:?}");
389                            enqueue_rooms_list_update(RoomsListUpdate::Status {
390                                status: RoomsCollectionStatus::Error(e.to_string()),
391                            });
392                            enqueue_toast_notification(ToastNotificationRequest::new(
393                                format!("Rooms list update error: {e}"),
394                                None,
395                                ToastNotificationVariant::Error,
396                            ));
397                        },
398                        Err(e) => {
399                            error!("BUG: failed to join async worker task: {e:?}");
400                        }
401                    }
402                    break;
403                }
404                _ = ui_event_receiver.recv() => {
405                    #[cfg(debug_assertions)]
406                    tracing::trace!("Received UI update event");
407                }
408            }
409        }
410    });
411
412    // Return broadcast receiver for the adapter to forward outgoing events
413    broadcast_receiver
414}
415
416// Re-exports
417
418pub use init::session::FullMatrixSession;
419pub use init::singletons::{CLIENT, LOGIN_STORE_READY};
420pub use models::async_requests::*;
421pub use room::frontend_events::events_dto::FrontendTimelineItem;
422pub use room::room_screen::RoomScreen;
423pub use room::rooms_list::RoomsList;
424pub use stores::login_store::{FrontendSyncServiceState, FrontendVerificationState, LoginState};
425pub use user::user_profile::UserProfile;
426// The adapter needs some types in those modules
427pub use matrix_sdk::AuthSession;
428pub use matrix_sdk::attachment::{
429    AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
430};
431pub use matrix_sdk::encryption::recovery::RecoveryState;
432pub use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
433pub use matrix_sdk::ruma::serde::base64::{Base64, Standard, UrlSafe};
434pub use matrix_sdk::ruma::{
435    OwnedDeviceId, OwnedMxcUri, OwnedRoomId, OwnedUserId, UInt,
436    events::room::{
437        EncryptedFile, EncryptedFileHashes, EncryptedFileInfo, MediaSource, V2EncryptedFileInfo,
438    },
439    media::Method,
440};
441pub use tokio::sync::mpsc;
442pub use tokio::sync::oneshot;