matrix-rust-sdk/bindings/matrix-sdk-ffi/src/client.rs

720 lines
23 KiB
Rust

use std::sync::{Arc, RwLock};
use anyhow::{anyhow, Context};
use eyeball::shared::Observable as SharedObservable;
use matrix_sdk::{
media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize},
room::Room as SdkRoom,
ruma::{
api::client::{
account::whoami,
error::ErrorKind,
media::get_content_thumbnail::v3::Method,
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
session::get_login_types,
user_directory::search_users,
},
events::{
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource,
},
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, TransactionId, UInt, UserId,
},
Client as MatrixClient, Error, LoopCtrl,
};
use ruma::push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat};
use serde_json::Value;
use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error, warn};
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
use crate::{client, ClientError};
#[derive(Clone, uniffi::Record)]
pub struct PusherIdentifiers {
pub pushkey: String,
pub app_id: String,
}
impl From<PusherIdentifiers> for PusherIds {
fn from(value: PusherIdentifiers) -> Self {
Self::new(value.pushkey, value.app_id)
}
}
#[derive(Clone, uniffi::Record)]
pub struct HttpPusherData {
pub url: String,
pub format: Option<PushFormat>,
pub default_payload: Option<String>,
}
#[derive(Clone, uniffi::Enum)]
pub enum PusherKind {
Http { data: HttpPusherData },
Email,
}
impl TryFrom<PusherKind> for RumaPusherKind {
type Error = anyhow::Error;
fn try_from(value: PusherKind) -> anyhow::Result<Self> {
match value {
PusherKind::Http { data } => {
let mut ruma_data = RumaHttpPusherData::new(data.url);
if let Some(payload) = data.default_payload {
let json: Value = serde_json::from_str(&payload)?;
ruma_data.default_payload = json;
}
ruma_data.format = data.format.map(Into::into);
Ok(Self::Http(ruma_data))
}
PusherKind::Email => {
let ruma_data = EmailPusherData::new();
Ok(Self::Email(ruma_data))
}
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum PushFormat {
EventIdOnly,
}
impl From<PushFormat> for RumaPushFormat {
fn from(value: PushFormat) -> Self {
match value {
client::PushFormat::EventIdOnly => Self::EventIdOnly,
}
}
}
impl std::ops::Deref for Client {
type Target = MatrixClient;
fn deref(&self) -> &MatrixClient {
&self.client
}
}
pub trait ClientDelegate: Sync + Send {
fn did_receive_auth_error(&self, is_soft_logout: bool);
}
#[derive(Clone)]
pub struct Client {
pub(crate) client: MatrixClient,
delegate: Arc<RwLock<Option<Box<dyn ClientDelegate>>>>,
session_verification_controller:
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
/// The sliding sync proxy that the client is configured to use by default.
/// If this value is `Some`, it will be automatically added to the builder
/// when calling `sliding_sync()`.
pub(crate) sliding_sync_proxy: Arc<RwLock<Option<String>>>,
pub(crate) sliding_sync_reset_broadcast_tx: Arc<SharedObservable<()>>,
}
impl Client {
pub fn new(client: MatrixClient) -> Self {
let session_verification_controller: Arc<
tokio::sync::RwLock<Option<SessionVerificationController>>,
> = Default::default();
let ctrl = session_verification_controller.clone();
client.add_event_handler(move |ev: AnyToDeviceEvent| {
let ctrl = ctrl.clone();
async move {
if let Some(session_verification_controller) = &*ctrl.clone().read().await {
session_verification_controller.process_to_device_message(ev).await;
} else {
debug!("received to-device message, but verification controller isn't ready");
}
}
});
let client = Client {
client,
delegate: Arc::new(RwLock::new(None)),
session_verification_controller,
sliding_sync_proxy: Arc::new(RwLock::new(None)),
sliding_sync_reset_broadcast_tx: Default::default(),
};
let mut unknown_token_error_receiver = client.subscribe_to_unknown_token_errors();
let client_clone = client.clone();
RUNTIME.spawn(async move {
loop {
match unknown_token_error_receiver.recv().await {
Ok(unknown_token) => client_clone.process_unknown_token_error(unknown_token),
Err(receive_error) => {
if let RecvError::Closed = receive_error {
break;
}
}
}
}
});
client
}
/// Login using a username and password.
pub fn login(
&self,
username: String,
password: String,
initial_device_name: Option<String>,
device_id: Option<String>,
) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
let mut builder = self.client.login_username(&username, &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}
if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}
builder.send().await?;
Ok(())
})
}
pub fn get_media_file(
&self,
media_source: Arc<MediaSource>,
mime_type: String,
) -> anyhow::Result<Arc<MediaFileHandle>> {
let client = self.client.clone();
let source = (*media_source).clone();
let mime_type: mime::Mime = mime_type.parse()?;
RUNTIME.block_on(async move {
let handle = client
.media()
.get_media_file(
&MediaRequest { source, format: MediaFormat::File },
&mime_type,
true,
)
.await?;
Ok(Arc::new(MediaFileHandle { inner: handle }))
})
}
}
#[uniffi::export]
impl Client {
/// Restores the client from a `Session`.
pub fn restore_session(&self, session: Session) -> Result<(), ClientError> {
let Session {
access_token,
refresh_token,
user_id,
device_id,
homeserver_url: _,
sliding_sync_proxy,
} = session;
*self.sliding_sync_proxy.write().unwrap() = sliding_sync_proxy;
let session = matrix_sdk::Session {
access_token,
refresh_token,
user_id: user_id.try_into()?,
device_id: device_id.into(),
};
Ok(self.restore_session_inner(session)?)
}
}
impl Client {
/// Restores the client from a `matrix_sdk::Session`.
pub(crate) fn restore_session_inner(&self, session: matrix_sdk::Session) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
self.client.restore_session(session).await?;
Ok(())
})
}
pub fn set_delegate(&self, delegate: Option<Box<dyn ClientDelegate>>) {
*self.delegate.write().unwrap() = delegate;
}
pub async fn async_homeserver(&self) -> String {
self.client.homeserver().await.to_string()
}
/// The OIDC Provider that is trusted by the homeserver. `None` when
/// not configured.
pub async fn authentication_issuer(&self) -> Option<String> {
self.client.authentication_issuer().await
}
/// The sliding sync proxy that is trusted by the homeserver. `None` when
/// not configured.
pub fn discovered_sliding_sync_proxy(&self) -> Option<String> {
RUNTIME.block_on(async move {
self.client.sliding_sync_proxy().await.map(|server| server.to_string())
})
}
pub(crate) fn set_sliding_sync_proxy(&self, sliding_sync_proxy: Option<String>) {
*self.sliding_sync_proxy.write().unwrap() = sliding_sync_proxy;
}
/// Whether or not the client's homeserver supports the password login flow.
pub async fn supports_password_login(&self) -> anyhow::Result<bool> {
let login_types = self.client.get_login_types().await?;
let supports_password = login_types
.flows
.iter()
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Password(_)));
Ok(supports_password)
}
/// Gets information about the owner of a given access token.
pub fn whoami(&self) -> anyhow::Result<whoami::v3::Response> {
RUNTIME
.block_on(async move { self.client.whoami().await.map_err(|e| anyhow!(e.to_string())) })
}
}
#[uniffi::export]
impl Client {
pub fn session(&self) -> Result<Session, ClientError> {
RUNTIME.block_on(async move {
let matrix_sdk::Session { access_token, refresh_token, user_id, device_id } =
self.client.session().context("Missing session")?;
let homeserver_url = self.client.homeserver().await.into();
let sliding_sync_proxy = self.sliding_sync_proxy.read().unwrap().clone();
Ok(Session {
access_token,
refresh_token,
user_id: user_id.to_string(),
device_id: device_id.to_string(),
homeserver_url,
sliding_sync_proxy,
})
})
}
pub fn user_id(&self) -> Result<String, ClientError> {
let user_id = self.client.user_id().context("No User ID found")?;
Ok(user_id.to_string())
}
pub fn display_name(&self) -> Result<String, ClientError> {
let l = self.client.clone();
RUNTIME.block_on(async move {
let display_name = l.account().get_display_name().await?.context("No User ID found")?;
Ok(display_name)
})
}
pub fn set_display_name(&self, name: String) -> Result<(), ClientError> {
let client = self.client.clone();
RUNTIME.block_on(async move {
client
.account()
.set_display_name(Some(name.as_str()))
.await
.context("Unable to set display name")?;
Ok(())
})
}
pub fn avatar_url(&self) -> Result<Option<String>, ClientError> {
let l = self.client.clone();
RUNTIME.block_on(async move {
let avatar_url = l.account().get_avatar_url().await?;
Ok(avatar_url.map(|u| u.to_string()))
})
}
pub fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
let l = self.client.clone();
RUNTIME.block_on(async move {
let url = l.account().get_cached_avatar_url().await?;
Ok(url)
})
}
pub fn device_id(&self) -> Result<String, ClientError> {
let device_id = self.client.device_id().context("No Device ID found")?;
Ok(device_id.to_string())
}
pub fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
let client = self.client.clone();
RUNTIME.block_on(async move {
let response = client.create_room(request.into()).await?;
Ok(String::from(response.room_id()))
})
}
/// Get the content of the event of the given type out of the account data
/// store.
///
/// It will be returned as a JSON string.
pub fn account_data(&self, event_type: String) -> Result<Option<String>, ClientError> {
RUNTIME.block_on(async move {
let event = self.client.account().account_data_raw(event_type.into()).await?;
Ok(event.map(|e| e.json().get().to_owned()))
})
}
/// Set the given account data content for the given event type.
///
/// It should be supplied as a JSON string.
pub fn set_account_data(&self, event_type: String, content: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let raw_content = Raw::from_json_string(content)?;
self.client.account().set_account_data_raw(event_type.into(), raw_content).await?;
Ok(())
})
}
pub fn upload_media(&self, mime_type: String, data: Vec<u8>) -> Result<String, ClientError> {
let l = self.client.clone();
RUNTIME.block_on(async move {
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
let response = l.media().upload(&mime_type, data).await?;
Ok(String::from(response.content_uri))
})
}
pub fn get_media_content(
&self,
media_source: Arc<MediaSource>,
) -> Result<Vec<u8>, ClientError> {
let l = self.client.clone();
let source = (*media_source).clone();
RUNTIME.block_on(async move {
Ok(l.media()
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
.await?)
})
}
pub fn get_media_thumbnail(
&self,
media_source: Arc<MediaSource>,
width: u64,
height: u64,
) -> Result<Vec<u8>, ClientError> {
let l = self.client.clone();
let source = (*media_source).clone();
RUNTIME.block_on(async move {
Ok(l.media()
.get_media_content(
&MediaRequest {
source,
format: MediaFormat::Thumbnail(MediaThumbnailSize {
method: Method::Scale,
width: UInt::new(width).unwrap(),
height: UInt::new(height).unwrap(),
}),
},
true,
)
.await?)
})
}
pub fn get_session_verification_controller(
&self,
) -> Result<Arc<SessionVerificationController>, ClientError> {
RUNTIME.block_on(async move {
if let Some(session_verification_controller) =
&*self.session_verification_controller.read().await
{
return Ok(Arc::new(session_verification_controller.clone()));
}
let user_id = self.client.user_id().context("Failed retrieving current user_id")?;
let user_identity = self
.client
.encryption()
.get_user_identity(user_id)
.await?
.context("Failed retrieving user identity")?;
let session_verification_controller =
SessionVerificationController::new(self.client.encryption(), user_identity);
*self.session_verification_controller.write().await =
Some(session_verification_controller.clone());
Ok(Arc::new(session_verification_controller))
})
}
/// Log out the current user
pub fn logout(&self) -> Result<(), ClientError> {
RUNTIME.block_on(self.client.logout())?;
Ok(())
}
/// Registers a pusher with given parameters
pub fn set_pusher(
&self,
identifiers: PusherIdentifiers,
kind: PusherKind,
app_display_name: String,
device_display_name: String,
profile_tag: Option<String>,
lang: String,
) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let ids = identifiers.into();
let pusher_init = PusherInit {
ids,
kind: kind.try_into()?,
app_display_name,
device_display_name,
profile_tag,
lang,
};
self.client.set_pusher(pusher_init.into()).await?;
Ok(())
})
}
/// The homeserver this client is configured to use.
pub fn homeserver(&self) -> String {
RUNTIME.block_on(self.async_homeserver())
}
pub fn rooms(&self) -> Vec<Arc<Room>> {
self.client.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
}
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_room = self.client.get_dm_room(&user_id).map(SdkRoom::Joined);
let dm = sdk_room.map(|room| Arc::new(Room::new(room)));
Ok(dm)
}
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let user_id = UserId::parse(user_id)?;
self.client.account().ignore_user(&user_id).await?;
Ok(())
})
}
pub fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let user_id = UserId::parse(user_id)?;
self.client.account().unignore_user(&user_id).await?;
Ok(())
})
}
pub fn search_users(
&self,
search_term: String,
limit: u64,
) -> Result<SearchUsersResults, ClientError> {
RUNTIME.block_on(async move {
let response = self.client.search_users(&search_term, limit).await?;
Ok(SearchUsersResults::from(response))
})
}
}
#[derive(uniffi::Record)]
pub struct SearchUsersResults {
pub results: Vec<UserProfile>,
pub limited: bool,
}
impl From<search_users::v3::Response> for SearchUsersResults {
fn from(value: search_users::v3::Response) -> Self {
let results: Vec<UserProfile> = value.results.iter().map(UserProfile::from).collect();
SearchUsersResults { results, limited: value.limited }
}
}
#[derive(uniffi::Record)]
pub struct UserProfile {
pub user_id: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
}
impl From<&search_users::v3::User> for UserProfile {
fn from(value: &search_users::v3::User) -> Self {
UserProfile {
user_id: value.user_id.to_string(),
display_name: value.display_name.clone(),
avatar_url: value.avatar_url.as_ref().map(|url| url.to_string()),
}
}
}
impl Client {
/// Process a sync error and return loop control accordingly
pub(crate) fn process_sync_error(&self, sync_error: Error) -> LoopCtrl {
let client_api_error_kind = sync_error.client_api_error_kind();
match client_api_error_kind {
Some(ErrorKind::UnknownPos) => {
self.sliding_sync_reset_broadcast_tx.set(());
LoopCtrl::Continue
}
_ => {
warn!("Ignoring sync error: {sync_error:?}");
LoopCtrl::Continue
}
}
}
fn process_unknown_token_error(&self, unknown_token: matrix_sdk::UnknownToken) {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_auth_error(unknown_token.soft_logout);
}
}
}
pub struct CreateRoomParameters {
pub name: Option<String>,
pub topic: Option<String>,
pub is_encrypted: bool,
pub is_direct: bool,
pub visibility: RoomVisibility,
pub preset: RoomPreset,
pub invite: Option<Vec<String>>,
pub avatar: Option<String>,
}
impl From<CreateRoomParameters> for create_room::v3::Request {
fn from(value: CreateRoomParameters) -> create_room::v3::Request {
let mut request = create_room::v3::Request::new();
request.name = value.name;
request.topic = value.topic;
request.is_direct = value.is_direct;
request.visibility = value.visibility.into();
request.preset = Some(value.preset.into());
request.invite = match value.invite {
Some(invite) => invite
.iter()
.filter_map(|user_id| match UserId::parse(user_id) {
Ok(id) => Some(id),
Err(e) => {
error!(user_id, "Skipping invalid user ID, error: {e}");
None
}
})
.collect(),
None => vec![],
};
let mut initial_state: Vec<Raw<AnyInitialStateEvent>> = vec![];
if value.is_encrypted {
let content =
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
if let Some(url) = value.avatar {
let mut content = RoomAvatarEventContent::new();
content.url = Some(url.into());
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
request.initial_state = initial_state;
request
}
}
pub enum RoomVisibility {
/// Indicates that the room will be shown in the published room list.
Public,
/// Indicates that the room will not be shown in the published room list.
Private,
}
impl From<RoomVisibility> for Visibility {
fn from(value: RoomVisibility) -> Self {
match value {
RoomVisibility::Public => Self::Public,
RoomVisibility::Private => Self::Private,
}
}
}
pub enum RoomPreset {
/// `join_rules` is set to `invite` and `history_visibility` is set to
/// `shared`.
PrivateChat,
/// `join_rules` is set to `public` and `history_visibility` is set to
/// `shared`.
PublicChat,
/// Same as `PrivateChat`, but all initial invitees get the same power level
/// as the creator.
TrustedPrivateChat,
}
impl From<RoomPreset> for create_room::v3::RoomPreset {
fn from(value: RoomPreset) -> Self {
match value {
RoomPreset::PrivateChat => Self::PrivateChat,
RoomPreset::PublicChat => Self::PublicChat,
RoomPreset::TrustedPrivateChat => Self::TrustedPrivateChat,
}
}
}
#[derive(uniffi::Record)]
pub struct Session {
// Same fields as the Session type in matrix-sdk, just simpler types
/// The access token used for this session.
pub access_token: String,
/// The token used for [refreshing the access token], if any.
///
/// [refreshing the access token]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
pub refresh_token: Option<String>,
/// The user the access token was issued for.
pub user_id: String,
/// The ID of the client device.
pub device_id: String,
// FFI-only fields (for now)
pub homeserver_url: String,
pub sliding_sync_proxy: Option<String>,
}
#[uniffi::export]
fn gen_transaction_id() -> String {
TransactionId::new().to_string()
}
/// A file handle that takes ownership of a media file on disk. When the handle
/// is dropped, the file will be removed from the disk.
pub struct MediaFileHandle {
inner: SdkMediaFileHandle,
}
impl MediaFileHandle {
/// Get the media file's path.
pub fn path(&self) -> String {
self.inner.path().to_str().unwrap().to_owned()
}
}