1use crate::media::cache;
2use crate::synthesis::{SynthesisEvent, SynthesisOption};
3use crate::{
4 app::AppState,
5 call::{
6 ActiveCall, ActiveCallType, Command,
7 active_call::{ActiveCallGuard, CallParams, CallSpec},
8 },
9 handler::playbook,
10 playbook::{Playbook, PlaybookRunner},
11};
12use crate::{event::SessionEvent, media::track::TrackConfig};
13use axum::{
14 Json, Router,
15 extract::{Path, Query, State, WebSocketUpgrade, ws::Message},
16 response::sse::{Event, KeepAlive, Sse},
17 response::{IntoResponse, Response},
18 routing::{get, post},
19};
20use bytes::Bytes;
21use chrono::Utc;
22use futures::{SinkExt, StreamExt};
23use rustrtc::IceServer;
24use serde_json::json;
25use std::collections::HashMap;
26use std::{path::PathBuf, sync::Arc, time::Duration};
27use tokio::{join, select};
28use tokio_util::sync::CancellationToken;
29use tracing::{debug, info, trace, warn};
30use uuid::Uuid;
31
32fn filter_headers(
33 extras: &mut std::collections::HashMap<String, serde_json::Value>,
34 allowed_headers: &[String],
35) {
36 extras.retain(|k, _| allowed_headers.iter().any(|h| h.eq_ignore_ascii_case(k)));
37}
38
39pub fn call_router() -> Router<AppState> {
40 let r = Router::new()
41 .route("/call", get(ws_handler))
42 .route("/call/webrtc", get(webrtc_handler))
43 .route("/call/sip", get(sip_handler))
44 .route("/list", get(list_active_calls))
45 .route("/kill/{id}", get(kill_active_call))
46 .route("/events/{id}", get(stream_events))
47 .route("/command/{id}", post(send_command))
48 .route("/precache", post(precache));
49 r
50}
51
52pub fn iceservers_router() -> Router<AppState> {
53 let r = Router::new();
54 r.route("/iceservers", get(get_iceservers))
55}
56
57pub fn playbook_router() -> Router<AppState> {
58 Router::new()
59 .route("/api/playbooks", get(playbook::list_playbooks))
60 .route(
61 "/api/playbooks/{name}",
62 get(playbook::get_playbook).post(playbook::save_playbook),
63 )
64 .route(
65 "/api/playbook/run",
66 axum::routing::post(playbook::run_playbook),
67 )
68 .route("/api/records", get(playbook::list_records))
69}
70
71pub async fn ws_handler(
72 ws: WebSocketUpgrade,
73 State(state): State<AppState>,
74 Query(params): Query<CallParams>,
75) -> Response {
76 call_handler(ActiveCallType::WebSocket, ws, state, params).await
77}
78
79pub async fn sip_handler(
80 ws: WebSocketUpgrade,
81 State(state): State<AppState>,
82 Query(params): Query<CallParams>,
83) -> Response {
84 call_handler(ActiveCallType::Sip, ws, state, params).await
85}
86
87pub async fn webrtc_handler(
88 ws: WebSocketUpgrade,
89 State(state): State<AppState>,
90 Query(params): Query<CallParams>,
91) -> Response {
92 call_handler(ActiveCallType::Webrtc, ws, state, params).await
93}
94
95pub async fn call_handler_core(
102 call_type: ActiveCallType,
103 session_id: String,
104 app_state: AppState,
105 cancel_token: CancellationToken,
106 audio_receiver: tokio::sync::mpsc::UnboundedReceiver<Bytes>,
107 server_side_track: Option<String>,
108 dump_events: bool,
109 ping_interval: u64,
110 mut command_receiver: tokio::sync::mpsc::UnboundedReceiver<Command>,
111 event_sender_to_client: tokio::sync::mpsc::UnboundedSender<crate::event::SessionEvent>,
112 extras: Option<HashMap<String, serde_json::Value>>,
113 playbook_name: Option<String>,
114) -> Option<HashMap<String, serde_json::Value>> {
115 let _cancel_guard = cancel_token.clone().drop_guard();
116 let track_config = TrackConfig::default();
117
118 let active_call = Arc::new(ActiveCall::new(CallSpec {
119 call_type: call_type.clone(),
120 cancel_token: cancel_token.clone(),
121 session_id: session_id.clone(),
122 invitation: app_state.invitation.clone(),
123 app_state: app_state.clone(),
124 track_config,
125 audio_receiver: Some(audio_receiver),
126 dump_events,
127 server_side_track_id: server_side_track.clone(),
128 extras,
129 }));
130
131 {
134 let name_or_content = playbook_name.or_else(|| {
135 app_state
136 .pending_playbooks
137 .try_lock()
138 .ok()
139 .and_then(|mut pending| pending.remove(&session_id).map(|(val, _)| val))
140 });
141 if let Some(name_or_content) = name_or_content {
142 let playbook_result = if name_or_content.trim().starts_with("---") {
143 Playbook::parse(&name_or_content)
144 } else {
145 let path = if name_or_content.starts_with("config/playbook/") {
147 PathBuf::from(&name_or_content)
148 } else {
149 PathBuf::from("config/playbook").join(&name_or_content)
150 };
151 Playbook::load(path).await
152 };
153
154 match playbook_result {
155 Ok(mut playbook) => {
156 if call_type == ActiveCallType::Sip {
158 if let Some(sip_config) = &playbook.config.sip {
159 if let Some(allowed_headers) = &sip_config.extract_headers {
160 let extras = active_call.extras.rcu(|e| {
161 let mut e = HashMap::clone(e);
162 filter_headers(&mut e, allowed_headers);
163 e
164 });
165 let header_keys: Vec<String> = extras
167 .keys()
168 .filter(|k| !k.starts_with('_'))
169 .cloned()
170 .collect();
171 active_call.set_extra(
172 "_sip_header_keys",
173 serde_json::to_value(&header_keys).unwrap_or_default(),
174 );
175 if let Ok(result) = playbook.render(&active_call.extras.load_full())
176 {
177 playbook = result;
178 }
179 }
180 }
181 }
182
183 match PlaybookRunner::new(playbook, active_call.clone()) {
184 Ok(runner) => {
185 crate::spawn(async move {
186 runner.run().await;
187 });
188 let display_name = if name_or_content.trim().starts_with("---") {
189 "custom content"
190 } else {
191 &name_or_content
192 };
193 info!(session_id, "Playbook runner started for {}", display_name);
194 }
195 Err(e) => {
196 let display_name = if name_or_content.trim().starts_with("---") {
197 "custom content"
198 } else {
199 &name_or_content
200 };
201 warn!(
202 session_id,
203 "Failed to create runner {}: {}", display_name, e
204 )
205 }
206 }
207 }
208 Err(e) => {
209 let display_name = if name_or_content.trim().starts_with("---") {
210 "custom content"
211 } else {
212 &name_or_content
213 };
214 warn!(
215 session_id,
216 "Failed to load playbook {}: {}", display_name, e
217 );
218 let event = SessionEvent::Error {
219 timestamp: crate::media::get_timestamp(),
220 track_id: session_id.clone(),
221 sender: "playbook".to_string(),
222 error: format!("{}", e),
223 code: None,
224 };
225 event_sender_to_client.send(event).ok();
226 return None;
227 }
228 }
229 }
230 }
231
232 let recv_commands_loop = async {
233 while let Some(command) = command_receiver.recv().await {
234 if let Err(_) = active_call.enqueue_command(command).await {
235 break;
236 }
237 }
238 };
239
240 let mut event_receiver = active_call.event_sender.subscribe();
241 let send_events_loop = async {
242 loop {
243 match event_receiver.recv().await {
244 Ok(event) => {
245 if let Err(_) = event_sender_to_client.send(event) {
246 break;
247 }
248 }
249 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
250 Err(_) => break,
251 }
252 }
253 };
254
255 let send_ping_loop = async {
256 if ping_interval == 0 {
257 active_call.cancel_token.cancelled().await;
258 return;
259 }
260 let mut ticker = tokio::time::interval(Duration::from_secs(ping_interval));
261 loop {
262 ticker.tick().await;
263 let payload = Utc::now().to_rfc3339();
264 let event = SessionEvent::Ping {
265 timestamp: crate::media::get_timestamp(),
266 payload: Some(payload),
267 };
268 if let Err(_) = active_call.event_sender.send(event) {
269 break;
270 }
271 }
272 };
273
274 let guard = ActiveCallGuard::new(active_call.clone());
275 info!(
276 session_id,
277 active_calls = guard.active_calls,
278 ?call_type,
279 "new call started"
280 );
281 let receiver = active_call.new_receiver();
282
283 let (r, _) = join! {
284 active_call.clone().serve(receiver),
285 async {
286 select!{
287 _ = send_ping_loop => {},
288 _ = cancel_token.cancelled() => {},
289 _ = send_events_loop => { },
290 _ = recv_commands_loop => {
291 info!(session_id, "Command receiver closed");
292 },
293 }
294 cancel_token.cancel();
295 }
296 };
297 while let Ok(event) = event_receiver.try_recv() {
299 if let Err(_) = event_sender_to_client.send(event) {
300 break;
301 }
302 }
303 match r {
304 Ok(_) => info!(session_id, "call ended successfully"),
305 Err(e) => warn!(session_id, "call ended with error: {}", e),
306 }
307
308 let final_extras = Some(active_call.extras.load_full().as_ref().clone());
310
311 active_call.cleanup().await.ok();
312 debug!(session_id, "Call handler core completed");
313
314 final_extras
315}
316
317pub async fn call_handler(
318 call_type: ActiveCallType,
319 ws: WebSocketUpgrade,
320 app_state: AppState,
321 params: CallParams,
322) -> Response {
323 let has_explicit_id = params.id.is_some();
324 let session_id = params
325 .id
326 .clone()
327 .unwrap_or_else(|| format!("s.{}", Uuid::new_v4().to_string()));
328 let server_side_track = params.server_side_track.clone();
329 let dump_events = params.dump_events.unwrap_or(true);
330 let ping_interval = params.ping_interval.unwrap_or(20);
331
332 if has_explicit_id {
342 let found_locally = {
343 let active_calls = app_state.active_calls.lock().unwrap();
344 active_calls.contains_key(&session_id)
345 } || app_state
346 .invitation
347 .find_dialog_id_by_session_id(&session_id)
348 .is_some();
349 if !found_locally {
350 if params.forward.is_none() {
351 if let Some(peer_ws) =
352 crate::handler::peer::try_forward(&app_state, &session_id, ¶ms).await
353 {
354 info!(session_id, "forwarding websocket to peer hosting the call");
355 return ws.on_upgrade(move |socket| async move {
356 crate::handler::peer::tunnel(socket, peer_ws).await;
357 });
358 }
359 }
360 if params.forward == Some(true) {
361 warn!(
362 session_id,
363 "call not found on this node, rejecting forwarded request"
364 );
365 return (
366 axum::http::StatusCode::NOT_FOUND,
367 "call not found on this node",
368 )
369 .into_response();
370 }
371 }
372 }
373
374 let resp = ws.on_upgrade(move |socket| async move {
375 let (mut ws_sender, mut ws_receiver) = socket.split();
376 let (audio_sender, audio_receiver) = tokio::sync::mpsc::unbounded_channel::<Bytes>();
377 let (command_sender, command_receiver) = tokio::sync::mpsc::unbounded_channel::<Command>();
378 let (event_sender_to_client, mut event_receiver_from_core) =
379 tokio::sync::mpsc::unbounded_channel::<crate::event::SessionEvent>();
380 let cancel_token = CancellationToken::new();
381
382 let session_id_clone = session_id.clone();
384 let app_state_clone = app_state.clone();
385 let cancel_token_clone = cancel_token.clone();
386 crate::spawn(async move {
387 call_handler_core(
388 call_type,
389 session_id_clone,
390 app_state_clone,
391 cancel_token_clone,
392 audio_receiver,
393 server_side_track,
394 dump_events,
395 ping_interval.into(),
396 command_receiver,
397 event_sender_to_client,
398 None, None, )
401 .await;
402 });
403
404 let recv_from_ws_loop = async {
406 while let Some(Ok(message)) = ws_receiver.next().await {
407 match message {
408 Message::Text(text) => {
409 let command = match serde_json::from_str::<Command>(&text) {
410 Ok(cmd) => cmd,
411 Err(e) => {
412 warn!(session_id, %text, "Failed to parse command {}", e);
413 continue;
414 }
415 };
416 if let Err(_) = command_sender.send(command) {
417 break;
418 }
419 }
420 Message::Binary(bin) => {
421 audio_sender.send(bin.into()).ok();
422 }
423 Message::Close(_) => {
424 info!(session_id, "WebSocket closed by client");
425 break;
426 }
427 _ => {}
428 }
429 }
430 };
431
432 let send_to_ws_loop = async {
433 while let Some(event) = event_receiver_from_core.recv().await {
434 trace!(session_id, %event, "Sending WS message");
435 let message = match event.into_ws_message() {
436 Ok(msg) => msg,
437 Err(e) => {
438 warn!(session_id, error=%e, "Failed to serialize event to WS message");
439 continue;
440 }
441 };
442 if let Err(_) = ws_sender.send(message).await {
443 info!(session_id, "WebSocket send failed, closing");
444 break;
445 }
446 }
447 };
448
449 select! {
450 _ = recv_from_ws_loop => {
451 info!(session_id, "WebSocket receive loop ended");
452 },
453 _ = send_to_ws_loop => {
454 info!(session_id, "WebSocket send loop ended");
455 },
456 }
457
458 cancel_token.cancel();
459 ws_sender.flush().await.ok();
460 ws_sender.close().await.ok();
461 debug!(session_id, "WebSocket connection closed");
462 });
463 resp
464}
465
466pub(crate) async fn get_iceservers(State(state): State<AppState>) -> Response {
467 if let Some(ice_servers) = state.config.ice_servers.as_ref() {
468 return Json(ice_servers).into_response();
469 }
470 Json(vec![IceServer {
471 urls: vec!["stun:stun.l.google.com:19302".to_string()],
472 ..Default::default()
473 }])
474 .into_response()
475}
476
477pub(crate) async fn list_active_calls(State(state): State<AppState>) -> Response {
478 let calls = state
481 .active_calls
482 .lock()
483 .unwrap()
484 .iter()
485 .map(|(_, c)| c.clone())
486 .collect::<Vec<_>>();
487 let list = calls
488 .iter()
489 .map(|c| {
490 let progress = c.progress.load_full();
491 json!({
492 "id": c.session_id,
493 "callType": c.call_type,
494 "cs.option": progress.option,
495 "ringTime": progress.ring_time,
496 "startTime": progress.answer_time,
497 })
498 })
499 .collect::<Vec<_>>();
500 Json(serde_json::json!({ "active_calls": list })).into_response()
501}
502
503pub(crate) async fn kill_active_call(
504 Path(id): Path<String>,
505 State(state): State<AppState>,
506) -> Response {
507 let active_calls = state.active_calls.lock().unwrap();
508 if let Some(call) = active_calls.get(&id) {
509 call.cancel_token.cancel();
510 Json(serde_json::json!({ "status": "killed", "id": id })).into_response()
511 } else {
512 (
513 axum::http::StatusCode::NOT_FOUND,
514 Json(serde_json::json!({ "status": "not_found", "id": id })),
515 )
516 .into_response()
517 }
518}
519
520pub(crate) async fn stream_events(
521 Path(id): Path<String>,
522 State(state): State<AppState>,
523) -> Response {
524 let mut rx_events;
525 let mut rx_commands;
526 {
527 let active_calls = state.active_calls.lock().unwrap();
528 if let Some(call) = active_calls.get(&id) {
529 rx_events = call.event_sender.subscribe();
530 rx_commands = call.cmd_sender.subscribe();
531 } else {
532 return (axum::http::StatusCode::NOT_FOUND, "track not active").into_response();
533 }
534 }
535
536 let stream = async_stream::stream! {
537 loop {
538 let result = tokio::select! {
539 r = rx_events.recv() => r.map(|e| serde_json::to_string(&e).map(|json| Event::default().event("event").data(json))),
540 r = rx_commands.recv() => r.map(|c| serde_json::to_string(&c).map(|json| Event::default().event("command").data(json))),
541 };
542 match result {
543 Ok(Ok(sse_event)) => yield Ok::<Event, serde_json::Error>(sse_event),
544 Ok(Err(e)) => yield Err(e.into()),
545 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
546 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
547 }
548 }
549 };
550
551 let mut response = Sse::new(stream)
552 .keep_alive(KeepAlive::default())
553 .into_response();
554 response.headers_mut().insert(
555 axum::http::header::CONTENT_TYPE,
556 "text/event-stream;charset=utf-8".parse().unwrap(),
557 );
558 response
559}
560
561pub(crate) async fn send_command(
562 Path(id): Path<String>,
563 State(state): State<AppState>,
564 Json(command): Json<Command>,
565) -> Response {
566 let active_calls = state.active_calls.lock().unwrap();
567 if let Some(call) = active_calls.get(&id) {
568 if let Ok(_) = call.cmd_sender.send(command) {
569 return Json(serde_json::json!({ "status": "sent", "id": id })).into_response();
570 }
571 }
572
573 (
574 axum::http::StatusCode::NOT_FOUND,
575 Json(serde_json::json!({ "status": "not_found", "id": id })),
576 )
577 .into_response()
578}
579
580pub(crate) async fn precache(
591 State(state): State<AppState>,
592 Json(command): Json<Command>,
593) -> Response {
594 let result = match command {
595 Command::Tts {
596 text,
597 speaker,
598 option,
599 cache_key,
600 ..
601 } => precache_tts(&state, text, speaker, option, cache_key).await,
602 _ => {
603 return (
604 axum::http::StatusCode::BAD_REQUEST,
605 Json(json!({ "status": "error", "error": "precache only accepts tts commands" })),
606 )
607 .into_response();
608 }
609 };
610
611 match result {
612 Ok((cache_key, bytes, existed)) => Json(json!({
613 "status": if existed { "exists" } else { "cached" },
614 "cacheKey": cache_key,
615 "bytes": bytes,
616 }))
617 .into_response(),
618 Err(e) => (
619 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
620 Json(json!({ "status": "error", "error": e.to_string() })),
621 )
622 .into_response(),
623 }
624}
625
626async fn precache_tts(
629 state: &AppState,
630 text: String,
631 speaker: Option<String>,
632 option: Option<SynthesisOption>,
633 cache_key: Option<String>,
634) -> anyhow::Result<(String, usize, bool)> {
635 let mut opt = option.ok_or_else(|| anyhow::anyhow!("tts precache requires an option"))?;
636 opt.speaker = speaker.or(opt.speaker);
638 opt.check_default();
639
640 let mut client = state.stream_engine.create_tts_client(false, &opt).await?;
641 let sample_rate = opt.samplerate.unwrap_or(16000) as u32;
643 let cache_key = cache_key.unwrap_or_else(|| {
644 cache::generate_cache_key(
645 &format!("tts:{}{}", client.provider(), text),
646 sample_rate,
647 opt.speaker.as_ref(),
648 opt.speed,
649 )
650 });
651 if cache::is_cached(&cache_key).await.unwrap_or(false) {
652 return Ok((cache_key, 0, true));
653 }
654
655 let mut stream = client.start().await?;
656 client.synthesize(&text, Some(0), Some(opt.clone())).await?;
657 client.stop().await?;
658
659 let mut chunks: Vec<Bytes> = Vec::new();
660 let mut first_chunk = true;
661 while let Some((_seq, res)) = stream.next().await {
662 match res? {
663 SynthesisEvent::AudioChunk(mut chunk) => {
664 if first_chunk {
666 if chunk.len() > 44 && chunk[..4] == [0x52, 0x49, 0x46, 0x46] {
667 let _ = chunk.split_to(44);
668 }
669 first_chunk = false;
670 }
671 chunks.push(chunk);
672 }
673 SynthesisEvent::Finished => break,
674 _ => {}
675 }
676 }
677 if chunks.is_empty() {
678 return Err(anyhow::anyhow!("tts produced no audio"));
679 }
680 let bytes: usize = chunks.iter().map(|c| c.len()).sum();
681 cache::store_in_cache_vectored(&cache_key, &chunks).await?;
682 info!(cache_key = %cache_key, bytes, "precache: stored tts audio");
683 Ok((cache_key, bytes, false))
684}
685
686trait IntoWsMessage {
687 fn into_ws_message(self) -> Result<Message, serde_json::Error>;
688}
689
690impl IntoWsMessage for crate::event::SessionEvent {
691 fn into_ws_message(self) -> Result<Message, serde_json::Error> {
692 match self {
693 SessionEvent::Binary { data, .. } => Ok(Message::Binary(data.into())),
694 SessionEvent::Ping { timestamp, payload } => {
695 let payload = payload.unwrap_or_else(|| timestamp.to_string());
696 Ok(Message::Ping(payload.into()))
697 }
698 event => serde_json::to_string(&event).map(|payload| Message::Text(payload.into())),
699 }
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use serde_json::json;
707 use std::collections::HashMap;
708
709 #[test]
710 fn test_filter_headers() {
711 let mut extras = HashMap::new();
712 extras.insert("X-Tenant-ID".to_string(), json!("123"));
713 extras.insert("X-User-ID".to_string(), json!("456"));
714 extras.insert("Custom-Header".to_string(), json!("abc"));
715 extras.insert("Irrelevant-Header".to_string(), json!("ignore"));
716
717 let allowed = vec!["x-tenant-id".to_string(), "Custom-Header".to_string()];
719
720 filter_headers(&mut extras, &allowed);
721
722 assert!(extras.contains_key("X-Tenant-ID"));
723 assert!(extras.contains_key("Custom-Header"));
724 assert!(!extras.contains_key("X-User-ID"));
725 assert!(!extras.contains_key("Irrelevant-Header"));
726
727 assert_eq!(extras.get("X-Tenant-ID").unwrap(), &json!("123"));
729 assert_eq!(extras.get("Custom-Header").unwrap(), &json!("abc"));
730 }
731
732 #[tokio::test]
733 async fn test_call_handler_core_extras_are_session_scoped() {
734 use crate::app::AppStateBuilder;
735 use crate::call::{ActiveCallType, Command};
736 use crate::config::Config;
737
738 let mut config = Config::default();
739 config.udp_port = 0;
740 let app_state = AppStateBuilder::new()
741 .with_config(config)
742 .build()
743 .await
744 .expect("Failed to build app state");
745
746 let session_id = "test-session-scoped".to_string();
747 let cancel_token = CancellationToken::new();
748
749 let mut extras = HashMap::new();
751 extras.insert("X-Custom".to_string(), json!("value"));
752
753 let (_audio_sender, audio_receiver) = tokio::sync::mpsc::unbounded_channel::<Bytes>();
754 let (command_sender, command_receiver) = tokio::sync::mpsc::unbounded_channel::<Command>();
755 let (event_sender, _event_receiver) =
756 tokio::sync::mpsc::unbounded_channel::<crate::event::SessionEvent>();
757
758 command_sender
760 .send(Command::Hangup {
761 reason: None,
762 initiator: None,
763 headers: None,
764 refer: None,
765 })
766 .ok();
767 drop(command_sender);
768
769 let final_extras = call_handler_core(
771 ActiveCallType::Sip,
772 session_id.clone(),
773 app_state.clone(),
774 cancel_token,
775 audio_receiver,
776 None,
777 false,
778 0,
779 command_receiver,
780 event_sender,
781 Some(extras), None, )
784 .await;
785
786 assert!(final_extras.is_some(), "final extras should be returned");
788 let extras = final_extras.unwrap();
789 assert_eq!(
790 extras.get("X-Custom"),
791 Some(&json!("value")),
792 "session-scoped extras should be preserved"
793 );
794 }
795}