Some checks failed
BotServer CI / build (push) Failing after 1m34s
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>
509 lines
15 KiB
Rust
509 lines
15 KiB
Rust
use crate::core::shared::models::{BotResponse, UserMessage};
|
|
use crate::core::shared::state::AppState;
|
|
use anyhow::Result;
|
|
use async_trait::async_trait;
|
|
use axum::extract::ws::{Message, WebSocket};
|
|
use botlib::MessageType;
|
|
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;
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum MeetingMessage {
|
|
JoinMeeting {
|
|
room_id: String,
|
|
participant_name: String,
|
|
participant_id: Option<String>,
|
|
},
|
|
|
|
LeaveMeeting {
|
|
room_id: String,
|
|
participant_id: String,
|
|
},
|
|
|
|
Transcription {
|
|
room_id: String,
|
|
participant_id: String,
|
|
text: String,
|
|
timestamp: chrono::DateTime<chrono::Utc>,
|
|
confidence: f32,
|
|
is_final: bool,
|
|
},
|
|
|
|
ChatMessage {
|
|
room_id: String,
|
|
participant_id: String,
|
|
content: String,
|
|
timestamp: chrono::DateTime<chrono::Utc>,
|
|
},
|
|
|
|
BotMessage {
|
|
room_id: String,
|
|
content: String,
|
|
in_response_to: Option<String>,
|
|
metadata: HashMap<String, String>,
|
|
},
|
|
|
|
ScreenShare {
|
|
room_id: String,
|
|
participant_id: String,
|
|
is_sharing: bool,
|
|
share_type: Option<ScreenShareType>,
|
|
},
|
|
|
|
StatusUpdate {
|
|
room_id: String,
|
|
status: MeetingStatus,
|
|
details: Option<String>,
|
|
},
|
|
|
|
ParticipantUpdate {
|
|
room_id: String,
|
|
participant: Participant,
|
|
action: ParticipantAction,
|
|
},
|
|
|
|
RecordingControl {
|
|
room_id: String,
|
|
action: RecordingAction,
|
|
participant_id: String,
|
|
},
|
|
|
|
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,
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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().is_none_or(|s| s.enable_transcription),
|
|
max_participants: 100,
|
|
settings: settings.unwrap_or_default(),
|
|
};
|
|
|
|
self.rooms.write().await.insert(room_id, room.clone());
|
|
|
|
if room.settings.bot_enabled {
|
|
self.add_bot_to_room(&room.id).await?;
|
|
}
|
|
|
|
info!("Created meeting room: {} ({})", room.name, room.id);
|
|
Ok(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());
|
|
|
|
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)
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
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);
|
|
|
|
self.connections
|
|
.write()
|
|
.await
|
|
.insert(room_id.clone(), tx.clone());
|
|
|
|
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)).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
self.connections.write().await.remove(&room_id);
|
|
}
|
|
|
|
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);
|
|
|
|
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 { .. } => {
|
|
self.broadcast_to_room(room_id, message.clone()).await;
|
|
}
|
|
_ => {
|
|
trace!("Handling meeting message: {:?}", message);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn process_bot_command(&self, text: &str, room_id: &str, participant_id: &str) {
|
|
if text.to_lowercase().contains("hey bot") || text.to_lowercase().contains("assistant") {
|
|
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: MessageType::USER,
|
|
media_url: None,
|
|
timestamp: chrono::Utc::now(),
|
|
context_name: None,
|
|
};
|
|
|
|
if let Ok(response) = Self::process_with_bot(user_message) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_with_bot(message: UserMessage) -> Result<BotResponse> {
|
|
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: MessageType::BOT_RESPONSE,
|
|
stream_token: None,
|
|
is_complete: true,
|
|
suggestions: Vec::new(),
|
|
context_name: None,
|
|
context_length: 0,
|
|
context_max_length: 0,
|
|
})
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
pub async fn get_room(&self, room_id: &str) -> Option<MeetingRoom> {
|
|
self.rooms.read().await.get(room_id).cloned()
|
|
}
|
|
|
|
pub async fn list_rooms(&self) -> Vec<MeetingRoom> {
|
|
self.rooms.read().await.values().cloned().collect()
|
|
}
|
|
}
|
|
|
|
#[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>;
|
|
}
|
|
|
|
#[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> {
|
|
Ok(format!("Transcribed text for room {}", room_id))
|
|
}
|
|
}
|