Skip to main content

freeswitch_types/conference_info/
endpoint.rs

1use std::fmt;
2use std::str::FromStr;
3
4use super::common::{ExecutionInfo, State};
5use super::media::Media;
6
7/// A participant's device/session within a conference (RFC 4575 Section 5.5).
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[non_exhaustive]
11pub struct Endpoint {
12    /// Unique endpoint identifier (required XML attribute), typically a SIP
13    /// contact URI with a `participantid` parameter.
14    #[cfg_attr(feature = "serde", serde(rename = "@entity"))]
15    pub entity: String,
16    /// Partial notification state.
17    #[cfg_attr(
18        feature = "serde",
19        serde(rename = "@state", default, skip_serializing_if = "Option::is_none")
20    )]
21    pub state: Option<State>,
22    /// Human-readable device name.
23    #[cfg_attr(
24        feature = "serde",
25        serde(
26            rename = "display-text",
27            default,
28            skip_serializing_if = "Option::is_none"
29        )
30    )]
31    pub display_text: Option<String>,
32    /// If this endpoint was REFER'd into the conference.
33    #[cfg_attr(
34        feature = "serde",
35        serde(default, skip_serializing_if = "Option::is_none")
36    )]
37    pub referred: Option<ExecutionInfo>,
38    /// Current connection status.
39    #[cfg_attr(
40        feature = "serde",
41        serde(default, skip_serializing_if = "Option::is_none")
42    )]
43    pub status: Option<EndpointStatus>,
44    /// How this endpoint joined the conference.
45    #[cfg_attr(
46        feature = "serde",
47        serde(
48            rename = "joining-method",
49            default,
50            skip_serializing_if = "Option::is_none"
51        )
52    )]
53    pub joining_method: Option<JoiningMethod>,
54    /// When and by whom this endpoint joined.
55    #[cfg_attr(
56        feature = "serde",
57        serde(
58            rename = "joining-info",
59            default,
60            skip_serializing_if = "Option::is_none"
61        )
62    )]
63    pub joining_info: Option<ExecutionInfo>,
64    /// How this endpoint disconnected.
65    #[cfg_attr(
66        feature = "serde",
67        serde(
68            rename = "disconnection-method",
69            default,
70            skip_serializing_if = "Option::is_none"
71        )
72    )]
73    pub disconnection_method: Option<DisconnectionMethod>,
74    /// When and by whom this endpoint was disconnected.
75    #[cfg_attr(
76        feature = "serde",
77        serde(
78            rename = "disconnection-info",
79            default,
80            skip_serializing_if = "Option::is_none"
81        )
82    )]
83    pub disconnection_info: Option<ExecutionInfo>,
84    /// Active media streams.
85    #[cfg_attr(
86        feature = "serde",
87        serde(rename = "media", default, skip_serializing_if = "Vec::is_empty")
88    )]
89    pub media: Vec<Media>,
90    /// SIP dialog identifiers for this endpoint's leg.
91    #[cfg_attr(
92        feature = "serde",
93        serde(rename = "call-info", default, skip_serializing_if = "Option::is_none")
94    )]
95    pub call_info: Option<CallInfo>,
96}
97
98impl Endpoint {
99    /// Create an endpoint with the given entity URI.
100    pub fn new(entity: impl Into<String>) -> Self {
101        Self {
102            entity: entity.into(),
103            state: None,
104            display_text: None,
105            referred: None,
106            status: None,
107            joining_method: None,
108            joining_info: None,
109            disconnection_method: None,
110            disconnection_info: None,
111            media: Vec::new(),
112            call_info: None,
113        }
114    }
115
116    /// Set the connection status.
117    pub fn with_status(mut self, status: EndpointStatus) -> Self {
118        self.status = Some(status);
119        self
120    }
121
122    /// Set the joining method.
123    pub fn with_joining_method(mut self, method: JoiningMethod) -> Self {
124        self.joining_method = Some(method);
125        self
126    }
127
128    /// Set the joining info.
129    pub fn with_joining_info(mut self, info: ExecutionInfo) -> Self {
130        self.joining_info = Some(info);
131        self
132    }
133
134    /// Add a media stream.
135    pub fn with_media(mut self, media: Media) -> Self {
136        self.media
137            .push(media);
138        self
139    }
140
141    /// Set the SIP call info.
142    pub fn with_call_info(mut self, call_info: CallInfo) -> Self {
143        self.call_info = Some(call_info);
144        self
145    }
146}
147
148/// Connection status of a conference endpoint (RFC 4575 Section 5.5).
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151#[non_exhaustive]
152pub enum EndpointStatus {
153    /// Awaiting response from endpoint.
154    #[cfg_attr(feature = "serde", serde(rename = "pending"))]
155    Pending,
156    /// Focus is dialing the endpoint.
157    #[cfg_attr(feature = "serde", serde(rename = "dialing-out"))]
158    DialingOut,
159    /// Endpoint is dialing the focus.
160    #[cfg_attr(feature = "serde", serde(rename = "dialing-in"))]
161    DialingIn,
162    /// Ringing, before answer.
163    #[cfg_attr(feature = "serde", serde(rename = "alerting"))]
164    Alerting,
165    /// Participant is on hold.
166    #[cfg_attr(feature = "serde", serde(rename = "on-hold"))]
167    OnHold,
168    /// Active in the conference.
169    #[cfg_attr(feature = "serde", serde(rename = "connected"))]
170    Connected,
171    /// Audio suppressed by the focus.
172    #[cfg_attr(feature = "serde", serde(rename = "muted-via-focus"))]
173    MutedViaFocus,
174    /// Teardown in progress.
175    #[cfg_attr(feature = "serde", serde(rename = "disconnecting"))]
176    Disconnecting,
177    /// Departed or rejected.
178    #[cfg_attr(feature = "serde", serde(rename = "disconnected"))]
179    Disconnected,
180}
181
182impl fmt::Display for EndpointStatus {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.write_str(match self {
185            Self::Pending => "pending",
186            Self::DialingOut => "dialing-out",
187            Self::DialingIn => "dialing-in",
188            Self::Alerting => "alerting",
189            Self::OnHold => "on-hold",
190            Self::Connected => "connected",
191            Self::MutedViaFocus => "muted-via-focus",
192            Self::Disconnecting => "disconnecting",
193            Self::Disconnected => "disconnected",
194        })
195    }
196}
197
198/// Error returned when parsing an invalid [`EndpointStatus`] string.
199#[derive(Debug, Clone)]
200pub struct ParseEndpointStatusError(pub String);
201
202impl fmt::Display for ParseEndpointStatusError {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "invalid endpoint status: {:?}", self.0)
205    }
206}
207
208impl std::error::Error for ParseEndpointStatusError {}
209
210impl FromStr for EndpointStatus {
211    type Err = ParseEndpointStatusError;
212
213    fn from_str(s: &str) -> Result<Self, Self::Err> {
214        match s {
215            "pending" => Ok(Self::Pending),
216            "dialing-out" => Ok(Self::DialingOut),
217            "dialing-in" => Ok(Self::DialingIn),
218            "alerting" => Ok(Self::Alerting),
219            "on-hold" => Ok(Self::OnHold),
220            "connected" => Ok(Self::Connected),
221            "muted-via-focus" => Ok(Self::MutedViaFocus),
222            "disconnecting" => Ok(Self::Disconnecting),
223            "disconnected" => Ok(Self::Disconnected),
224            other => Err(ParseEndpointStatusError(other.to_owned())),
225        }
226    }
227}
228
229/// How an endpoint joined the conference (RFC 4575 Section 5.5).
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232#[non_exhaustive]
233pub enum JoiningMethod {
234    /// Endpoint dialed the focus (incoming).
235    #[cfg_attr(feature = "serde", serde(rename = "dialed-in"))]
236    DialedIn,
237    /// Focus dialed the endpoint (outgoing).
238    #[cfg_attr(feature = "serde", serde(rename = "dialed-out"))]
239    DialedOut,
240    /// Endpoint is the focus itself.
241    #[cfg_attr(feature = "serde", serde(rename = "focus-owner"))]
242    FocusOwner,
243}
244
245impl fmt::Display for JoiningMethod {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        f.write_str(match self {
248            Self::DialedIn => "dialed-in",
249            Self::DialedOut => "dialed-out",
250            Self::FocusOwner => "focus-owner",
251        })
252    }
253}
254
255/// Error returned when parsing an invalid [`JoiningMethod`] string.
256#[derive(Debug, Clone)]
257pub struct ParseJoiningMethodError(pub String);
258
259impl fmt::Display for ParseJoiningMethodError {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(f, "invalid joining method: {:?}", self.0)
262    }
263}
264
265impl std::error::Error for ParseJoiningMethodError {}
266
267impl FromStr for JoiningMethod {
268    type Err = ParseJoiningMethodError;
269
270    fn from_str(s: &str) -> Result<Self, Self::Err> {
271        match s {
272            "dialed-in" => Ok(Self::DialedIn),
273            "dialed-out" => Ok(Self::DialedOut),
274            "focus-owner" => Ok(Self::FocusOwner),
275            other => Err(ParseJoiningMethodError(other.to_owned())),
276        }
277    }
278}
279
280/// How an endpoint left the conference (RFC 4575 Section 5.5).
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
282#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
283#[non_exhaustive]
284pub enum DisconnectionMethod {
285    /// Endpoint-initiated BYE.
286    #[cfg_attr(feature = "serde", serde(rename = "departed"))]
287    Departed,
288    /// Focus rejected or removed the endpoint.
289    #[cfg_attr(feature = "serde", serde(rename = "booted"))]
290    Booted,
291    /// Connection attempt failed.
292    #[cfg_attr(feature = "serde", serde(rename = "failed"))]
293    Failed,
294    /// Endpoint returned busy (486).
295    #[cfg_attr(feature = "serde", serde(rename = "busy"))]
296    Busy,
297}
298
299impl fmt::Display for DisconnectionMethod {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        f.write_str(match self {
302            Self::Departed => "departed",
303            Self::Booted => "booted",
304            Self::Failed => "failed",
305            Self::Busy => "busy",
306        })
307    }
308}
309
310/// Error returned when parsing an invalid [`DisconnectionMethod`] string.
311#[derive(Debug, Clone)]
312pub struct ParseDisconnectionMethodError(pub String);
313
314impl fmt::Display for ParseDisconnectionMethodError {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        write!(f, "invalid disconnection method: {:?}", self.0)
317    }
318}
319
320impl std::error::Error for ParseDisconnectionMethodError {}
321
322impl FromStr for DisconnectionMethod {
323    type Err = ParseDisconnectionMethodError;
324
325    fn from_str(s: &str) -> Result<Self, Self::Err> {
326        match s {
327            "departed" => Ok(Self::Departed),
328            "booted" => Ok(Self::Booted),
329            "failed" => Ok(Self::Failed),
330            "busy" => Ok(Self::Busy),
331            other => Err(ParseDisconnectionMethodError(other.to_owned())),
332        }
333    }
334}
335
336/// SIP dialog identifiers for a conference endpoint's call leg.
337#[derive(Debug, Clone, PartialEq, Eq, Default)]
338#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
339#[non_exhaustive]
340pub struct CallInfo {
341    /// SIP dialog identifier triplet.
342    #[cfg_attr(
343        feature = "serde",
344        serde(default, skip_serializing_if = "Option::is_none")
345    )]
346    pub sip: Option<SipDialogId>,
347}
348
349impl CallInfo {
350    /// Create a call-info with SIP dialog identifiers.
351    pub fn with_sip(sip: SipDialogId) -> Self {
352        Self { sip: Some(sip) }
353    }
354}
355
356/// SIP dialog identifier triplet (Call-ID, From-tag, To-tag) that uniquely
357/// identifies a SIP dialog per RFC 3261.
358#[derive(Debug, Clone, PartialEq, Eq, Default)]
359#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360#[non_exhaustive]
361pub struct SipDialogId {
362    /// Human-readable description.
363    #[cfg_attr(
364        feature = "serde",
365        serde(
366            rename = "display-text",
367            default,
368            skip_serializing_if = "Option::is_none"
369        )
370    )]
371    pub display_text: Option<String>,
372    /// SIP Call-ID header value.
373    #[cfg_attr(
374        feature = "serde",
375        serde(rename = "call-id", default, skip_serializing_if = "Option::is_none")
376    )]
377    pub call_id: Option<String>,
378    /// SIP From header tag parameter.
379    #[cfg_attr(
380        feature = "serde",
381        serde(rename = "from-tag", default, skip_serializing_if = "Option::is_none")
382    )]
383    pub from_tag: Option<String>,
384    /// SIP To header tag parameter.
385    #[cfg_attr(
386        feature = "serde",
387        serde(rename = "to-tag", default, skip_serializing_if = "Option::is_none")
388    )]
389    pub to_tag: Option<String>,
390}
391
392impl SipDialogId {
393    /// Create a SIP dialog ID from the three identifying components.
394    pub fn new(
395        call_id: impl Into<String>,
396        from_tag: impl Into<String>,
397        to_tag: impl Into<String>,
398    ) -> Self {
399        Self {
400            display_text: None,
401            call_id: Some(call_id.into()),
402            from_tag: Some(from_tag.into()),
403            to_tag: Some(to_tag.into()),
404        }
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn endpoint_status_round_trip() {
414        for (s, expected) in [
415            ("pending", EndpointStatus::Pending),
416            ("dialing-out", EndpointStatus::DialingOut),
417            ("dialing-in", EndpointStatus::DialingIn),
418            ("alerting", EndpointStatus::Alerting),
419            ("on-hold", EndpointStatus::OnHold),
420            ("connected", EndpointStatus::Connected),
421            ("muted-via-focus", EndpointStatus::MutedViaFocus),
422            ("disconnecting", EndpointStatus::Disconnecting),
423            ("disconnected", EndpointStatus::Disconnected),
424        ] {
425            let parsed: EndpointStatus = s
426                .parse()
427                .unwrap();
428            assert_eq!(parsed, expected);
429            assert_eq!(parsed.to_string(), s);
430        }
431    }
432
433    #[test]
434    fn joining_method_round_trip() {
435        for (s, expected) in [
436            ("dialed-in", JoiningMethod::DialedIn),
437            ("dialed-out", JoiningMethod::DialedOut),
438            ("focus-owner", JoiningMethod::FocusOwner),
439        ] {
440            let parsed: JoiningMethod = s
441                .parse()
442                .unwrap();
443            assert_eq!(parsed, expected);
444            assert_eq!(parsed.to_string(), s);
445        }
446    }
447
448    #[test]
449    fn disconnection_method_round_trip() {
450        for (s, expected) in [
451            ("departed", DisconnectionMethod::Departed),
452            ("booted", DisconnectionMethod::Booted),
453            ("failed", DisconnectionMethod::Failed),
454            ("busy", DisconnectionMethod::Busy),
455        ] {
456            let parsed: DisconnectionMethod = s
457                .parse()
458                .unwrap();
459            assert_eq!(parsed, expected);
460            assert_eq!(parsed.to_string(), s);
461        }
462    }
463
464    #[test]
465    fn sip_dialog_id_new() {
466        let id = SipDialogId::new("call-123", "from-abc", "to-xyz");
467        assert_eq!(
468            id.call_id
469                .as_deref(),
470            Some("call-123")
471        );
472        assert_eq!(
473            id.from_tag
474                .as_deref(),
475            Some("from-abc")
476        );
477        assert_eq!(
478            id.to_tag
479                .as_deref(),
480            Some("to-xyz")
481        );
482        assert!(id
483            .display_text
484            .is_none());
485    }
486
487    #[test]
488    fn endpoint_builder() {
489        let ep = Endpoint::new("sip:alice@example.com")
490            .with_status(EndpointStatus::Connected)
491            .with_joining_method(JoiningMethod::DialedIn)
492            .with_media(super::super::media::Media::new("1").with_type("audio"));
493        assert_eq!(ep.entity, "sip:alice@example.com");
494        assert_eq!(ep.status, Some(EndpointStatus::Connected));
495        assert_eq!(
496            ep.media
497                .len(),
498            1
499        );
500    }
501}