feat: implemented leave workspace and list buffer users, various fixes

This commit is contained in:
zaaarf 2024-01-25 16:31:38 +01:00
parent 6a061ca432
commit 741a074464
13 changed files with 88 additions and 125 deletions

View file

@ -10,7 +10,7 @@ name = "codemp"
# core # core
tracing = "0.1" tracing = "0.1"
# woot # woot
codemp-woot = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/woot.git", features = ["serde"], tag = "v0.1.0", optional = true } codemp-woot = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/woot.git", features = ["serde"], tag = "v0.1.1", optional = true }
# proto # proto
tonic = { version = "0.9", features = ["tls", "tls-roots"], optional = true } tonic = { version = "0.9", features = ["tls", "tls-roots"], optional = true }
prost = { version = "0.11.8", optional = true } prost = { version = "0.11.8", optional = true }

View file

@ -14,4 +14,4 @@ message BufferTree {
message WorkspaceFileTree { message WorkspaceFileTree {
// list of strings may be more efficient but it's a lot more hassle // list of strings may be more efficient but it's a lot more hassle
required string payload = 1; // spappolata di json required string payload = 1; // spappolata di json
} //Alla fine non si usa questo per ora ma BufferTree }

View file

@ -6,5 +6,5 @@ package user;
// payload identifying user // payload identifying user
message UserIdentity { message UserIdentity {
// user identifier // user identifier
required string id = 1; required bytes id = 1; //since uuid is 8 bytes we prefer to just send the raw bytes instead of string
} }

View file

@ -20,8 +20,8 @@ message WorkspaceMessage {
} }
message JoinRequest { message JoinRequest {
required string username=1; required string username = 1;
required string password=2; required string password = 2;
} }
message AttachRequest { message AttachRequest {
@ -67,6 +67,6 @@ message UserList {
repeated user.UserIdentity users = 1; repeated user.UserIdentity users = 1;
} }
message WorkspaceDetails{ message WorkspaceDetails {
required int32 id=1; required string id = 1;
} }

View file

@ -1,27 +1,3 @@
// Workspace effimero: sta in /tmp o proprio in memoria
// Workspace e` autenticato: come si decide mentre si rifa il server
// Workspace ha id univoco (stringa), usato per connettercisi
// Workspace implementera` access control:
// * accedere al workspace
// * i singoli buffer
// - i metadati maybe????
// Workspace offre le seguenti features:
// * listare i buffer DONE
// * listare gli user connessi DONE
// * creare buffers DONE REPLACE THE ONE ON buffer.proto
// * NO ATTACH: responsabilita` del buffer service
// * contiene metadata dei buffers:
// * path
// * data creazione
// Buffer id NON E` il path DONE
// BufferService NON ha metadata:
// Workspace tiene traccia di utenti attached (nel futuro) DONE
syntax = "proto2"; syntax = "proto2";
package workspace_service; package workspace_service;
@ -29,11 +5,8 @@ import "user.proto";
import "files.proto"; import "files.proto";
import "workspace.proto"; import "workspace.proto";
service Workspace { service Workspace {
rpc Attach (workspace.AttachRequest) returns (workspace.Token); rpc Attach (workspace.AttachRequest) returns (workspace.Token);
rpc LeaveWorkspace (workspace.WorkspaceDetails) returns (workspace.Empty); rpc LeaveWorkspace (workspace.WorkspaceDetails) returns (workspace.Empty);
@ -46,13 +19,10 @@ service Workspace {
rpc ListUsers (workspace.UserListRequest) returns (workspace.UserList); rpc ListUsers (workspace.UserListRequest) returns (workspace.UserList);
rpc ListBufferUsers (workspace.BufferPayload) returns (workspace.Empty); rpc ListBufferUsers (workspace.BufferPayload) returns (workspace.UserList); //TODO discuss
rpc Join (workspace.JoinRequest) returns (workspace.Token); rpc Join (workspace.JoinRequest) returns (workspace.Token);
rpc Delete (workspace.BufferPayload) returns (workspace.Empty); //deletes buffer rpc Delete (workspace.BufferPayload) returns (workspace.Empty); //deletes buffer
} }

View file

@ -84,11 +84,11 @@ impl TextChange {
/// convert from byte index to row and column /// convert from byte index to row and column
/// txt must be the whole content of the buffer, in order to count lines /// txt must be the whole content of the buffer, in order to count lines
pub fn index_to_rowcol(txt: &str, index: usize) -> crate::proto::RowCol { pub fn index_to_rowcol(txt: &str, index: usize) -> RowCol {
// FIXME might panic, use .get() // FIXME might panic, use .get()
let row = txt[..index].matches('\n').count() as i32; let row = txt[..index].matches('\n').count() as i32;
let col = txt[..index].split('\n').last().unwrap_or("").len() as i32; let col = txt[..index].split('\n').last().unwrap_or("").len() as i32;
crate::proto::RowCol { row, col } RowCol { row, col }
} }
} }

View file

@ -63,7 +63,7 @@ impl BufferWorker {
} }
#[async_trait] #[async_trait]
impl ControllerWorker<TextChange> for BufferControllerWorker { impl ControllerWorker<TextChange> for BufferWorker {
type Controller = BufferController; type Controller = BufferController;
type Tx = mpsc::Sender<Operation>; type Tx = mpsc::Sender<Operation>;
type Rx = Streaming<Operation>; type Rx = Streaming<Operation>;

View file

@ -4,7 +4,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::sync::{mpsc, RwLock};
use tonic::service::interceptor::InterceptedService; use tonic::service::interceptor::InterceptedService;
use tonic::service::Interceptor; use tonic::service::Interceptor;
use tonic::transport::{Channel, Endpoint}; use tonic::transport::{Channel, Endpoint};
@ -14,7 +15,7 @@ use crate::api::controller::ControllerWorker;
use crate::cursor::worker::CursorWorker; use crate::cursor::worker::CursorWorker;
use crate::proto::buffer_service::buffer_client::BufferClient; use crate::proto::buffer_service::buffer_client::BufferClient;
use crate::proto::cursor_service::cursor_client::CursorClient; use crate::proto::cursor_service::cursor_client::CursorClient;
use crate::proto::workspace::{JoinRequest, Token}; use crate::proto::workspace::{JoinRequest, Token, WorkspaceDetails};
use crate::proto::workspace_service::workspace_client::WorkspaceClient; use crate::proto::workspace_service::workspace_client::WorkspaceClient;
use crate::workspace::Workspace; use crate::workspace::Workspace;
@ -26,7 +27,7 @@ use crate::workspace::Workspace;
pub struct Client { pub struct Client {
user_id: Uuid, user_id: Uuid,
token_tx: Arc<tokio::sync::watch::Sender<Token>>, token_tx: Arc<tokio::sync::watch::Sender<Token>>,
pub workspaces: BTreeMap<String, Workspace>, pub workspaces: BTreeMap<String, Arc<RwLock<Workspace>>>,
services: Arc<Services> services: Arc<Services>
} }
@ -62,7 +63,7 @@ fn parse_codemp_connection_string<'a>(string: &'a str) -> (String, String) {
impl Client { impl Client {
/// instantiate and connect a new client /// instantiate and connect a new client
pub async fn new(dest: &str) -> crate::Result<Self> { //TODO interceptor pub async fn new(dest: &str) -> crate::Result<Self> {
let (_host, _workspace_id) = parse_codemp_connection_string(dest); let (_host, _workspace_id) = parse_codemp_connection_string(dest);
let channel = Endpoint::from_shared(dest.to_string())? let channel = Endpoint::from_shared(dest.to_string())?
@ -89,11 +90,18 @@ impl Client {
}) })
} }
/// join a workspace, starting a cursorcontroller and returning a new reference to it /// creates a new workspace (and joins it implicitly), returns an [tokio::sync::RwLock] to interact with it
/// pub async fn create_workspace(&mut self, workspace_id: &str) -> crate::Result<Arc<RwLock<Workspace>>> {
/// to interact with such workspace [crate::api::Controller::send] cursor events or let mut workspace_client = self.services.workspace.clone();
/// [crate::api::Controller::recv] for events on the associated [crate::cursor::Controller]. workspace_client.create_workspace(
pub async fn join(&mut self, workspace_id: &str) -> crate::Result<()> { tonic::Request::new(WorkspaceDetails { id: workspace_id.to_string() })
).await?;
self.join_workspace(workspace_id).await
}
/// join a workspace, returns an [tokio::sync::RwLock] to interact with it
pub async fn join_workspace(&mut self, workspace_id: &str) -> crate::Result<Arc<RwLock<Workspace>>> {
self.token_tx.send(self.services.workspace.clone().join( self.token_tx.send(self.services.workspace.clone().join(
tonic::Request::new(JoinRequest { username: "".to_string(), password: "".to_string() }) //TODO tonic::Request::new(JoinRequest { username: "".to_string(), password: "".to_string() }) //TODO
).await?.into_inner())?; ).await?.into_inner())?;
@ -112,17 +120,32 @@ impl Client {
tracing::debug!("controller worker stopped"); tracing::debug!("controller worker stopped");
}); });
self.workspaces.insert(workspace_id.to_string(), Workspace::new( let lock = Arc::new(RwLock::new(
workspace_id.to_string(), Workspace::new(
self.user_id, workspace_id.to_string(),
self.token_tx.clone(), self.user_id,
controller, self.token_tx.clone(),
self.services.clone() controller,
).await?); self.services.clone()
).await?
));
self.workspaces.insert(workspace_id.to_string(), lock.clone());
Ok(lock)
}
/// leave given workspace, disconnecting buffer and cursor controllers
pub async fn leave_workspace(&self, workspace_id: &str) -> crate::Result<()> {
let mut workspace_client = self.services.workspace.clone();
workspace_client.leave_workspace(
tonic::Request::new(WorkspaceDetails { id: workspace_id.to_string() })
).await?;
Ok(()) Ok(())
} }
/// accessor for user id
pub fn user_id(&self) -> Uuid { pub fn user_id(&self) -> Uuid {
self.user_id.clone() self.user_id.clone()
} }

View file

@ -57,7 +57,7 @@ impl Controller<CursorEvent> for CursorController {
std::mem::swap(&mut cursor.start, &mut cursor.end); std::mem::swap(&mut cursor.start, &mut cursor.end);
} }
Ok(self.op.send(CursorEvent { Ok(self.op.send(CursorEvent {
user: UserIdentity { id: self.user_id.to_string() }, user: UserIdentity { id: self.user_id.as_bytes().to_vec() },
position: cursor, position: cursor,
})?) })?)
} }

View file

@ -39,7 +39,7 @@ impl CursorWorker {
} }
#[async_trait] #[async_trait]
impl ControllerWorker<CursorEvent> for CursorControllerWorker { impl ControllerWorker<CursorEvent> for CursorWorker {
type Controller = CursorController; type Controller = CursorController;
type Tx = mpsc::Sender<CursorEvent>; type Tx = mpsc::Sender<CursorEvent>;
type Rx = Streaming<CursorEvent>; type Rx = Streaming<CursorEvent>;
@ -58,7 +58,7 @@ impl ControllerWorker<CursorEvent> for CursorControllerWorker {
loop { loop {
tokio::select!{ tokio::select!{
Ok(Some(cur)) = rx.message() => { Ok(Some(cur)) = rx.message() => {
if cur.user.id == self.user_id.to_string() { continue } if Uuid::from(cur.user.clone()) == self.user_id { continue }
self.channel.send(cur.clone()).unwrap_or_warn("could not broadcast event"); self.channel.send(cur.clone()).unwrap_or_warn("could not broadcast event");
self.changed.send(cur).unwrap_or_warn("could not update last event"); self.changed.send(cur).unwrap_or_warn("could not update last event");
}, },

View file

@ -1,4 +1,4 @@
use std::{collections::{BTreeMap, BTreeSet}, str::FromStr, sync::Arc}; use std::{collections::{BTreeMap, BTreeSet}, sync::Arc};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@ -25,13 +25,11 @@ impl From<Uuid> for UserInfo {
impl From<UserIdentity> for Uuid { impl From<UserIdentity> for Uuid {
fn from(uid: UserIdentity) -> Uuid { fn from(uid: UserIdentity) -> Uuid {
Uuid::from_str(&uid.id).expect("expected an uuid") let b: [u8; 16] = uid.id.try_into().expect("expected an uuid");
Uuid::from_bytes(b)
} }
} }
/// list_users -> A() , B()
/// get_user_info(B) -> B(cacca, pipu@piu)
pub struct Workspace { pub struct Workspace {
id: String, id: String,
user_id: Uuid, user_id: Uuid,
@ -68,10 +66,10 @@ impl Workspace {
Ok(ws) Ok(ws)
} }
/// create a new buffer in current workspace, with optional given content /// create a new buffer in current workspace
pub async fn create(&mut self, path: &str) -> crate::Result<()> { pub async fn create(&mut self, path: &str) -> crate::Result<()> {
let mut workspace_client = self.services.workspace.clone(); let mut workspace_client = self.services.workspace.clone();
workspace_client.create( workspace_client.create_buffer(
tonic::Request::new(BufferPayload { path: path.to_string() }) tonic::Request::new(BufferPayload { path: path.to_string() })
).await?; ).await?;
@ -110,6 +108,7 @@ impl Workspace {
Ok(controller) Ok(controller)
} }
/// fetch a list of all buffers in a workspace
pub async fn fetch_buffers(&mut self) -> crate::Result<()> { pub async fn fetch_buffers(&mut self) -> crate::Result<()> {
let mut workspace_client = self.services.workspace.clone(); let mut workspace_client = self.services.workspace.clone();
let buffers = workspace_client.list_buffers( let buffers = workspace_client.list_buffers(
@ -124,6 +123,7 @@ impl Workspace {
Ok(()) Ok(())
} }
/// fetch a list of all users in a workspace
pub async fn fetch_users(&mut self) -> crate::Result<()> { pub async fn fetch_users(&mut self) -> crate::Result<()> {
let mut workspace_client = self.services.workspace.clone(); let mut workspace_client = self.services.workspace.clone();
let users = BTreeSet::from_iter(workspace_client.list_users( let users = BTreeSet::from_iter(workspace_client.list_users(
@ -141,10 +141,27 @@ impl Workspace {
Ok(()) Ok(())
} }
pub async fn list_buffer_users() { /// get a list of the users attached to a specific buffer
todo!(); //TODO what is this ///
/// TODO: discuss implementation details
pub async fn list_buffer_users(&mut self, path: &str) -> crate::Result<Vec<UserIdentity>> {
let mut workspace_client = self.services.workspace.clone();
let buffer_users = workspace_client.list_buffer_users(
tonic::Request::new(BufferPayload { path: path.to_string() })
).await?.into_inner().users;
Ok(buffer_users)
} }
/// detach from a specific buffer, returns false if there
pub fn detach(&mut self, path: &str) -> bool {
match &mut self.buffers.remove(path) {
None => false,
Some(_) => true
}
}
/// delete a buffer
pub async fn delete(&mut self, path: &str) -> crate::Result<()> { pub async fn delete(&mut self, path: &str) -> crate::Result<()> {
let mut workspace_client = self.services.workspace.clone(); let mut workspace_client = self.services.workspace.clone();
workspace_client.delete( workspace_client.delete(
@ -156,60 +173,13 @@ impl Workspace {
Ok(()) Ok(())
} }
/// leave current workspace if in one, disconnecting buffer and cursor controllers
pub fn leave_workspace(&self) {
todo!(); //TODO need proto
}
/// disconnect from a specific buffer
pub fn disconnect_buffer(&mut self, path: &str) -> bool {
match &mut self.buffers.remove(path) {
None => false,
Some(_) => true
}
}
pub fn id(&self) -> String { self.id.clone() } pub fn id(&self) -> String { self.id.clone() }
/// return a reference to current cursor controller, if currently in a workspace
pub fn cursor(&self) -> Arc<cursor::Controller> { self.cursor.clone() }
/// get a new reference to a buffer controller, if any is active to given path /// get a new reference to a buffer controller, if any is active to given path
pub fn buffer_by_name(&self, path: &str) -> Option<Arc<buffer::Controller>> { pub fn buffer_by_name(&self, path: &str) -> Option<Arc<buffer::Controller>> {
self.buffers.get(path).cloned() self.buffers.get(path).cloned()
} }
/// return a reference to current cursor controller, if currently in a workspace
pub fn cursor(&self) -> Arc<cursor::Controller> { self.cursor.clone() }
} }
/*
impl Interceptor for Workspace { //TODO
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
request.metadata_mut().insert("auth", self.token.token.parse().unwrap());
Ok(request)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum FSNode {
File(String),
Directory(String, Vec<FSNode>),
}
fn file_tree_rec(path: &str, root: &mut Vec<FSNode>) {
if let Some(idx) = path.find("/") {
let dir = path[..idx].to_string();
let mut dir_node = vec![];
Self::file_tree_rec(&path[idx..], &mut dir_node);
root.push(FSNode::Directory(dir, dir_node));
} else {
root.push(FSNode::File(path.to_string()));
}
}
fn file_tree(&self) -> Vec<FSNode> {
let mut root = vec![];
for path in &self.filetree {
Self::file_tree_rec(&path, &mut root);
}
root
}
*/