use std::sync::Arc;
use rvoip_sip_core::types::Method;
use rvoip_sip_dialog::transaction::TransactionKey;
use crate::api::handle::CallId;
use crate::api::headers::{take_staged, BuilderHeaderState, SipRequestOptions};
use crate::api::incoming::ExactResponseObligation;
use crate::api::unified::UnifiedCoordinator;
use crate::errors::{Result, SessionError};
use crate::session_registry::SessionRegistryHandle;
pub struct GenericResponseBuilder {
coord: Arc<UnifiedCoordinator>,
call_id: CallId,
lifecycle_handle: Option<SessionRegistryHandle>,
method: Method,
status: u16,
reason: Option<String>,
exact_transaction: Option<TransactionKey>,
response_obligation: Option<Arc<ExactResponseObligation>>,
state: BuilderHeaderState,
}
impl GenericResponseBuilder {
pub(crate) fn new(
coord: Arc<UnifiedCoordinator>,
call_id: CallId,
method: Method,
status: u16,
) -> Result<Self> {
let lifecycle_handle = coord.helpers.state_machine.store.lifecycle_handle(&call_id);
Self::new_captured(coord, call_id, lifecycle_handle, method, status)
}
pub(crate) fn new_exact(
coord: Arc<UnifiedCoordinator>,
call_id: CallId,
lifecycle_handle: SessionRegistryHandle,
method: Method,
status: u16,
) -> Result<Self> {
Self::new_captured(coord, call_id, Some(lifecycle_handle), method, status)
}
pub(crate) fn new_captured(
coord: Arc<UnifiedCoordinator>,
call_id: CallId,
lifecycle_handle: Option<SessionRegistryHandle>,
method: Method,
status: u16,
) -> Result<Self> {
if !(300..=699).contains(&status) {
return Err(SessionError::InvalidInput(format!(
"GenericResponseBuilder status must be 3xx/4xx/5xx/6xx, got {status}"
)));
}
Ok(Self {
coord,
call_id,
lifecycle_handle,
method,
status,
reason: None,
exact_transaction: None,
response_obligation: None,
state: BuilderHeaderState::default(),
})
}
pub(crate) fn new_in_dialog(
coord: Arc<UnifiedCoordinator>,
call_id: CallId,
method: Method,
transaction_id: TransactionKey,
status: u16,
response_obligation: Arc<ExactResponseObligation>,
) -> Result<Self> {
if !(200..=699).contains(&status) {
return Err(SessionError::InvalidInput(format!(
"exact in-dialog response status must be 2xx/3xx/4xx/5xx/6xx, got {status}"
)));
}
if !transaction_id.is_server()
|| transaction_id.method() != &method
|| method == Method::Invite
{
return Err(SessionError::InvalidInput(
"exact in-dialog response requires the matching non-INVITE server transaction"
.to_string(),
));
}
Ok(Self {
coord,
call_id,
lifecycle_handle: None,
method,
status,
reason: None,
exact_transaction: Some(transaction_id),
response_obligation: Some(response_obligation),
state: BuilderHeaderState::default(),
})
}
pub fn with_reason(mut self, r: impl Into<String>) -> Self {
self.reason = Some(r.into());
self
}
pub async fn send(mut self) -> Result<()> {
let reason = self.reason.unwrap_or_else(|| "OK".to_string());
let extras = take_staged(&mut self.state);
if let Some(transaction_id) = self.exact_transaction.take() {
let obligation = self.response_obligation.take().ok_or_else(|| {
SessionError::InternalError(
"exact in-dialog response has no response obligation".to_string(),
)
})?;
let claim = obligation.claim()?;
let result = self
.coord
.dialog_adapter()
.send_response_with_options_for_transaction_classified(
&self.call_id,
&transaction_id,
self.status,
None,
extras,
)
.await;
return match result {
Ok(rvoip_sip_dialog::FinalResponseCompletionDisposition::WrittenSuccessTerminal) => {
claim.complete();
Ok(())
}
Ok(disposition) => {
claim.complete();
Err(SessionError::InternalError(format!(
"exact response returned nonterminal success disposition: {disposition:?}"
)))
}
Err(error)
if error.disposition
== rvoip_sip_dialog::FinalResponseCompletionDisposition::ZeroWireRetryable =>
{
claim.release_after_failure();
Err(SessionError::DialogError(format!(
"Failed to send exact in-dialog response: {}",
error.source
)))
}
Err(error) => {
claim.complete();
Err(SessionError::DialogError(format!(
"Exact in-dialog response became wire-unknown and will not be retried: {}",
error.source
)))
}
};
}
let lifecycle_handle = self
.lifecycle_handle
.as_ref()
.ok_or_else(|| SessionError::SessionNotFound(self.call_id.to_string()))?;
if (300..=399).contains(&self.status) {
self.coord
.helpers
.redirect_call_with_extras_exact(
lifecycle_handle,
self.status,
vec![reason],
extras,
)
.await
} else {
self.coord
.helpers
.reject_call_with_extras_exact(lifecycle_handle, self.status, &reason, extras)
.await
}
}
}
impl SipRequestOptions for GenericResponseBuilder {
fn method(&self) -> Method {
self.method.clone()
}
fn header_state_mut(&mut self) -> &mut BuilderHeaderState {
&mut self.state
}
fn header_state(&self) -> &BuilderHeaderState {
&self.state
}
}