matrix-ui-serializable 0.4.0

Opinionated abstraction of the matrix-sdk crate with serializable structs
Documentation
//! All the actions exposed to the frontend that returns a `Result`.

use crate::{
    FrontendVerificationState,
    events::timeline::TimelineKind,
    get_timeline_kind,
    init::{
        login::build_client,
        singletons::{
            CLIENT, CURRENT_USER_ID, HAS_SESSION_STORED, TEMP_CLIENT, TEMP_CLIENT_SESSION,
            get_event_bridge,
        },
    },
    models::{
        async_requests::MatrixRequest,
        events::{EmitEvent, FrontendDevice},
        misc::{EditRoomInformationPayload, EditUserInformationPayload},
        state_updater::StateUpdater,
    },
    room::{
        frontend_events::events_dto::{FrontendTimelineItem, map_event_timeline_item},
        joined_room::get_timeline,
        rooms_list::{RoomsListUpdate, enqueue_rooms_list_update},
    },
    user::{user_power_level::UserPowerLevels, user_profile::UserProfile},
    utils::guess_device_type,
};
use anyhow::anyhow;
use matrix_sdk_ui::timeline::{AttachmentConfig, AttachmentSource};
use mime::Mime;
use rand::{RngExt, distr::Alphanumeric, rng};
use std::sync::Arc;
use tracing::info;
use url::Url;

pub use crate::{init::FrontendAuthTypeResponse, models::events::VerifyDeviceEvent};
pub use matrix_sdk::ruma::{
    MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
};
use matrix_sdk::{
    attachment::{AttachmentInfo, Thumbnail},
    encryption::CrossSigningResetAuthType,
    ruma::{
        DeviceId, OwnedMxcUri,
        api::client::uiaa::{self, MatrixUserIdentifier, UserIdentifier},
        events::room::message::TextMessageEventContent,
    },
};

use tokio::sync::oneshot;

/// Try to build the client from the url given by the frontend and set its singleton.
/// Once called, the client is set and the init process can proceed (by calling check_homeserver_auth_type)
pub async fn build_temp_client_from_homeserver_url(homeserver: String) -> crate::Result<()> {
    let (client, client_session) = build_client(Some(homeserver), None).await?;
    {
        let mut temp_session = TEMP_CLIENT_SESSION.lock().unwrap();
        *temp_session = Some(client_session);
    }
    {
        let mut guard = TEMP_CLIENT.lock.lock().unwrap();
        *guard = Some(client);
    }
    TEMP_CLIENT.cvar.notify_all();
    Ok(())
}

pub async fn check_homeserver_auth_type() -> crate::Result<FrontendAuthTypeResponse> {
    let (auth_type, _) = crate::init::check_homeserver_auth_type().await?;
    Ok(auth_type)
}

/// Submit a request to the Matrix Client that will be executed asynchronously.
pub fn submit_async_request(request: MatrixRequest) {
    crate::models::async_requests::submit_async_request(request);
}

pub async fn fetch_user_profile(
    user_id: OwnedUserId,
    room_id: Option<&OwnedRoomId>,
) -> crate::Result<UserProfile> {
    let (tx, rx) = oneshot::channel();
    crate::user::user_profile::with_sender(user_id, room_id, true, tx);
    Ok(rx
        .await
        .map_err(anyhow::Error::from)?
        .ok_or(anyhow!("Update was room only. Cannot get user profile"))?)
}

/// Get the list of this user's account registered devices.
pub async fn get_devices(user_id: &UserId) -> crate::Result<Vec<FrontendDevice>> {
    let client = CLIENT.wait();
    let devices_list = client.devices().await.map_err(anyhow::Error::from)?;
    let devices: Vec<FrontendDevice> = client
        .encryption()
        .get_user_devices(user_id)
        .await?
        .devices()
        .filter(|device| !device.is_deleted())
        .map(|device| {
            let last_seen_ts = devices_list
                .devices
                .iter()
                .find(|i| i.device_id.eq(device.device_id()))
                .and_then(|d| d.last_seen_ts);
            FrontendDevice {
                device_id: device.device_id().to_owned(),
                display_name: device.display_name().map(|n| n.to_owned()),
                is_verified: device.is_verified(),
                is_verified_with_cross_signing: device.is_verified_with_cross_signing(),
                last_seen_ts,
                guessed_type: guess_device_type(device.display_name()),
                is_current_device: device.device_id().eq(client.device_id().unwrap()),
            }
        })
        .collect();
    Ok(devices)
}

/// Check whether this device is verified or not
pub fn check_device_verification() -> FrontendVerificationState {
    match CLIENT.get() {
        Some(client) => client.encryption().verification_state().get().into(),
        None => FrontendVerificationState::new(matrix_sdk::encryption::VerificationState::Unknown),
    }
}

/// Checks whether this account has secret backup setup
pub async fn has_backup_setup() -> crate::Result<bool> {
    crate::account::backup::has_backup_setup().await
}

/// Try to restore encryption state from backup
pub async fn restore_backup_with_passphrase(passphrase: String) -> crate::Result<()> {
    crate::account::backup::restore_backup_with_passphrase(passphrase).await
}

/// Setup a new backup for secret keys
pub async fn setup_new_backup() -> crate::Result<String> {
    crate::account::backup::setup_new_backup().await
}

pub fn get_dm_room_from_user_id(user_id: &UserId) -> crate::Result<Option<OwnedRoomId>> {
    let client = CLIENT.wait();
    Ok(client.get_dm_room(user_id).map(|r| r.room_id().to_owned()))
}

/// Start the SAS V1 Emoji verification process with another user's device.
pub async fn verify_device(
    user_id: OwnedUserId,
    device_id: OwnedDeviceId,
    cancel_rx: oneshot::Receiver<()>,
    status_tx: std::sync::mpsc::Sender<VerifyDeviceEvent>,
) -> crate::Result<()> {
    crate::events::emoji_verification::verify_device(&user_id, &device_id, cancel_rx, status_tx)
        .await
        .map_err(crate::Error::Anyhow)
}

/// Disconnect the connected user
pub async fn disconnect_user() -> crate::Result<()> {
    let client = CLIENT.wait();
    // Logout the session
    client.logout().await.map_err(|e| e.into())
}

/// Disconnect the connected user
pub async fn check_if_last_device() -> crate::Result<bool> {
    let client = CLIENT.wait();
    client
        .encryption()
        .recovery()
        .is_last_device()
        .await
        .map_err(anyhow::Error::from)
        .map_err(|e| e.into())
}

/// Check the login state
pub fn is_logged_in() -> bool {
    CLIENT.get().is_some()
}

pub fn has_session_stored() -> bool {
    *HAS_SESSION_STORED.wait()
}

pub async fn reset_cross_signing(password: Option<String>) -> crate::Result<()> {
    let client = CLIENT.wait();
    let encryption = client.encryption();
    if let Some(handle) = encryption
        .recovery()
        .reset_identity()
        .await
        .map_err(anyhow::Error::from)?
    {
        match handle.auth_type() {
            CrossSigningResetAuthType::Uiaa(uiaa) => {
                if password.is_none() {
                    panic!("You should provide a password if you reset identity in Uiaa mode");
                }
                let mut password = uiaa::Password::new(
                    UserIdentifier::Matrix(MatrixUserIdentifier::new(
                        client.user_id().unwrap().to_string(),
                    )),
                    password.unwrap(),
                );
                password.session = uiaa.session.clone();

                handle
                    .reset(Some(uiaa::AuthData::Password(password)))
                    .await
                    .map_err(anyhow::Error::from)?;
            }
            CrossSigningResetAuthType::OAuth(o) => {
                let url = o.approval_url.clone();
                info!(
                    "To reset your end-to-end encryption cross-signing identity, \
                    you first need to approve it at {}",
                    url
                );
                tokio::spawn(async move { handle.reset(None).await });
                let event_bridge = get_event_bridge().expect("event bridge should be defined");
                event_bridge.emit(EmitEvent::ResetCrossSigngingUrl(url.to_string()));
            }
        }
    }
    Ok(())
}

pub async fn edit_user_information(
    payload: EditUserInformationPayload,
    updater: Arc<Box<dyn StateUpdater>>,
) -> crate::Result<()> {
    let client = CLIENT.wait();
    let account_manager = client.account();
    if let Some(ref display_name) = payload.new_display_name {
        account_manager.set_display_name(Some(display_name)).await?;
    }
    if let Some(ref mxc_uri) = payload.new_avatar_uri {
        account_manager.set_avatar_url(Some(mxc_uri)).await?;
    }
    if let Some(ref device_name) = payload.new_device_name {
        rename_device(client.device_id().unwrap(), device_name).await?;
    }
    updater.update_current_user_info(
        None,
        payload.new_avatar_uri,
        payload.new_display_name,
        payload.new_device_name,
    )?;
    Ok(())
}

pub async fn rename_device(device_id: &DeviceId, display_name: &str) -> crate::Result<()> {
    let client = CLIENT.wait();
    match client.rename_device(device_id, display_name).await {
        Ok(_) => Ok(()),
        Err(err) => Err(crate::Error::Anyhow(anyhow!(err))),
    }
}

pub async fn upload_media(content_type: Mime, data: Vec<u8>) -> crate::Result<OwnedMxcUri> {
    let client = CLIENT.wait();
    let res = client.media().upload(&content_type, data, None).await?;
    Ok(res.content_uri)
}

pub fn filter_room_list(keywords: String) {
    enqueue_rooms_list_update(RoomsListUpdate::ApplyFilter { keywords });
}

pub async fn define_room_informations(payload: EditRoomInformationPayload) -> crate::Result<()> {
    let client = CLIENT.wait();
    let room = client
        .get_room(&payload.room_id)
        .ok_or(anyhow!("Couldn't get room for given id"))?;
    if let Some(uri) = payload.new_avatar_uri {
        room.set_avatar_url(&uri, None).await?;
    }
    if let Some(name) = payload.new_display_name {
        room.set_name(name).await?;
    }
    if let Some(topic) = payload.topic {
        room.set_room_topic(&topic).await?;
    }
    Ok(())
}

pub fn get_dm_room_id_or_create_it(user_id: OwnedUserId) -> Option<OwnedRoomId> {
    let client = CLIENT.wait();
    let res = client
        .get_dm_room(&user_id)
        .map(|room| room.room_id().to_owned());
    if res.is_none() {
        // If the room doesn't exist, then we send a request to create it.
        // The room_id will be sent to front through an event.
        crate::models::async_requests::submit_async_request(MatrixRequest::CreateDMRoom {
            user_id,
        });
    }
    res
}

pub async fn get_event_from_main_timeline(
    room_id: OwnedRoomId,
    event_id: OwnedEventId,
) -> crate::Result<FrontendTimelineItem> {
    let kind = TimelineKind::MainRoom { room_id };
    let timeline = get_timeline(&kind).ok_or(anyhow!("Cannot get timeline"))?;

    let pl = timeline.room().power_levels_or_default().await;

    let event = timeline
        .item_by_event_id(&event_id)
        .await
        .ok_or(anyhow!("Event not found"))?;

    let unique_id: String = rng()
        .sample_iter(Alphanumeric)
        .take(7)
        .map(char::from)
        .collect();

    Ok(map_event_timeline_item(
        unique_id,
        &event,
        &kind,
        &UserPowerLevels::from(&pl, CURRENT_USER_ID.get().unwrap()),
    )
    .ok_or(anyhow!("This item cannot be mapped to a frontend struct"))?)
}

#[allow(clippy::too_many_arguments)]
pub async fn send_media_message(
    room_id: OwnedRoomId,
    thread_root: Option<OwnedEventId>,
    buffer: Vec<u8>,
    filename: String,
    mime_type: Mime,
    caption: Option<String>,
    in_reply_to: Option<OwnedEventId>,
    info: AttachmentInfo,
    thumbnail: Option<Thumbnail>,
) -> crate::Result<()> {
    let timeline = get_timeline(&get_timeline_kind(room_id, thread_root))
        .ok_or(anyhow!("Cannot get timeline"))?;

    let source = AttachmentSource::Data {
        bytes: buffer,
        filename,
    };

    let config = AttachmentConfig {
        caption: caption.map(TextMessageEventContent::plain),
        in_reply_to,
        info: Some(info),
        thumbnail,
        ..Default::default()
    };

    timeline
        .send_attachment(source, mime_type, config)
        .await
        .map_err(anyhow::Error::from)
        .map_err(Into::into)
}

pub async fn register_notifications(
    _token: String,
    _user_language: String,
    _android_sygnal_url: Url,
    _ios_sygnal_url: Url,
    _app_id: String,
) -> anyhow::Result<()> {
    let client = CLIENT.wait();
    #[cfg(any(target_os = "android", target_os = "ios"))]
    crate::room::notifications::register_mobile_push_notifications(
        &client,
        _token,
        _user_language,
        _android_sygnal_url,
        _ios_sygnal_url,
        _app_id,
    )
    .await?;
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    crate::room::notifications::register_os_desktop_notifications(client).await;

    Ok(())
}