ftth_rsipstack/dialog/
invitation.rs

1use super::{
2    authenticate::Credential,
3    client_dialog::ClientInviteDialog,
4    dialog::{DialogInner, DialogStateSender},
5    dialog_layer::DialogLayer,
6};
7use crate::rsip;
8use crate::{
9    dialog::{dialog::Dialog, dialog_layer::DialogLayerInnerRef, DialogId},
10    transaction::{
11        key::{TransactionKey, TransactionRole},
12        make_tag,
13        transaction::Transaction,
14    },
15    transport::SipAddr,
16    Result,
17};
18use rsip::{Request, Response};
19use std::sync::Arc;
20use tracing::{debug, info, warn};
21
22/// INVITE Request Options
23///
24/// `InviteOption` contains all the parameters needed to create and send
25/// an INVITE request to establish a SIP session. This structure provides
26/// a convenient way to specify all the necessary information for initiating
27/// a call or session.
28///
29/// # Fields
30///
31/// * `caller` - URI of the calling party (From header)
32/// * `callee` - URI of the called party (To header and Request-URI)
33/// * `content_type` - MIME type of the message body (default: "application/sdp")
34/// * `offer` - Optional message body (typically SDP offer)
35/// * `contact` - Contact URI for this user agent
36/// * `credential` - Optional authentication credentials
37/// * `headers` - Optional additional headers to include
38///
39/// # Examples
40///
41/// ## Basic Voice Call
42///
43/// ```rust,no_run
44/// # use ftth_rsipstack::dialog::invitation::InviteOption;
45/// # fn example() -> ftth_rsipstack::Result<()> {
46/// # let sdp_offer_bytes = vec![];
47/// let invite_option = InviteOption {
48///     caller: "sip:alice@example.com".try_into()?,
49///     callee: "sip:bob@example.com".try_into()?,
50///     content_type: Some("application/sdp".to_string()),
51///     destination: None,
52///     offer: Some(sdp_offer_bytes),
53///     contact: "sip:alice@192.168.1.100:5060".try_into()?,
54///     credential: None,
55///     headers: None,
56/// };
57/// # Ok(())
58/// # }
59/// ```
60///
61/// ```rust,no_run
62/// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
63/// # use ftth_rsipstack::dialog::invitation::InviteOption;
64/// # fn example() -> ftth_rsipstack::Result<()> {
65/// # let dialog_layer: DialogLayer = todo!();
66/// # let invite_option: InviteOption = todo!();
67/// let request = dialog_layer.make_invite_request(&invite_option)?;
68/// println!("Created INVITE to: {}", request.uri);
69/// # Ok(())
70/// # }
71/// ```
72///
73/// ## Call with Custom Headers
74///
75/// ```rust,no_run
76/// # use ftth_rsipstack::dialog::invitation::InviteOption;
77/// # fn example() -> ftth_rsipstack::Result<()> {
78/// # let sdp_bytes = vec![];
79/// # let auth_credential = todo!();
80/// let custom_headers = vec![
81///     rsip::Header::UserAgent("MyApp/1.0".into()),
82///     rsip::Header::Subject("Important Call".into()),
83/// ];
84///
85/// let invite_option = InviteOption {
86///     caller: "sip:alice@example.com".try_into()?,
87///     callee: "sip:bob@example.com".try_into()?,
88///     content_type: Some("application/sdp".to_string()),
89///     destination: None,
90///     offer: Some(sdp_bytes),
91///     contact: "sip:alice@192.168.1.100:5060".try_into()?,
92///     credential: Some(auth_credential),
93///     headers: Some(custom_headers),
94/// };
95/// # Ok(())
96/// # }
97/// ```
98///
99/// ## Call with Authentication
100///
101/// ```rust,no_run
102/// # use ftth_rsipstack::dialog::invitation::InviteOption;
103/// # use ftth_rsipstack::dialog::authenticate::Credential;
104/// # fn example() -> ftth_rsipstack::Result<()> {
105/// # let sdp_bytes = vec![];
106/// let credential = Credential {
107///     username: "alice".to_string(),
108///     password: "secret123".to_string(),
109///     realm: Some("example.com".to_string()),
110/// };
111///
112/// let invite_option = InviteOption {
113///     caller: "sip:alice@example.com".try_into()?,
114///     callee: "sip:bob@example.com".try_into()?,
115///     content_type: None, // Will default to "application/sdp"
116///     destination: None,
117///     offer: Some(sdp_bytes),
118///     contact: "sip:alice@192.168.1.100:5060".try_into()?,
119///     credential: Some(credential),
120///     headers: None,
121/// };
122/// # Ok(())
123/// # }
124/// ```
125#[derive(Default, Clone)]
126pub struct InviteOption {
127    pub caller: rsip::Uri,
128    pub callee: rsip::Uri,
129    pub destination: Option<SipAddr>,
130    pub content_type: Option<String>,
131    pub offer: Option<Vec<u8>>,
132    pub contact: rsip::Uri,
133    pub credential: Option<Credential>,
134    pub headers: Option<Vec<rsip::Header>>,
135}
136
137pub struct DialogGuard {
138    pub dialog_layer_inner: DialogLayerInnerRef,
139    pub id: DialogId,
140}
141
142impl DialogGuard {
143    pub fn new(dialog_layer: &Arc<DialogLayer>, id: DialogId) -> Self {
144        Self {
145            dialog_layer_inner: dialog_layer.inner.clone(),
146            id,
147        }
148    }
149}
150
151impl Drop for DialogGuard {
152    fn drop(&mut self) {
153        let dlg = match self.dialog_layer_inner.dialogs.write() {
154            Ok(mut dialogs) => match dialogs.remove(&self.id) {
155                Some(dlg) => dlg,
156                None => return,
157            },
158            _ => return,
159        };
160        let _ = tokio::spawn(async move {
161            if let Err(e) = dlg.hangup().await {
162                info!(id=%dlg.id(), "failed to hangup dialog: {}", e);
163            }
164        });
165    }
166}
167
168pub(super) struct DialogGuardForUnconfirmed<'a> {
169    pub dialog_layer_inner: &'a DialogLayerInnerRef,
170    pub id: &'a DialogId,
171}
172
173impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
174    fn drop(&mut self) {
175        // If the dialog is still unconfirmed, we should try to cancel it
176        match self.dialog_layer_inner.dialogs.write() {
177            Ok(mut dialogs) => {
178                if let Some(dlg) = dialogs.get(self.id) {
179                    if !dlg.can_cancel() {
180                        return;
181                    }
182                    match dialogs.remove(self.id) {
183                        Some(dlg) => {
184                            info!(%self.id, "unconfirmed dialog dropped, cancelling it");
185                            let _ = tokio::spawn(async move {
186                                if let Err(e) = dlg.hangup().await {
187                                    info!(id=%dlg.id(), "failed to hangup unconfirmed dialog: {}", e);
188                                }
189                            });
190                        }
191                        None => {}
192                    }
193                }
194            }
195            Err(e) => {
196                warn!(%self.id, "failed to acquire write lock on dialogs: {}", e);
197            }
198        }
199    }
200}
201
202impl DialogLayer {
203    /// Create an INVITE request from options
204    ///
205    /// Constructs a properly formatted SIP INVITE request based on the
206    /// provided options. This method handles all the required headers
207    /// and parameters according to RFC 3261.
208    ///
209    /// # Parameters
210    ///
211    /// * `opt` - INVITE options containing all necessary parameters
212    ///
213    /// # Returns
214    ///
215    /// * `Ok(Request)` - Properly formatted INVITE request
216    /// * `Err(Error)` - Failed to create request
217    ///
218    /// # Generated Headers
219    ///
220    /// The method automatically generates:
221    /// * Via header with branch parameter
222    /// * From header with tag parameter
223    /// * To header (without tag for initial request)
224    /// * Contact header
225    /// * Content-Type header
226    /// * CSeq header with incremented sequence number
227    /// * Call-ID header
228    ///
229    /// # Examples
230    ///
231    /// ```rust,no_run
232    /// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
233    /// # use ftth_rsipstack::dialog::invitation::InviteOption;
234    /// # fn example() -> ftth_rsipstack::Result<()> {
235    /// # let dialog_layer: DialogLayer = todo!();
236    /// # let invite_option: InviteOption = todo!();
237    /// let request = dialog_layer.make_invite_request(&invite_option)?;
238    /// println!("Created INVITE to: {}", request.uri);
239    /// # Ok(())
240    /// # }
241    /// ```
242    pub fn make_invite_request(&self, opt: &InviteOption) -> Result<Request> {
243        let last_seq = self.increment_last_seq();
244        let to = rsip::typed::To {
245            display_name: None,
246            uri: opt.callee.clone(),
247            params: vec![],
248        };
249        let recipient = to.uri.clone();
250
251        let form = rsip::typed::From {
252            display_name: None,
253            uri: opt.caller.clone(),
254            params: vec![],
255        }
256        .with_tag(make_tag());
257
258        let via = self.endpoint.get_via(None, None)?;
259        let mut request =
260            self.endpoint
261                .make_request(rsip::Method::Invite, recipient, via, form, to, last_seq);
262
263        let contact = rsip::typed::Contact {
264            display_name: None,
265            uri: opt.contact.clone(),
266            params: vec![],
267        };
268
269        request
270            .headers
271            .unique_push(rsip::Header::Contact(contact.into()));
272
273        request.headers.unique_push(rsip::Header::ContentType(
274            opt.content_type
275                .clone()
276                .unwrap_or("application/sdp".to_string())
277                .into(),
278        ));
279        // can override default headers
280        if let Some(headers) = opt.headers.as_ref() {
281            for header in headers {
282                request.headers.unique_push(header.clone());
283            }
284        }
285        Ok(request)
286    }
287
288    /// Send an INVITE request and create a client dialog
289    ///
290    /// This is the main method for initiating outbound calls. It creates
291    /// an INVITE request, sends it, and manages the resulting dialog.
292    /// The method handles the complete INVITE transaction including
293    /// authentication challenges and response processing.
294    ///
295    /// # Parameters
296    ///
297    /// * `opt` - INVITE options containing all call parameters
298    /// * `state_sender` - Channel for receiving dialog state updates
299    ///
300    /// # Returns
301    ///
302    /// * `Ok((ClientInviteDialog, Option<Response>))` - Created dialog and final response
303    /// * `Err(Error)` - Failed to send INVITE or process responses
304    ///
305    /// # Call Flow
306    ///
307    /// 1. Creates INVITE request from options
308    /// 2. Creates client dialog and transaction
309    /// 3. Sends INVITE request
310    /// 4. Processes responses (1xx, 2xx, 3xx-6xx)
311    /// 5. Handles authentication challenges if needed
312    /// 6. Returns established dialog and final response
313    ///
314    /// # Examples
315    ///
316    /// ## Basic Call Setup
317    ///
318    /// ```rust,no_run
319    /// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
320    /// # use ftth_rsipstack::dialog::invitation::InviteOption;
321    /// # async fn example() -> ftth_rsipstack::Result<()> {
322    /// # let dialog_layer: DialogLayer = todo!();
323    /// # let invite_option: InviteOption = todo!();
324    /// # let state_sender = todo!();
325    /// let (dialog, response) = dialog_layer.do_invite(invite_option, state_sender).await?;
326    ///
327    /// if let Some(resp) = response {
328    ///     match resp.status_code {
329    ///         rsip::StatusCode::OK => {
330    ///             println!("Call answered!");
331    ///             // Process SDP answer in resp.body
332    ///         },
333    ///         rsip::StatusCode::BusyHere => {
334    ///             println!("Called party is busy");
335    ///         },
336    ///         _ => {
337    ///             println!("Call failed: {}", resp.status_code);
338    ///         }
339    ///     }
340    /// }
341    /// # Ok(())
342    /// # }
343    /// ```
344    ///
345    /// ## Monitoring Dialog State
346    ///
347    /// ```rust,no_run
348    /// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
349    /// # use ftth_rsipstack::dialog::invitation::InviteOption;
350    /// # use ftth_rsipstack::dialog::dialog::DialogState;
351    /// # async fn example() -> ftth_rsipstack::Result<()> {
352    /// # let dialog_layer: DialogLayer = todo!();
353    /// # let invite_option: InviteOption = todo!();
354    /// let (state_tx, mut state_rx) = tokio::sync::mpsc::unbounded_channel();
355    /// let (dialog, response) = dialog_layer.do_invite(invite_option, state_tx).await?;
356    ///
357    /// // Monitor dialog state changes
358    /// tokio::spawn(async move {
359    ///     while let Some(state) = state_rx.recv().await {
360    ///         match state {
361    ///             DialogState::Early(_, resp) => {
362    ///                 println!("Ringing: {}", resp.status_code);
363    ///             },
364    ///             DialogState::Confirmed(_,_) => {
365    ///                 println!("Call established");
366    ///             },
367    ///             DialogState::Terminated(_, code) => {
368    ///                 println!("Call ended: {:?}", code);
369    ///                 break;
370    ///             },
371    ///             _ => {}
372    ///         }
373    ///     }
374    /// });
375    /// # Ok(())
376    /// # }
377    /// ```
378    ///
379    /// # Error Handling
380    ///
381    /// The method can fail for various reasons:
382    /// * Network connectivity issues
383    /// * Authentication failures
384    /// * Invalid SIP URIs or headers
385    /// * Transaction timeouts
386    /// * Protocol violations
387    ///
388    /// # Authentication
389    ///
390    /// If credentials are provided in the options, the method will
391    /// automatically handle 401/407 authentication challenges by
392    /// resending the request with proper authentication headers.
393    pub async fn do_invite(
394        &self,
395        opt: InviteOption,
396        state_sender: DialogStateSender,
397    ) -> Result<(ClientInviteDialog, Option<Response>)> {
398        let (dialog, tx) = self.create_client_invite_dialog(opt, state_sender)?;
399        let id = dialog.id();
400        self.inner
401            .dialogs
402            .write()
403            .unwrap()
404            .insert(id.clone(), Dialog::ClientInvite(dialog.clone()));
405        info!(%id, "client invite dialog created");
406
407        let _guard = DialogGuardForUnconfirmed {
408            dialog_layer_inner: &self.inner,
409            id: &id,
410        };
411
412        match dialog.process_invite(tx).await {
413            Ok((new_dialog_id, resp)) => {
414                debug!(
415                    "client invite dialog confirmed: {} => {}",
416                    id, new_dialog_id
417                );
418                self.inner.dialogs.write().unwrap().remove(&id);
419                // update with new dialog id
420                self.inner
421                    .dialogs
422                    .write()
423                    .unwrap()
424                    .insert(new_dialog_id, Dialog::ClientInvite(dialog.clone()));
425                return Ok((dialog, resp));
426            }
427            Err(e) => {
428                self.inner.dialogs.write().unwrap().remove(&id);
429                return Err(e);
430            }
431        }
432    }
433
434    pub fn create_client_invite_dialog(
435        &self,
436        opt: InviteOption,
437        state_sender: DialogStateSender,
438    ) -> Result<(ClientInviteDialog, Transaction)> {
439        let mut request = self.make_invite_request(&opt)?;
440        request.body = opt.offer.unwrap_or_default();
441        request.headers.unique_push(rsip::Header::ContentLength(
442            (request.body.len() as u32).into(),
443        ));
444        let key = TransactionKey::from_request(&request, TransactionRole::Client)?;
445        let mut tx = Transaction::new_client(key, request.clone(), self.endpoint.clone(), None);
446        tx.destination = opt.destination;
447
448        let id = DialogId::try_from(&request)?;
449        let mut dlg_inner = DialogInner::new(
450            TransactionRole::Client,
451            id.clone(),
452            request.clone(),
453            self.endpoint.clone(),
454            state_sender,
455            opt.credential,
456            Some(opt.contact),
457            tx.tu_sender.clone(),
458        )?;
459        dlg_inner.initial_destination = tx.destination.clone();
460
461        let dialog = ClientInviteDialog {
462            inner: Arc::new(dlg_inner),
463        };
464        Ok((dialog, tx))
465    }
466
467    pub fn confirm_client_dialog(&self, dialog: ClientInviteDialog) {
468        self.inner
469            .dialogs
470            .write()
471            .unwrap()
472            .insert(dialog.id(), Dialog::ClientInvite(dialog));
473    }
474}