541 lines
17 KiB
Rust
541 lines
17 KiB
Rust
use crate::shared::models::{BotResponse, UserMessage};
|
|
use crate::shared::state::AppState;
|
|
use anyhow::Result;
|
|
use async_trait::async_trait;
|
|
use axum::extract::ws::{Message, WebSocket};
|
|
use futures::{SinkExt, StreamExt};
|
|
use log::{info, trace, warn};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{mpsc, RwLock};
|
|
use uuid::Uuid;
|
|
|
|
/// Meeting participant information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Participant {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub email: Option<String>,
|
|
pub role: ParticipantRole,
|
|
pub is_bot: bool,
|
|
pub joined_at: chrono::DateTime<chrono::Utc>,
|
|
pub is_active: bool,
|
|
pub has_video: bool,
|
|
pub has_audio: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ParticipantRole {
|
|
Host,
|
|
Moderator,
|
|
Participant,
|
|
Bot,
|
|
}
|
|
|
|
/// Meeting room configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MeetingRoom {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub created_by: String,
|
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
pub participants: Vec<Participant>,
|
|
pub is_recording: bool,
|
|
pub is_transcribing: bool,
|
|
pub max_participants: usize,
|
|
pub settings: MeetingSettings,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MeetingSettings {
|
|
pub enable_transcription: bool,
|
|
pub enable_recording: bool,
|
|
pub enable_chat: bool,
|
|
pub enable_screen_share: bool,
|
|
pub auto_admit: bool,
|
|
pub waiting_room: bool,
|
|
pub bot_enabled: bool,
|
|
pub bot_id: Option<String>,
|
|
}
|
|
|
|
impl Default for MeetingSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
enable_transcription: true,
|
|
enable_recording: false,
|
|
enable_chat: true,
|
|
enable_screen_share: true,
|
|
auto_admit: true,
|
|
waiting_room: false,
|
|
bot_enabled: true,
|
|
bot_id: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// WebSocket message types for meeting communication
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum MeetingMessage {
|
|
/// Join meeting request
|
|
JoinMeeting {
|
|
room_id: String,
|
|
participant_name: String,
|
|
participant_id: Option<String>,
|
|
},
|
|
/// Leave meeting
|
|
LeaveMeeting {
|
|
room_id: String,
|
|
participant_id: String,
|
|
},
|
|
/// Audio/Video transcription
|
|
Transcription {
|
|
room_id: String,
|
|
participant_id: String,
|
|
text: String,
|
|
timestamp: chrono::DateTime<chrono::Utc>,
|
|
confidence: f32,
|
|
is_final: bool,
|
|
},
|
|
/// Chat message in meeting
|
|
ChatMessage {
|
|
room_id: String,
|
|
participant_id: String,
|
|
content: String,
|
|
timestamp: chrono::DateTime<chrono::Utc>,
|
|
},
|
|
/// Bot response
|
|
BotMessage {
|
|
room_id: String,
|
|
content: String,
|
|
in_response_to: Option<String>,
|
|
metadata: HashMap<String, String>,
|
|
},
|
|
/// Screen share status
|
|
ScreenShare {
|
|
room_id: String,
|
|
participant_id: String,
|
|
is_sharing: bool,
|
|
share_type: Option<ScreenShareType>,
|
|
},
|
|
/// Meeting status update
|
|
StatusUpdate {
|
|
room_id: String,
|
|
status: MeetingStatus,
|
|
details: Option<String>,
|
|
},
|
|
/// Participant status update
|
|
ParticipantUpdate {
|
|
room_id: String,
|
|
participant: Participant,
|
|
action: ParticipantAction,
|
|
},
|
|
/// Meeting recording control
|
|
RecordingControl {
|
|
room_id: String,
|
|
action: RecordingAction,
|
|
participant_id: String,
|
|
},
|
|
/// Request bot action
|
|
BotRequest {
|
|
room_id: String,
|
|
participant_id: String,
|
|
command: String,
|
|
parameters: HashMap<String, String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ScreenShareType {
|
|
Screen,
|
|
Window,
|
|
Tab,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MeetingStatus {
|
|
Waiting,
|
|
Active,
|
|
Paused,
|
|
Ended,
|
|
Error,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ParticipantAction {
|
|
Joined,
|
|
Left,
|
|
Updated,
|
|
Muted,
|
|
Unmuted,
|
|
VideoOn,
|
|
VideoOff,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum RecordingAction {
|
|
Start,
|
|
Stop,
|
|
Pause,
|
|
Resume,
|
|
}
|
|
|
|
/// Meeting service for managing video conferences
|
|
pub struct MeetingService {
|
|
pub state: Arc<AppState>,
|
|
pub rooms: Arc<RwLock<HashMap<String, MeetingRoom>>>,
|
|
pub connections: Arc<RwLock<HashMap<String, mpsc::Sender<MeetingMessage>>>>,
|
|
pub transcription_service: Arc<dyn TranscriptionService + Send + Sync>,
|
|
}
|
|
|
|
impl std::fmt::Debug for MeetingService {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("MeetingService")
|
|
.field("state", &self.state)
|
|
.field("rooms", &"Arc<RwLock<HashMap<String, MeetingRoom>>>")
|
|
.field(
|
|
"connections",
|
|
&"Arc<RwLock<HashMap<String, mpsc::Sender<MeetingMessage>>>>",
|
|
)
|
|
.field("transcription_service", &"Arc<dyn TranscriptionService>")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl MeetingService {
|
|
pub fn new(state: Arc<AppState>, transcription_service: Arc<dyn TranscriptionService>) -> Self {
|
|
Self {
|
|
state,
|
|
rooms: Arc::new(RwLock::new(HashMap::new())),
|
|
connections: Arc::new(RwLock::new(HashMap::new())),
|
|
transcription_service,
|
|
}
|
|
}
|
|
|
|
/// Create a new meeting room
|
|
pub async fn create_room(
|
|
&self,
|
|
name: String,
|
|
created_by: String,
|
|
settings: Option<MeetingSettings>,
|
|
) -> Result<MeetingRoom> {
|
|
let room_id = Uuid::new_v4().to_string();
|
|
let room = MeetingRoom {
|
|
id: room_id.clone(),
|
|
name,
|
|
description: None,
|
|
created_by,
|
|
created_at: chrono::Utc::now(),
|
|
participants: Vec::new(),
|
|
is_recording: false,
|
|
is_transcribing: settings.as_ref().map_or(true, |s| s.enable_transcription),
|
|
max_participants: 100,
|
|
settings: settings.unwrap_or_default(),
|
|
};
|
|
|
|
self.rooms.write().await.insert(room_id, room.clone());
|
|
|
|
// Add bot participant if enabled
|
|
if room.settings.bot_enabled {
|
|
self.add_bot_to_room(&room.id).await?;
|
|
}
|
|
|
|
info!("Created meeting room: {} ({})", room.name, room.id);
|
|
Ok(room)
|
|
}
|
|
|
|
/// Join a meeting room
|
|
pub async fn join_room(
|
|
&self,
|
|
room_id: &str,
|
|
participant_name: String,
|
|
participant_id: Option<String>,
|
|
) -> Result<Participant> {
|
|
let mut rooms = self.rooms.write().await;
|
|
let room = rooms
|
|
.get_mut(room_id)
|
|
.ok_or_else(|| anyhow::anyhow!("Room not found"))?;
|
|
|
|
let participant = Participant {
|
|
id: participant_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
|
|
name: participant_name,
|
|
email: None,
|
|
role: ParticipantRole::Participant,
|
|
is_bot: false,
|
|
joined_at: chrono::Utc::now(),
|
|
is_active: true,
|
|
has_video: false,
|
|
has_audio: true,
|
|
};
|
|
|
|
room.participants.push(participant.clone());
|
|
|
|
// Start transcription if enabled and this is the first human participant
|
|
if room.is_transcribing && room.participants.iter().filter(|p| !p.is_bot).count() == 1 {
|
|
self.start_transcription(room_id).await?;
|
|
}
|
|
|
|
info!(
|
|
"Participant {} joined room {} ({})",
|
|
participant.name, room.name, room.id
|
|
);
|
|
|
|
Ok(participant)
|
|
}
|
|
|
|
/// Add bot to meeting room
|
|
async fn add_bot_to_room(&self, room_id: &str) -> Result<()> {
|
|
let bot_participant = Participant {
|
|
id: format!("bot-{}", Uuid::new_v4()),
|
|
name: "Meeting Assistant".to_string(),
|
|
email: Some("bot@botserver.com".to_string()),
|
|
role: ParticipantRole::Bot,
|
|
is_bot: true,
|
|
joined_at: chrono::Utc::now(),
|
|
is_active: true,
|
|
has_video: false,
|
|
has_audio: true,
|
|
};
|
|
|
|
let mut rooms = self.rooms.write().await;
|
|
if let Some(room) = rooms.get_mut(room_id) {
|
|
room.participants.push(bot_participant);
|
|
info!("Bot added to room: {}", room_id);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Start transcription for a meeting
|
|
pub async fn start_transcription(&self, room_id: &str) -> Result<()> {
|
|
info!("Starting transcription for room: {}", room_id);
|
|
|
|
let rooms = self.rooms.read().await;
|
|
if let Some(room) = rooms.get(room_id) {
|
|
if room.is_transcribing {
|
|
self.transcription_service
|
|
.start_transcription(room_id)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle WebSocket connection for meeting
|
|
pub async fn handle_websocket(&self, socket: WebSocket, room_id: String) {
|
|
let (mut sender, mut receiver) = socket.split();
|
|
let (tx, mut rx) = mpsc::channel::<MeetingMessage>(100);
|
|
|
|
// Store connection
|
|
self.connections
|
|
.write()
|
|
.await
|
|
.insert(room_id.clone(), tx.clone());
|
|
|
|
// Spawn task to handle outgoing messages
|
|
tokio::spawn(async move {
|
|
while let Some(msg) = rx.recv().await {
|
|
if let Ok(json) = serde_json::to_string(&msg) {
|
|
if sender.send(Message::Text(json.into())).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle incoming messages
|
|
while let Some(msg) = receiver.next().await {
|
|
if let Ok(Message::Text(text)) = msg {
|
|
if let Ok(meeting_msg) = serde_json::from_str::<MeetingMessage>(&text) {
|
|
self.handle_meeting_message(meeting_msg, &room_id).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up connection
|
|
self.connections.write().await.remove(&room_id);
|
|
}
|
|
|
|
/// Handle incoming meeting messages
|
|
async fn handle_meeting_message(&self, message: MeetingMessage, room_id: &str) {
|
|
match message {
|
|
MeetingMessage::Transcription {
|
|
text,
|
|
participant_id,
|
|
is_final,
|
|
..
|
|
} => {
|
|
if is_final {
|
|
info!("Transcription from {}: {}", participant_id, text);
|
|
|
|
// Process transcription with bot if enabled
|
|
if let Some(room) = self.rooms.read().await.get(room_id) {
|
|
if room.settings.bot_enabled {
|
|
self.process_bot_command(&text, room_id, &participant_id)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MeetingMessage::BotRequest {
|
|
command,
|
|
parameters,
|
|
participant_id,
|
|
..
|
|
} => {
|
|
info!("Bot request from {}: {}", participant_id, command);
|
|
self.handle_bot_request(&command, parameters, room_id, &participant_id)
|
|
.await;
|
|
}
|
|
MeetingMessage::ChatMessage { .. } => {
|
|
// Echo to all participants
|
|
self.broadcast_to_room(room_id, message.clone()).await;
|
|
}
|
|
_ => {
|
|
trace!("Handling meeting message: {:?}", message);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Process bot commands from transcription
|
|
async fn process_bot_command(&self, text: &str, room_id: &str, participant_id: &str) {
|
|
// Check for wake word or bot mention
|
|
if text.to_lowercase().contains("hey bot") || text.to_lowercase().contains("assistant") {
|
|
// Create bot request
|
|
let user_message = UserMessage {
|
|
bot_id: "meeting-assistant".to_string(),
|
|
user_id: participant_id.to_string(),
|
|
session_id: room_id.to_string(),
|
|
channel: "meeting".to_string(),
|
|
content: text.to_string(),
|
|
message_type: 0,
|
|
media_url: None,
|
|
timestamp: chrono::Utc::now(),
|
|
context_name: None,
|
|
};
|
|
|
|
// Process with bot orchestrator
|
|
if let Ok(response) = self.process_with_bot(user_message).await {
|
|
let bot_msg = MeetingMessage::ChatMessage {
|
|
room_id: room_id.to_string(),
|
|
content: response.content,
|
|
participant_id: "bot".to_string(),
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
self.broadcast_to_room(room_id, bot_msg).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handle bot requests
|
|
async fn handle_bot_request(
|
|
&self,
|
|
command: &str,
|
|
_parameters: HashMap<String, String>,
|
|
room_id: &str,
|
|
participant_id: &str,
|
|
) {
|
|
match command {
|
|
"summarize" => {
|
|
let summary = "Meeting summary: Discussion about project updates and next steps.";
|
|
let bot_msg = MeetingMessage::BotMessage {
|
|
room_id: room_id.to_string(),
|
|
content: summary.to_string(),
|
|
in_response_to: Some(participant_id.to_string()),
|
|
metadata: HashMap::from([("command".to_string(), "summarize".to_string())]),
|
|
};
|
|
self.broadcast_to_room(room_id, bot_msg).await;
|
|
}
|
|
"action_items" => {
|
|
let actions = "Action items:\n1. Review documentation\n2. Schedule follow-up";
|
|
let bot_msg = MeetingMessage::BotMessage {
|
|
room_id: room_id.to_string(),
|
|
content: actions.to_string(),
|
|
in_response_to: Some(participant_id.to_string()),
|
|
metadata: HashMap::from([("command".to_string(), "action_items".to_string())]),
|
|
};
|
|
self.broadcast_to_room(room_id, bot_msg).await;
|
|
}
|
|
_ => {
|
|
warn!("Unknown bot command: {}", command);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Process message with bot
|
|
async fn process_with_bot(&self, message: UserMessage) -> Result<BotResponse> {
|
|
// Integrate with bot orchestrator
|
|
// For now, return mock response
|
|
Ok(BotResponse {
|
|
bot_id: message.bot_id,
|
|
user_id: message.user_id,
|
|
session_id: message.session_id,
|
|
channel: "meeting".to_string(),
|
|
content: format!("Processing: {}", message.content),
|
|
message_type: 1,
|
|
stream_token: None,
|
|
is_complete: true,
|
|
suggestions: Vec::new(),
|
|
context_name: None,
|
|
context_length: 0,
|
|
context_max_length: 0,
|
|
})
|
|
}
|
|
|
|
/// Broadcast message to all participants in a room
|
|
async fn broadcast_to_room(&self, room_id: &str, message: MeetingMessage) {
|
|
let connections = self.connections.read().await;
|
|
if let Some(tx) = connections.get(room_id) {
|
|
let _ = tx.send(message).await;
|
|
}
|
|
}
|
|
|
|
/// Get meeting room details
|
|
pub async fn get_room(&self, room_id: &str) -> Option<MeetingRoom> {
|
|
self.rooms.read().await.get(room_id).cloned()
|
|
}
|
|
|
|
/// List all active rooms
|
|
pub async fn list_rooms(&self) -> Vec<MeetingRoom> {
|
|
self.rooms.read().await.values().cloned().collect()
|
|
}
|
|
}
|
|
|
|
/// Trait for transcription services
|
|
#[async_trait]
|
|
pub trait TranscriptionService: Send + Sync {
|
|
async fn start_transcription(&self, room_id: &str) -> Result<()>;
|
|
async fn stop_transcription(&self, room_id: &str) -> Result<()>;
|
|
async fn process_audio(&self, audio_data: Vec<u8>, room_id: &str) -> Result<String>;
|
|
}
|
|
|
|
/// Default transcription service implementation
|
|
#[derive(Debug)]
|
|
pub struct DefaultTranscriptionService;
|
|
|
|
#[async_trait]
|
|
impl TranscriptionService for DefaultTranscriptionService {
|
|
async fn start_transcription(&self, room_id: &str) -> Result<()> {
|
|
info!("Starting transcription for room: {}", room_id);
|
|
Ok(())
|
|
}
|
|
|
|
async fn stop_transcription(&self, room_id: &str) -> Result<()> {
|
|
info!("Stopping transcription for room: {}", room_id);
|
|
Ok(())
|
|
}
|
|
|
|
async fn process_audio(&self, _audio_data: Vec<u8>, room_id: &str) -> Result<String> {
|
|
// Implement actual transcription using speech-to-text service
|
|
Ok(format!("Transcribed text for room {}", room_id))
|
|
}
|
|
}
|