pub mod api;
pub mod error;
#[doc(inline)]
pub use crate::api::auth::Session;
#[doc(inline)]
pub use error::*;
pub mod exports {
pub use futures;
pub use reqwest;
}
#[cfg(feature = "request_method")]
use api::ClientRequest;
use api::{auth::*, chat::EventSource};
use std::sync::Arc;
#[cfg(not(feature = "parking_lot"))]
use std::sync::{Mutex, MutexGuard};
use async_mutex::Mutex as AsyncMutex;
use futures::prelude::*;
use http::{uri::PathAndQuery, Uri};
#[cfg(feature = "parking_lot")]
use parking_lot::{Mutex, MutexGuard};
use reqwest::Client as HttpClient;
use tonic::transport::Channel;
type AuthService = crate::api::auth::auth_service_client::AuthServiceClient<Channel>;
type ChatService = crate::api::chat::chat_service_client::ChatServiceClient<Channel>;
type MediaProxyService =
crate::api::mediaproxy::media_proxy_service_client::MediaProxyServiceClient<Channel>;
#[derive(Debug, Clone)]
pub enum AuthStatus {
None,
InProgress(String),
Complete(Session),
}
impl AuthStatus {
pub fn session(&self) -> Option<&Session> {
match self {
AuthStatus::None => None,
AuthStatus::InProgress(_) => None,
AuthStatus::Complete(session) => Some(session),
}
}
pub fn is_authenticated(&self) -> bool {
matches!(self, AuthStatus::Complete(_))
}
}
#[derive(Debug)]
struct ClientData {
homeserver_url: Uri,
auth_status: Mutex<AuthStatus>,
chat: AsyncMutex<ChatService>,
auth: AsyncMutex<AuthService>,
mediaproxy: AsyncMutex<MediaProxyService>,
http: HttpClient,
}
#[derive(Clone, Debug)]
pub struct Client {
data: Arc<ClientData>,
}
impl Client {
pub async fn new(mut homeserver_url: Uri, session: Option<Session>) -> ClientResult<Self> {
use assign::assign;
if homeserver_url.scheme().is_none() {
let parts = homeserver_url.into_parts();
homeserver_url = Uri::builder()
.scheme("https")
.authority(parts.authority.unwrap())
.path_and_query(
parts
.path_and_query
.unwrap_or_else(|| PathAndQuery::from_static("")),
)
.build()
.unwrap();
}
let http = HttpClient::builder().build()?;
if homeserver_url.port().is_none() {
use serde::Deserialize;
#[derive(Deserialize)]
struct Server {
#[serde(rename(deserialize = "h.server"))]
server: String,
}
let uri = Uri::from_parts(assign!(
homeserver_url.clone().into_parts(),
{
path_and_query: Some(PathAndQuery::from_static("/_harmony/server"))
}
))
.unwrap();
if let Ok(response) = http
.get(&uri.to_string())
.send()
.await?
.json::<Server>()
.await
{
let host: Uri = response.server.parse().unwrap();
homeserver_url = host;
}
};
if let (None, Some(authority)) = (homeserver_url.port(), homeserver_url.authority()) {
let new_authority = format!("{}:2289", authority);
homeserver_url = Uri::from_parts(
assign!(homeserver_url.into_parts(), { authority: Some(new_authority.parse().unwrap()) }),
)
.unwrap();
}
log::debug!(
"Using homeserver URL {} with session {:?} to create a `Client`",
homeserver_url,
session
);
let mut endpoint = Channel::builder(homeserver_url.clone());
if homeserver_url.scheme_str().unwrap() == "https" {
endpoint = endpoint.tls_config(tonic::transport::ClientTlsConfig::new())?;
}
let channel = endpoint.connect().await?;
let auth = AuthService::new(channel.clone());
let chat = ChatService::new(channel.clone());
let mediaproxy = MediaProxyService::new(channel);
let data = ClientData {
homeserver_url,
auth_status: Mutex::new(AuthStatus::None),
chat: AsyncMutex::new(chat),
auth: AsyncMutex::new(auth),
mediaproxy: AsyncMutex::new(mediaproxy),
http,
};
Ok(Self {
data: Arc::new(data),
})
}
async fn chat_lock(&self) -> async_mutex::MutexGuard<'_, ChatService> {
self.data.chat.lock().await
}
async fn auth_lock(&self) -> async_mutex::MutexGuard<'_, AuthService> {
self.data.auth.lock().await
}
async fn mediaproxy_lock(&self) -> async_mutex::MutexGuard<'_, MediaProxyService> {
self.data.mediaproxy.lock().await
}
fn auth_status_lock(&self) -> MutexGuard<AuthStatus> {
#[cfg(not(feature = "parking_lot"))]
return self
.data
.auth_status
.lock()
.expect("auth status mutex was poisoned");
#[cfg(feature = "parking_lot")]
self.data.auth_status.lock()
}
#[cfg(feature = "request_method")]
pub async fn request<Req: ClientRequest<Resp>, Resp, IntoReq: Into<Req>>(
&self,
request: IntoReq,
) -> ClientResult<Resp> {
request.into().request(self).await
}
pub fn auth_status(&self) -> AuthStatus {
self.auth_status_lock().clone()
}
pub fn homeserver_url(&self) -> &Uri {
&self.data.homeserver_url
}
pub async fn begin_auth(&self) -> ClientResult<()> {
let auth_id = api::auth::begin_auth(self, ()).await?.auth_id;
*self.auth_status_lock() = AuthStatus::InProgress(auth_id);
Ok(())
}
pub async fn next_auth_step(
&self,
response: AuthStepResponse,
) -> ClientResult<Option<AuthStep>> {
if let AuthStatus::InProgress(auth_id) = self.auth_status() {
let step = api::auth::next_step(self, AuthResponse::new(auth_id, response)).await?;
Ok(if let Some(auth_step::Step::Session(session)) = step.step {
*self.auth_status_lock() = AuthStatus::Complete(session);
None
} else {
Some(step)
})
} else {
Err(ClientError::NoAuthId)
}
}
pub async fn prev_auth_step(&self) -> ClientResult<AuthStep> {
if let AuthStatus::InProgress(auth_id) = self.auth_status() {
api::auth::step_back(self, AuthId::new(auth_id)).await
} else {
Err(ClientError::NoAuthId)
}
}
pub async fn auth_stream(
&self,
) -> ClientResult<impl Stream<Item = ClientResult<AuthStep>> + Send + Sync> {
if let AuthStatus::InProgress(auth_id) = self.auth_status() {
api::auth::stream_steps(self, AuthId::new(auth_id))
.await
.map(|stream| stream.map_err(Into::into))
} else {
Err(ClientError::NoAuthId)
}
}
pub async fn subscribe_events(
&self,
subscriptions: Vec<EventSource>,
) -> ClientResult<(
impl Stream<Item = ClientResult<api::chat::event::Event>> + Send + Sync,
impl Sink<EventSource, Error = impl std::fmt::Debug> + Send + Sync,
)> {
let (tx, rx) = flume::unbounded();
for sub in subscriptions {
tx.send(sub).unwrap();
}
let sub = api::chat::stream_events(self, rx.into_stream()).await?;
Ok((
sub.map_err(Into::into)
.map_ok(|outer_event| outer_event.event)
.filter_map(|result| {
future::ready(match result {
Ok(maybe_event) => maybe_event.map(Ok),
Err(err) => Some(Err(err)),
})
}),
tx.into_sink(),
))
}
}