botserver/src/meet/whiteboard.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

896 lines
28 KiB
Rust

use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
Path, State,
},
response::IntoResponse,
routing::get,
Router,
};
use chrono::{DateTime, Utc};
use futures::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use uuid::Uuid;
use crate::core::shared::state::AppState;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ShapeType {
Rectangle,
Ellipse,
Line,
Arrow,
Freehand,
Text,
Image,
Sticky,
Polygon,
Triangle,
Diamond,
Star,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum StrokeStyle {
Solid,
Dashed,
Dotted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: f32,
}
impl Color {
pub fn to_rgba(&self) -> String {
format!("rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a)
}
pub fn from_hex(hex: &str) -> Option<Self> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 && hex.len() != 8 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
let a = if hex.len() == 8 {
u8::from_str_radix(&hex[6..8], 16).ok()? as f32 / 255.0
} else {
1.0
};
Some(Color { r, g, b, a })
}
}
impl Default for Color {
fn default() -> Self {
Color {
r: 0,
g: 0,
b: 0,
a: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShapeStyle {
pub stroke_color: Color,
pub fill_color: Option<Color>,
pub stroke_width: f64,
pub stroke_style: StrokeStyle,
pub opacity: f32,
pub font_size: Option<f64>,
pub font_family: Option<String>,
}
impl Default for ShapeStyle {
fn default() -> Self {
ShapeStyle {
stroke_color: Color::default(),
fill_color: None,
stroke_width: 2.0,
stroke_style: StrokeStyle::Solid,
opacity: 1.0,
font_size: None,
font_family: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Shape {
pub id: Uuid,
pub shape_type: ShapeType,
pub points: Vec<Point>,
pub style: ShapeStyle,
pub text: Option<String>,
pub image_url: Option<String>,
pub rotation: f64,
pub z_index: i32,
pub locked: bool,
pub created_by: Uuid,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CursorPosition {
pub user_id: Uuid,
pub username: String,
pub color: Color,
pub position: Point,
pub last_update: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Selection {
pub user_id: Uuid,
pub shape_ids: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WhiteboardOperation {
AddShape { shape: Shape },
UpdateShape { shape: Shape },
DeleteShape { shape_id: Uuid },
MoveShapes { shape_ids: Vec<Uuid>, delta: Point },
ResizeShape { shape_id: Uuid, bounds: ShapeBounds },
RotateShape { shape_id: Uuid, angle: f64 },
BringToFront { shape_id: Uuid },
SendToBack { shape_id: Uuid },
LockShape { shape_id: Uuid },
UnlockShape { shape_id: Uuid },
GroupShapes { shape_ids: Vec<Uuid>, group_id: Uuid },
UngroupShapes { group_id: Uuid },
Clear,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShapeBounds {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UndoRedoEntry {
pub id: Uuid,
pub operation: WhiteboardOperation,
pub inverse_operation: WhiteboardOperation,
pub user_id: Uuid,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WhiteboardMessage {
Join {
user_id: Uuid,
username: String,
color: Color,
},
Leave {
user_id: Uuid,
},
Operation {
user_id: Uuid,
operation: WhiteboardOperation,
},
CursorMove {
cursor: CursorPosition,
},
Select {
selection: Selection,
},
Deselect {
user_id: Uuid,
},
Undo {
user_id: Uuid,
},
Redo {
user_id: Uuid,
},
RequestSync,
FullSync {
shapes: Vec<Shape>,
cursors: Vec<CursorPosition>,
selections: Vec<Selection>,
},
Ping,
Pong,
Error {
message: String,
},
UserJoined {
user_id: Uuid,
username: String,
color: Color,
},
UserLeft {
user_id: Uuid,
},
}
#[derive(Debug)]
pub struct WhiteboardState {
pub id: Uuid,
pub conversation_id: Uuid,
pub name: String,
pub shapes: HashMap<Uuid, Shape>,
pub cursors: HashMap<Uuid, CursorPosition>,
pub selections: HashMap<Uuid, Selection>,
pub undo_stack: Vec<UndoRedoEntry>,
pub redo_stack: Vec<UndoRedoEntry>,
pub max_undo_history: usize,
pub next_z_index: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl WhiteboardState {
pub fn new(id: Uuid, conversation_id: Uuid, name: String) -> Self {
let now = Utc::now();
WhiteboardState {
id,
conversation_id,
name,
shapes: HashMap::new(),
cursors: HashMap::new(),
selections: HashMap::new(),
undo_stack: Vec::new(),
redo_stack: Vec::new(),
max_undo_history: 100,
next_z_index: 0,
created_at: now,
updated_at: now,
}
}
pub fn apply_operation(
&mut self,
operation: WhiteboardOperation,
user_id: Uuid,
) -> Result<Option<WhiteboardOperation>, String> {
let inverse = self.compute_inverse(&operation)?;
match operation {
WhiteboardOperation::AddShape { shape } => {
let mut shape = shape;
shape.z_index = self.next_z_index;
self.next_z_index += 1;
self.shapes.insert(shape.id, shape);
}
WhiteboardOperation::UpdateShape { shape } => {
if !self.shapes.contains_key(&shape.id) {
return Err("Shape not found".to_string());
}
self.shapes.insert(shape.id, shape);
}
WhiteboardOperation::DeleteShape { shape_id } => {
if self.shapes.remove(&shape_id).is_none() {
return Err("Shape not found".to_string());
}
for selection in self.selections.values_mut() {
selection.shape_ids.retain(|id| *id != shape_id);
}
}
WhiteboardOperation::MoveShapes { shape_ids, delta } => {
for shape_id in &shape_ids {
if let Some(shape) = self.shapes.get_mut(shape_id) {
for point in &mut shape.points {
point.x += delta.x;
point.y += delta.y;
}
shape.updated_at = Utc::now();
}
}
}
WhiteboardOperation::ResizeShape { shape_id, bounds } => {
if let Some(shape) = self.shapes.get_mut(&shape_id) {
if shape.points.len() >= 2 {
shape.points[0] = Point {
x: bounds.x,
y: bounds.y,
};
shape.points[1] = Point {
x: bounds.x + bounds.width,
y: bounds.y + bounds.height,
};
}
shape.updated_at = Utc::now();
}
}
WhiteboardOperation::RotateShape { shape_id, angle } => {
if let Some(shape) = self.shapes.get_mut(&shape_id) {
shape.rotation = angle;
shape.updated_at = Utc::now();
}
}
WhiteboardOperation::BringToFront { shape_id } => {
if let Some(shape) = self.shapes.get_mut(&shape_id) {
shape.z_index = self.next_z_index;
self.next_z_index += 1;
shape.updated_at = Utc::now();
}
}
WhiteboardOperation::SendToBack { shape_id } => {
let min_z = self.shapes.values().map(|s| s.z_index).min().unwrap_or(0) - 1;
if let Some(shape) = self.shapes.get_mut(&shape_id) {
shape.z_index = min_z;
shape.updated_at = Utc::now();
}
}
WhiteboardOperation::LockShape { shape_id } => {
if let Some(shape) = self.shapes.get_mut(&shape_id) {
shape.locked = true;
shape.updated_at = Utc::now();
}
}
WhiteboardOperation::UnlockShape { shape_id } => {
if let Some(shape) = self.shapes.get_mut(&shape_id) {
shape.locked = false;
shape.updated_at = Utc::now();
}
}
WhiteboardOperation::GroupShapes { .. } => {}
WhiteboardOperation::UngroupShapes { .. } => {}
WhiteboardOperation::Clear => {
self.shapes.clear();
self.selections.clear();
self.next_z_index = 0;
}
}
self.updated_at = Utc::now();
if let Some(inv) = inverse.clone() {
let entry = UndoRedoEntry {
id: Uuid::new_v4(),
operation: self.reconstruct_operation(&inv),
inverse_operation: inv,
user_id,
timestamp: Utc::now(),
};
self.undo_stack.push(entry);
if self.undo_stack.len() > self.max_undo_history {
self.undo_stack.remove(0);
}
self.redo_stack.clear();
}
Ok(inverse)
}
fn compute_inverse(&self, operation: &WhiteboardOperation) -> Result<Option<WhiteboardOperation>, String> {
match operation {
WhiteboardOperation::AddShape { shape } => {
Ok(Some(WhiteboardOperation::DeleteShape { shape_id: shape.id }))
}
WhiteboardOperation::UpdateShape { shape } => {
if let Some(old_shape) = self.shapes.get(&shape.id) {
Ok(Some(WhiteboardOperation::UpdateShape {
shape: old_shape.clone(),
}))
} else {
Err("Shape not found".to_string())
}
}
WhiteboardOperation::DeleteShape { shape_id } => {
if let Some(shape) = self.shapes.get(shape_id) {
Ok(Some(WhiteboardOperation::AddShape {
shape: shape.clone(),
}))
} else {
Err("Shape not found".to_string())
}
}
WhiteboardOperation::MoveShapes { shape_ids, delta } => {
Ok(Some(WhiteboardOperation::MoveShapes {
shape_ids: shape_ids.clone(),
delta: Point {
x: -delta.x,
y: -delta.y,
},
}))
}
WhiteboardOperation::RotateShape { shape_id, .. } => {
if let Some(shape) = self.shapes.get(shape_id) {
Ok(Some(WhiteboardOperation::RotateShape {
shape_id: *shape_id,
angle: shape.rotation,
}))
} else {
Ok(None)
}
}
WhiteboardOperation::LockShape { shape_id } => {
Ok(Some(WhiteboardOperation::UnlockShape {
shape_id: *shape_id,
}))
}
WhiteboardOperation::UnlockShape { shape_id } => {
Ok(Some(WhiteboardOperation::LockShape {
shape_id: *shape_id,
}))
}
WhiteboardOperation::Clear => {
Ok(None)
}
_ => Ok(None),
}
}
fn reconstruct_operation(&self, inverse: &WhiteboardOperation) -> WhiteboardOperation {
inverse.clone()
}
pub fn undo(&mut self, user_id: Uuid) -> Option<WhiteboardOperation> {
let user_entries: Vec<_> = self
.undo_stack
.iter()
.enumerate()
.filter(|(_, e)| e.user_id == user_id)
.map(|(i, _)| i)
.collect();
if let Some(&last_idx) = user_entries.last() {
let entry = self.undo_stack.remove(last_idx);
let inverse = entry.inverse_operation.clone();
let _ = self.apply_operation_without_history(inverse.clone());
self.redo_stack.push(entry);
Some(inverse)
} else {
None
}
}
pub fn redo(&mut self, user_id: Uuid) -> Option<WhiteboardOperation> {
let user_entries: Vec<_> = self
.redo_stack
.iter()
.enumerate()
.filter(|(_, e)| e.user_id == user_id)
.map(|(i, _)| i)
.collect();
if let Some(&last_idx) = user_entries.last() {
let entry = self.redo_stack.remove(last_idx);
let operation = entry.operation.clone();
let _ = self.apply_operation_without_history(operation.clone());
self.undo_stack.push(entry);
Some(operation)
} else {
None
}
}
fn apply_operation_without_history(&mut self, operation: WhiteboardOperation) -> Result<(), String> {
match operation {
WhiteboardOperation::AddShape { shape } => {
self.shapes.insert(shape.id, shape);
}
WhiteboardOperation::UpdateShape { shape } => {
self.shapes.insert(shape.id, shape);
}
WhiteboardOperation::DeleteShape { shape_id } => {
self.shapes.remove(&shape_id);
}
WhiteboardOperation::MoveShapes { shape_ids, delta } => {
for shape_id in &shape_ids {
if let Some(shape) = self.shapes.get_mut(shape_id) {
for point in &mut shape.points {
point.x += delta.x;
point.y += delta.y;
}
}
}
}
_ => {}
}
self.updated_at = Utc::now();
Ok(())
}
pub fn update_cursor(&mut self, cursor: CursorPosition) {
self.cursors.insert(cursor.user_id, cursor);
}
pub fn remove_cursor(&mut self, user_id: &Uuid) {
self.cursors.remove(user_id);
}
pub fn set_selection(&mut self, selection: Selection) {
self.selections.insert(selection.user_id, selection);
}
pub fn clear_selection(&mut self, user_id: &Uuid) {
self.selections.remove(user_id);
}
pub fn get_shapes_sorted(&self) -> Vec<&Shape> {
let mut shapes: Vec<_> = self.shapes.values().collect();
shapes.sort_by_key(|s| s.z_index);
shapes
}
pub fn to_sync_message(&self) -> WhiteboardMessage {
WhiteboardMessage::FullSync {
shapes: self.shapes.values().cloned().collect(),
cursors: self.cursors.values().cloned().collect(),
selections: self.selections.values().cloned().collect(),
}
}
}
pub struct WhiteboardManager {
whiteboards: Arc<RwLock<HashMap<Uuid, WhiteboardState>>>,
broadcast_channels: Arc<RwLock<HashMap<Uuid, broadcast::Sender<WhiteboardMessage>>>>,
}
impl Default for WhiteboardManager {
fn default() -> Self {
Self::new()
}
}
impl WhiteboardManager {
pub fn new() -> Self {
WhiteboardManager {
whiteboards: Arc::new(RwLock::new(HashMap::new())),
broadcast_channels: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn create_whiteboard(
&self,
conversation_id: Uuid,
name: String,
) -> Uuid {
let whiteboard_id = Uuid::new_v4();
let state = WhiteboardState::new(whiteboard_id, conversation_id, name);
let (tx, _) = broadcast::channel(1024);
{
let mut whiteboards = self.whiteboards.write().await;
whiteboards.insert(whiteboard_id, state);
}
{
let mut channels = self.broadcast_channels.write().await;
channels.insert(whiteboard_id, tx);
}
whiteboard_id
}
pub async fn get_whiteboard(&self, whiteboard_id: &Uuid) -> Option<WhiteboardState> {
let whiteboards = self.whiteboards.read().await;
whiteboards.get(whiteboard_id).cloned()
}
pub async fn delete_whiteboard(&self, whiteboard_id: &Uuid) -> bool {
let mut whiteboards = self.whiteboards.write().await;
let mut channels = self.broadcast_channels.write().await;
channels.remove(whiteboard_id);
whiteboards.remove(whiteboard_id).is_some()
}
pub async fn subscribe(&self, whiteboard_id: &Uuid) -> Option<broadcast::Receiver<WhiteboardMessage>> {
let channels = self.broadcast_channels.read().await;
channels.get(whiteboard_id).map(|tx| tx.subscribe())
}
pub async fn broadcast(&self, whiteboard_id: &Uuid, message: WhiteboardMessage) {
let channels = self.broadcast_channels.read().await;
if let Some(tx) = channels.get(whiteboard_id) {
let _ = tx.send(message);
}
}
pub async fn apply_operation(
&self,
whiteboard_id: &Uuid,
operation: WhiteboardOperation,
user_id: Uuid,
) -> Result<(), String> {
let mut whiteboards = self.whiteboards.write().await;
let whiteboard = whiteboards
.get_mut(whiteboard_id)
.ok_or("Whiteboard not found")?;
whiteboard.apply_operation(operation.clone(), user_id)?;
drop(whiteboards);
self.broadcast(
whiteboard_id,
WhiteboardMessage::Operation { user_id, operation },
)
.await;
Ok(())
}
pub async fn undo(&self, whiteboard_id: &Uuid, user_id: Uuid) -> Option<WhiteboardOperation> {
let mut whiteboards = self.whiteboards.write().await;
if let Some(whiteboard) = whiteboards.get_mut(whiteboard_id) {
whiteboard.undo(user_id)
} else {
None
}
}
pub async fn redo(&self, whiteboard_id: &Uuid, user_id: Uuid) -> Option<WhiteboardOperation> {
let mut whiteboards = self.whiteboards.write().await;
if let Some(whiteboard) = whiteboards.get_mut(whiteboard_id) {
whiteboard.redo(user_id)
} else {
None
}
}
pub async fn update_cursor(&self, whiteboard_id: &Uuid, cursor: CursorPosition) {
let mut whiteboards = self.whiteboards.write().await;
if let Some(whiteboard) = whiteboards.get_mut(whiteboard_id) {
whiteboard.update_cursor(cursor.clone());
}
drop(whiteboards);
self.broadcast(whiteboard_id, WhiteboardMessage::CursorMove { cursor })
.await;
}
pub async fn user_join(
&self,
whiteboard_id: &Uuid,
user_id: Uuid,
username: String,
color: Color,
) {
self.broadcast(
whiteboard_id,
WhiteboardMessage::UserJoined {
user_id,
username,
color,
},
)
.await;
}
pub async fn user_leave(&self, whiteboard_id: &Uuid, user_id: Uuid) {
{
let mut whiteboards = self.whiteboards.write().await;
if let Some(whiteboard) = whiteboards.get_mut(whiteboard_id) {
whiteboard.remove_cursor(&user_id);
whiteboard.clear_selection(&user_id);
}
}
self.broadcast(whiteboard_id, WhiteboardMessage::UserLeft { user_id })
.await;
}
}
impl Clone for WhiteboardState {
fn clone(&self) -> Self {
WhiteboardState {
id: self.id,
conversation_id: self.conversation_id,
name: self.name.clone(),
shapes: self.shapes.clone(),
cursors: self.cursors.clone(),
selections: self.selections.clone(),
undo_stack: self.undo_stack.clone(),
redo_stack: self.redo_stack.clone(),
max_undo_history: self.max_undo_history,
next_z_index: self.next_z_index,
created_at: self.created_at,
updated_at: self.updated_at,
}
}
}
pub fn configure() -> Router<Arc<AppState>> {
Router::new()
.route("/whiteboard/:id/ws", get(whiteboard_websocket))
.route("/whiteboard/create/:conversation_id", get(create_whiteboard))
}
async fn create_whiteboard(
State(state): State<Arc<AppState>>,
Path(conversation_id): Path<Uuid>,
) -> impl IntoResponse {
let manager = state
.extensions
.get::<WhiteboardManager>()
.await
.unwrap_or_else(|| Arc::new(WhiteboardManager::new()));
let whiteboard_id = manager
.create_whiteboard(conversation_id, "New Whiteboard".to_string())
.await;
axum::Json(serde_json::json!({
"whiteboard_id": whiteboard_id,
"conversation_id": conversation_id
}))
}
async fn whiteboard_websocket(
ws: WebSocketUpgrade,
State(state): State<Arc<AppState>>,
Path(whiteboard_id): Path<Uuid>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_whiteboard_socket(socket, state, whiteboard_id))
}
async fn handle_whiteboard_socket(
socket: WebSocket,
state: Arc<AppState>,
whiteboard_id: Uuid,
) {
let manager = state
.extensions
.get::<WhiteboardManager>()
.await
.unwrap_or_else(|| Arc::new(WhiteboardManager::new()));
let receiver = match manager.subscribe(&whiteboard_id).await {
Some(rx) => rx,
None => {
log::error!("Whiteboard {} not found", whiteboard_id);
return;
}
};
let (mut sender, mut ws_receiver) = socket.split();
let mut broadcast_receiver = receiver;
let user_id = Uuid::new_v4();
let username = format!("User-{}", &user_id.to_string()[..8]);
let user_color = Color {
r: (user_id.as_u128() % 200 + 55) as u8,
g: ((user_id.as_u128() >> 8) % 200 + 55) as u8,
b: ((user_id.as_u128() >> 16) % 200 + 55) as u8,
a: 1.0,
};
manager
.user_join(&whiteboard_id, user_id, username.clone(), user_color.clone())
.await;
if let Some(whiteboard) = manager.get_whiteboard(&whiteboard_id).await {
let sync_msg = whiteboard.to_sync_message();
if let Ok(json) = serde_json::to_string(&sync_msg) {
let _ = sender.send(Message::Text(json.into())).await;
}
}
let manager_clone = manager.clone();
let whiteboard_id_clone = whiteboard_id;
let send_task = tokio::spawn(async move {
while let Ok(msg) = broadcast_receiver.recv().await {
if let Ok(json) = serde_json::to_string(&msg) {
if sender.send(Message::Text(json.into())).await.is_err() {
break;
}
}
}
});
let receive_task = tokio::spawn(async move {
while let Some(result) = ws_receiver.next().await {
match result {
Ok(Message::Text(text)) => {
if let Ok(msg) = serde_json::from_str::<WhiteboardMessage>(&text) {
match msg {
WhiteboardMessage::Operation { operation, .. } => {
let _ = manager_clone
.apply_operation(&whiteboard_id_clone, operation, user_id)
.await;
}
WhiteboardMessage::CursorMove { mut cursor } => {
cursor.user_id = user_id;
cursor.username = username.clone();
cursor.color = user_color.clone();
cursor.last_update = Utc::now();
manager_clone.update_cursor(&whiteboard_id_clone, cursor).await;
}
WhiteboardMessage::Undo { .. } => {
if let Some(op) = manager_clone.undo(&whiteboard_id_clone, user_id).await {
manager_clone
.broadcast(
&whiteboard_id_clone,
WhiteboardMessage::Operation {
user_id,
operation: op,
},
)
.await;
}
}
WhiteboardMessage::Redo { .. } => {
if let Some(op) = manager_clone.redo(&whiteboard_id_clone, user_id).await {
manager_clone
.broadcast(
&whiteboard_id_clone,
WhiteboardMessage::Operation {
user_id,
operation: op,
},
)
.await;
}
}
WhiteboardMessage::Select { mut selection } => {
selection.user_id = user_id;
manager_clone
.broadcast(
&whiteboard_id_clone,
WhiteboardMessage::Select { selection },
)
.await;
}
WhiteboardMessage::Deselect { .. } => {
manager_clone
.broadcast(
&whiteboard_id_clone,
WhiteboardMessage::Deselect { user_id },
)
.await;
}
WhiteboardMessage::RequestSync => {
if let Some(whiteboard) =
manager_clone.get_whiteboard(&whiteboard_id_clone).await
{
manager_clone
.broadcast(&whiteboard_id_clone, whiteboard.to_sync_message())
.await;
}
}
WhiteboardMessage::Ping => {
manager_clone
.broadcast(&whiteboard_id_clone, WhiteboardMessage::Pong)
.await;
}
_ => {}
}
}
}
Ok(Message::Close(_)) => break,
Err(_) => break,
_ => {}
}
}
});
let _ = tokio::join!(send_task, receive_task);
manager
.user_leave(&whiteboard_id, user_id)
.await;
}