codemp/src/client.rs
alemi.dev 4fcab00d34
Some checks are pending
test / test-unit (push) Waiting to run
test / test-beta (beta) (push) Blocked by required conditions
test / test-beta (nightly) (push) Blocked by required conditions
test / test-functional (push) Blocked by required conditions
test / test-build (java, macos-latest) (push) Blocked by required conditions
test / test-build (java, ubuntu-latest) (push) Blocked by required conditions
test / test-build (java, windows-latest) (push) Blocked by required conditions
test / test-build (js, macos-latest) (push) Blocked by required conditions
test / test-build (js, ubuntu-latest) (push) Blocked by required conditions
test / test-build (js, windows-latest) (push) Blocked by required conditions
test / test-build (lua, macos-latest) (push) Blocked by required conditions
test / test-build (lua, ubuntu-latest) (push) Blocked by required conditions
test / test-build (lua, windows-latest) (push) Blocked by required conditions
test / test-build (py, macos-latest) (push) Blocked by required conditions
test / test-build (py, ubuntu-latest) (push) Blocked by required conditions
test / test-build (py, windows-latest) (push) Blocked by required conditions
chore: cargo features (#66)
* chore: changed features layout

now using `dep:` and `crate?/feat` syntaxes (introduced in rust 1.60).
this may mess with lua rockspec
2024-11-16 16:28:46 +01:00

216 lines
4.9 KiB
Rust

//! ### Client
//! Main `codemp` client, containing and managing all underlying services.
use std::sync::Arc;
use dashmap::DashMap;
use tonic::{
service::interceptor::InterceptedService,
transport::{Channel, Endpoint},
};
use crate::{
api::User,
errors::{ConnectionResult, RemoteResult},
ext::InternallyMutable,
network,
workspace::Workspace,
};
use codemp_proto::{
auth::{auth_client::AuthClient, LoginRequest},
common::{Empty, Token},
session::{session_client::SessionClient, InviteRequest, WorkspaceRequest},
};
#[cfg(feature = "py")]
use pyo3::prelude::*;
/// A `codemp` client handle.
///
/// It generates a new UUID and stores user credentials upon connecting.
///
/// A new [`Client`] can be obtained with [`Client::connect`].
#[derive(Debug, Clone)]
#[cfg_attr(feature = "js", napi_derive::napi)]
#[cfg_attr(feature = "py", pyclass)]
pub struct Client(Arc<ClientInner>);
#[derive(Debug)]
struct ClientInner {
user: User,
config: crate::api::Config,
workspaces: DashMap<String, Workspace>,
auth: AuthClient<Channel>,
session: SessionClient<InterceptedService<Channel, network::SessionInterceptor>>,
claims: InternallyMutable<Token>,
}
impl Client {
/// Connect to the server, authenticate and instantiate a new [`Client`].
pub async fn connect(config: crate::api::Config) -> ConnectionResult<Self> {
// TODO move these two into network.rs
let channel = Endpoint::from_shared(config.endpoint())?.connect().await?;
let mut auth = AuthClient::new(channel.clone());
let resp = auth
.login(LoginRequest {
username: config.username.clone(),
password: config.password.clone(),
})
.await?
.into_inner();
let claims = InternallyMutable::new(resp.token);
// TODO move this one into network.rs
let session =
SessionClient::with_interceptor(channel, network::SessionInterceptor(claims.channel()));
Ok(Client(Arc::new(ClientInner {
user: resp.user.into(),
workspaces: DashMap::default(),
claims,
auth,
session,
config,
})))
}
/// Refresh session token.
pub async fn refresh(&self) -> RemoteResult<()> {
let new_token = self
.0
.auth
.clone()
.refresh(self.0.claims.get())
.await?
.into_inner();
self.0.claims.set(new_token);
Ok(())
}
/// Attempt to create a new workspace with given name.
pub async fn create_workspace(&self, name: impl AsRef<str>) -> RemoteResult<()> {
self.0
.session
.clone()
.create_workspace(WorkspaceRequest {
workspace: name.as_ref().to_string(),
})
.await?;
Ok(())
}
/// Delete an existing workspace if possible.
pub async fn delete_workspace(&self, name: impl AsRef<str>) -> RemoteResult<()> {
self.0
.session
.clone()
.delete_workspace(WorkspaceRequest {
workspace: name.as_ref().to_string(),
})
.await?;
Ok(())
}
/// Invite user with given username to the given workspace, if possible.
pub async fn invite_to_workspace(
&self,
workspace_name: impl AsRef<str>,
user_name: impl AsRef<str>,
) -> RemoteResult<()> {
self.0
.session
.clone()
.invite_to_workspace(InviteRequest {
workspace: workspace_name.as_ref().to_string(),
user: user_name.as_ref().to_string(),
})
.await?;
Ok(())
}
/// Fetch the names of all workspaces owned by the current user.
pub async fn fetch_owned_workspaces(&self) -> RemoteResult<Vec<String>> {
self.fetch_workspaces(true).await
}
/// Fetch the names of all workspaces the current user has joined.
pub async fn fetch_joined_workspaces(&self) -> RemoteResult<Vec<String>> {
self.fetch_workspaces(false).await
}
async fn fetch_workspaces(&self, owned: bool) -> RemoteResult<Vec<String>> {
let workspaces = self
.0
.session
.clone()
.list_workspaces(Empty {})
.await?
.into_inner();
if owned {
Ok(workspaces.owned)
} else {
Ok(workspaces.invited)
}
}
/// Join and return a [`Workspace`].
pub async fn attach_workspace(
&self,
workspace: impl AsRef<str>,
) -> ConnectionResult<Workspace> {
let token = self
.0
.session
.clone()
.access_workspace(WorkspaceRequest {
workspace: workspace.as_ref().to_string(),
})
.await?
.into_inner();
let ws = Workspace::connect(
workspace.as_ref().to_string(),
self.0.user.clone(),
self.0.config.clone(),
token,
self.0.claims.channel(),
)
.await?;
self.0
.workspaces
.insert(workspace.as_ref().to_string(), ws.clone());
Ok(ws)
}
/// Leave the [`Workspace`] with the given name.
pub fn leave_workspace(&self, id: &str) -> bool {
match self.0.workspaces.remove(id) {
None => true,
Some(x) => x.1.consume(),
}
}
/// Gets a [`Workspace`] handle by name.
pub fn get_workspace(&self, id: &str) -> Option<Workspace> {
self.0.workspaces.get(id).map(|x| x.clone())
}
/// Get the names of all active [`Workspace`]s.
pub fn active_workspaces(&self) -> Vec<String> {
self.0
.workspaces
.iter()
.map(|x| x.key().to_string())
.collect()
}
/// Get the currently logged in user.
pub fn current_user(&self) -> &User {
&self.0.user
}
}