use crate::call::app::{
AppAction, ApplicationContext, CallApp, CallAppType, CallController, RecordingInfo,
};
use crate::metrics;
use crate::addons::voicemail::models::message;
use crate::addons::voicemail::notifier::{NewMessageEvent, Notifier, NotifierChain};
use crate::addons::voicemail::storage::VoicemailStorage;
use async_trait::async_trait;
use chrono::Utc;
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tracing::{info, warn};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct VoicemailSounds {
pub greeting_default: String,
pub beep: String,
pub saved: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum VoicemailState {
Init,
PlayingGreeting,
PlayingBeep,
Recording,
PlayingSaved,
Done,
}
pub struct VoicemailApp {
pub extension: String,
pub box_id: Uuid,
pub caller_id: String,
pub mailbox_email: Option<String>,
pub state: VoicemailState,
pub recording_timestamp: i64,
pub spool_path: Option<PathBuf>,
pub max_duration: Duration,
pub storage: Arc<VoicemailStorage>,
pub db: DatabaseConnection,
pub custom_greeting_path: Option<String>,
pub notifier: Arc<NotifierChain>,
pub sounds: VoicemailSounds,
}
impl VoicemailApp {
pub fn new(
extension: String,
box_id: Uuid,
caller_id: String,
mailbox_email: Option<String>,
max_duration: Duration,
storage: Arc<VoicemailStorage>,
db: DatabaseConnection,
custom_greeting_path: Option<String>,
notifier: Arc<NotifierChain>,
sounds: VoicemailSounds,
) -> Self {
Self {
extension,
box_id,
caller_id,
mailbox_email,
state: VoicemailState::Init,
recording_timestamp: 0,
spool_path: None,
max_duration,
storage,
db,
custom_greeting_path,
notifier,
sounds,
}
}
async fn resolve_greeting(&self) -> String {
if let Some(ref key) = self.custom_greeting_path {
if let Some(local) = self.storage.local_path(key) {
if local.exists() {
return local.to_string_lossy().into_owned();
}
} else {
return key.clone();
}
}
self.sounds.greeting_default.clone()
}
async fn persist_message(
storage: Arc<VoicemailStorage>,
db: DatabaseConnection,
notifier: Arc<NotifierChain>,
box_id: Uuid,
caller_id: String,
extension: String,
mailbox_email: Option<String>,
spool_path: PathBuf,
timestamp: i64,
duration_secs: i32,
) {
let audio_path = match storage
.upload_recording(&spool_path, &extension, timestamp)
.await
{
Ok(key) => key,
Err(e) => {
warn!(
extension = %extension,
spool = ?spool_path,
"voicemail upload failed: {}; keeping spool path", e
);
spool_path.to_string_lossy().into_owned()
}
};
info!(
extension = %extension,
audio_path = %audio_path,
duration = duration_secs,
"voicemail message stored"
);
metrics::voicemail::message_received(&extension);
metrics::voicemail::message_duration_seconds(duration_secs as f64, &extension);
let row = message::ActiveModel {
id: Set(Uuid::new_v4()),
box_id: Set(box_id),
caller_id: Set(caller_id.clone()),
duration: Set(duration_secs),
audio_path: Set(audio_path.clone()),
read: Set(false),
created_at: Set(Utc::now().naive_utc()),
transcript: Set(None),
summary: Set(None),
};
if let Err(e) = row.insert(&db).await {
warn!(extension = %extension, "failed to insert voicemail_message: {}", e);
return;
}
if !notifier.is_empty() {
let event = NewMessageEvent {
extension: extension.clone(),
email: mailbox_email,
caller_id,
duration_secs,
transcript: None,
audio_url: Some(audio_path),
};
if let Err(e) = notifier.notify(&event).await {
warn!(extension = %extension, "voicemail notifier error: {}", e);
}
}
}
}
#[async_trait]
impl CallApp for VoicemailApp {
fn app_type(&self) -> CallAppType {
CallAppType::Voicemail
}
fn name(&self) -> &str {
"voicemail"
}
async fn on_enter(
&mut self,
ctrl: &mut CallController,
_ctx: &ApplicationContext,
) -> anyhow::Result<AppAction> {
ctrl.answer().await?;
let greeting = self.resolve_greeting().await;
ctrl.play_audio(greeting, true).await?;
self.state = VoicemailState::PlayingGreeting;
Ok(AppAction::Continue)
}
async fn on_audio_complete(
&mut self,
_track_id: String,
ctrl: &mut CallController,
_ctx: &ApplicationContext,
) -> anyhow::Result<AppAction> {
match self.state {
VoicemailState::PlayingGreeting => {
ctrl.play_audio(&self.sounds.beep, true).await?;
self.state = VoicemailState::PlayingBeep;
Ok(AppAction::Continue)
}
VoicemailState::PlayingBeep => {
let ts = Utc::now().timestamp();
self.recording_timestamp = ts;
let spool_path = self.storage.spool_path(&self.extension, ts);
let spool_str = spool_path.to_string_lossy().into_owned();
self.spool_path = Some(spool_path);
ctrl.start_recording(spool_str, Some(self.max_duration), false)
.await?;
self.state = VoicemailState::Recording;
Ok(AppAction::Continue)
}
VoicemailState::PlayingSaved => {
self.state = VoicemailState::Done;
Ok(AppAction::Hangup {
reason: None,
code: None,
})
}
_ => Ok(AppAction::Continue),
}
}
async fn on_record_complete(
&mut self,
info: RecordingInfo,
ctrl: &mut CallController,
_ctx: &ApplicationContext,
) -> anyhow::Result<AppAction> {
if self.state != VoicemailState::Recording {
return Ok(AppAction::Continue);
}
let duration_secs = info.duration.as_secs() as i32;
match self.spool_path.take() {
Some(spool_path) if duration_secs > 0 => {
let storage = self.storage.clone();
let db = self.db.clone();
let notifier = self.notifier.clone();
let box_id = self.box_id;
let caller_id = self.caller_id.clone();
let extension = self.extension.clone();
let email = self.mailbox_email.clone();
let ts = self.recording_timestamp;
tokio::spawn(Self::persist_message(
storage,
db,
notifier,
box_id,
caller_id,
extension,
email,
spool_path,
ts,
duration_secs,
));
}
Some(spool_path) => {
warn!(spool = ?spool_path, "voicemail recording has zero duration, discarding");
self.storage.discard_spool(&spool_path).await;
}
None => {
warn!(extension = %self.extension, "on_record_complete with no spool_path");
}
}
ctrl.play_audio(&self.sounds.saved, true).await?;
self.state = VoicemailState::PlayingSaved;
Ok(AppAction::Continue)
}
async fn on_dtmf(
&mut self,
digit: String,
ctrl: &mut CallController,
_ctx: &ApplicationContext,
) -> anyhow::Result<AppAction> {
if self.state == VoicemailState::Recording && digit == "#" {
ctrl.stop_recording().await?;
}
Ok(AppAction::Continue)
}
}