diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..16b6ff5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased +- Added support for the `` BXML verb (including `` child, `referCompleteUrl`, `referCompleteMethod`, and optional `tag`). +- Added support for the `referComplete` webhook via `ReferCompleteCallback`. +- Added support for `referCallStatus`, optional `referSipResponseCode`, and optional `notifySipResponseCode` fields. + +> Note: This change should not be released before https://github.com/Bandwidth/api-specs/pull/2142 is merged. diff --git a/README.md b/README.md index 70cbdf8..a7cd432 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,33 @@ namespace Example } ``` +## BXML `` example + +```csharp +using Bandwidth.Standard.Model.Bxml; +using Bandwidth.Standard.Model.Bxml.Verbs; + +var refer = new Refer() + .WithSipUri("sip:alice@atlanta.example.com") + .WithReferCompleteUrl("https://example.com/handleRefer") + .WithReferCompleteMethod("POST") + .WithTag("handoff-123"); + +var bxml = new Response(refer).ToBXML(); +``` + +When a REFER succeeds, the remote SIP endpoint redirects away from Bandwidth and the call is terminated. Do not expect post-success BXML execution on that call leg. + +## `referComplete` failure recovery example + +```csharp +// Example callback handler pseudocode +if (callback.ReferCallStatus == ReferCallStatusEnum.Failure) +{ + // Recover only on failure (e.g. fallback flow, retry, alternate route) +} +``` + ## Documentation for API Endpoints @@ -328,6 +355,8 @@ Class | Method | HTTP request | Description - [Model.RecordingStateEnum](docs/RecordingStateEnum.md) - [Model.RecordingTranscriptionMetadata](docs/RecordingTranscriptionMetadata.md) - [Model.RecordingTranscriptions](docs/RecordingTranscriptions.md) + - [Model.ReferCallStatusEnum](docs/ReferCallStatusEnum.md) + - [Model.ReferCompleteCallback](docs/ReferCompleteCallback.md) - [Model.RedirectCallback](docs/RedirectCallback.md) - [Model.RedirectMethodEnum](docs/RedirectMethodEnum.md) - [Model.SipConnectionMetadata](docs/SipConnectionMetadata.md) @@ -391,4 +420,3 @@ Authentication schemes defined for the API: - **Flow**: application - **Authorization URL**: https://api.bandwidth.com/api/v1/oauth2/token - **Scopes**: N/A - diff --git a/docs/ReferCallStatusEnum.md b/docs/ReferCallStatusEnum.md new file mode 100644 index 0000000..0e29b68 --- /dev/null +++ b/docs/ReferCallStatusEnum.md @@ -0,0 +1,8 @@ +# Bandwidth.Standard.Model.ReferCallStatusEnum + +## Enum + +* `Success` (value: `success`) +* `Failure` (value: `failure`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/docs/ReferCompleteCallback.md b/docs/ReferCompleteCallback.md new file mode 100644 index 0000000..a1fb454 --- /dev/null +++ b/docs/ReferCompleteCallback.md @@ -0,0 +1,24 @@ +# Bandwidth.Standard.Model.ReferCompleteCallback +This event is sent to the referCompleteUrl of the A-leg's verb when the REFER attempt completes. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**EventType** | **string** | The event type. Always `referComplete`. | [optional] +**EventTime** | **DateTime** | The approximate UTC date and time when the event was generated by the Bandwidth server. | [optional] +**AccountId** | **string** | The user account associated with the call. | [optional] +**ApplicationId** | **string** | The id of the application associated with the call. | [optional] +**From** | **string** | The provided identifier of the caller. | [optional] +**To** | **string** | The phone number that received the call. | [optional] +**Direction** | **CallDirectionEnum** | Call direction (`inbound`). | [optional] +**CallId** | **string** | The call id associated with the event. | [optional] +**CallUrl** | **string** | The URL of the call associated with the event. | [optional] +**StartTime** | **DateTime** | Time the call was started. | [optional] +**AnswerTime** | **DateTime** | Time the call was answered. | [optional] +**ReferCallStatus** | **ReferCallStatusEnum** | Whether REFER succeeded or failed. | [optional] +**Tag** | **string** | Optional custom string included in callbacks. | [optional] +**ReferSipResponseCode** | **int?** | Optional SIP response code from the REFER transaction. | [optional] +**NotifySipResponseCode** | **int?** | Optional SIP response code from the NOTIFY transaction. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs b/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs new file mode 100644 index 0000000..188fe3e --- /dev/null +++ b/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Xml.Serialization; +using Bandwidth.Standard.Model.Bxml; +using Bandwidth.Standard.Model.Bxml.Verbs; +using Xunit; + +namespace Bandwidth.Standard.Test.Unit.Model.Bxml +{ +public class TestRefer +{ +[Fact] +public void ReferRoundTripTest() +{ +var expected = " sip:alice@atlanta.example.com "; + +var refer = new Refer() +.WithSipUri("sip:alice@atlanta.example.com") +.WithReferCompleteUrl("https://example.com/handleRefer") +.WithReferCompleteMethod("POST") +.WithTag("refer-tag"); + +var actual = new Response(refer).ToBXML(); +Assert.Equal(expected, actual.Replace("\n", "").Replace("\r", "")); + +const string referOnlyXml = "sip:alice@atlanta.example.com"; +var serializer = new XmlSerializer(typeof(Refer), ""); +Refer deserializedRefer; +using (var reader = new StringReader(referOnlyXml)) +{ +deserializedRefer = (Refer)serializer.Deserialize(reader); +} + +Assert.Equal("sip:alice@atlanta.example.com", deserializedRefer.SipUriElement.Uri); +var roundTrip = new Response(deserializedRefer).ToBXML(); +Assert.Equal(expected, roundTrip.Replace("\n", "").Replace("\r", "")); +} + +[Fact] +public void ReferSipUriMustStartWithSipScheme() +{ +var refer = new Refer(); +Assert.Throws(() => refer.WithSipUri("tel:+15551234567")); +} +} +} diff --git a/src/Bandwidth.Standard.Test/Unit/Model/ReferCompleteCallbackTests.cs b/src/Bandwidth.Standard.Test/Unit/Model/ReferCompleteCallbackTests.cs new file mode 100644 index 0000000..ac78555 --- /dev/null +++ b/src/Bandwidth.Standard.Test/Unit/Model/ReferCompleteCallbackTests.cs @@ -0,0 +1,58 @@ +using Bandwidth.Standard.Model; +using Newtonsoft.Json; +using Xunit; + +namespace Bandwidth.Standard.Test.Unit.Model +{ + public class ReferCompleteCallbackTests + { + [Fact] + public void ReferCompleteSuccessTest() + { + var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"success\"}"); + + Assert.Equal(ReferCallStatusEnum.Success, callback.ReferCallStatus); + Assert.Null(callback.ReferSipResponseCode); + Assert.Null(callback.NotifySipResponseCode); + } + + [Fact] + public void ReferCompleteReferRejectedTest() + { + var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"failure\",\"referSipResponseCode\":405}"); + + Assert.Equal(ReferCallStatusEnum.Failure, callback.ReferCallStatus); + Assert.Equal(405, callback.ReferSipResponseCode); + Assert.Null(callback.NotifySipResponseCode); + } + + [Fact] + public void ReferCompleteDestinationUnreachableTest() + { + var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"failure\",\"referSipResponseCode\":202,\"notifySipResponseCode\":486}"); + + Assert.Equal(ReferCallStatusEnum.Failure, callback.ReferCallStatus); + Assert.Equal(202, callback.ReferSipResponseCode); + Assert.Equal(486, callback.NotifySipResponseCode); + } + + [Fact] + public void ReferCompleteNotifyTimeoutTest() + { + var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"failure\",\"referSipResponseCode\":202}"); + + Assert.Equal(ReferCallStatusEnum.Failure, callback.ReferCallStatus); + Assert.Equal(202, callback.ReferSipResponseCode); + Assert.Null(callback.NotifySipResponseCode); + } + + private static ReferCompleteCallback Deserialize(string json) + { + var callback = JsonConvert.DeserializeObject(json); + Assert.NotNull(callback); + Assert.Equal("referComplete", callback.EventType); + Assert.Equal(CallDirectionEnum.Inbound, callback.Direction); + return callback; + } + } +} diff --git a/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs new file mode 100644 index 0000000..2aac375 --- /dev/null +++ b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs @@ -0,0 +1,130 @@ +using Bandwidth.Standard.Model.Bxml; +using System; +using System.Xml.Serialization; + +namespace Bandwidth.Standard.Model.Bxml.Verbs +{ + /// + /// The Refer verb is used to hand off a call to a SIP endpoint. + /// + /// + public class Refer : IVerb + { + private string _referCompleteMethod; + + /// + /// URL to receive the refer complete callback. + /// + [XmlAttribute("referCompleteUrl")] + public string ReferCompleteUrl { get; set; } + + /// + /// HTTP method to send the refer complete callback. + /// + [XmlAttribute("referCompleteMethod")] + public string ReferCompleteMethod + { + get { return _referCompleteMethod; } + set + { + if (value != null && value != "GET" && value != "POST") + { + throw new ArgumentException("ReferCompleteMethod must be either 'GET' or 'POST'."); + } + + _referCompleteMethod = value; + } + } + + /// + /// Optional custom string to include in callbacks. + /// + [XmlAttribute("tag")] + public string Tag { get; set; } + + /// + /// SIP URI destination for the REFER. + /// + [XmlElement("SipUri")] + public SipUri SipUriElement { get; set; } + + /// + /// Initializes a new instance of the Refer class. + /// + public Refer() + { + ReferCompleteMethod = "POST"; + } + + /// + /// Sets the SIP URI destination. + /// + public Refer WithSipUri(string sipUri) + { + SipUriElement = new SipUri { Uri = sipUri }; + return this; + } + + /// + /// Sets the SIP URI destination. + /// + public Refer WithSipUri(SipUri sipUri) + { + SipUriElement = sipUri; + return this; + } + + /// + /// Sets referCompleteUrl. + /// + public Refer WithReferCompleteUrl(string referCompleteUrl) + { + ReferCompleteUrl = referCompleteUrl; + return this; + } + + /// + /// Sets referCompleteMethod. + /// + public Refer WithReferCompleteMethod(string referCompleteMethod) + { + ReferCompleteMethod = referCompleteMethod; + return this; + } + + /// + /// Sets tag. + /// + public Refer WithTag(string tag) + { + Tag = tag; + return this; + } + + /// + /// BXML tag to represent a SIP URI for the refer verb. + /// + public class SipUri : IVerb + { + private string _uri; + + /// + /// SIP URI to refer the call to (must start with sip:). + /// + [XmlText] + public string Uri + { + get { return _uri; } + set + { + if (value != null && !value.StartsWith("sip:", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("SipUri must start with 'sip:'."); + } + + _uri = value; + } + } + } + } +} diff --git a/src/Bandwidth.Standard/Model/ReferCallStatusEnum.cs b/src/Bandwidth.Standard/Model/ReferCallStatusEnum.cs new file mode 100644 index 0000000..b6c6f5b --- /dev/null +++ b/src/Bandwidth.Standard/Model/ReferCallStatusEnum.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Bandwidth.Standard.Model +{ + /// + /// Result of a refer attempt. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum ReferCallStatusEnum + { + /// + /// Enum Success for value: success + /// + [EnumMember(Value = "success")] + Success = 1, + + /// + /// Enum Failure for value: failure + /// + [EnumMember(Value = "failure")] + Failure = 2 + } +} diff --git a/src/Bandwidth.Standard/Model/ReferCompleteCallback.cs b/src/Bandwidth.Standard/Model/ReferCompleteCallback.cs new file mode 100644 index 0000000..9164af6 --- /dev/null +++ b/src/Bandwidth.Standard/Model/ReferCompleteCallback.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using System.Text; + +namespace Bandwidth.Standard.Model +{ + /// + /// This event is sent to the referCompleteUrl of the A-leg's <Refer> verb when the REFER attempt completes. + /// + [DataContract(Name = "referCompleteCallback")] + public partial class ReferCompleteCallback : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + public ReferCompleteCallback( + string eventType = default(string), + DateTime eventTime = default(DateTime), + string accountId = default(string), + string applicationId = default(string), + string from = default(string), + string to = default(string), + CallDirectionEnum? direction = default(CallDirectionEnum?), + string callId = default(string), + string callUrl = default(string), + DateTime startTime = default(DateTime), + DateTime answerTime = default(DateTime), + ReferCallStatusEnum referCallStatus = default(ReferCallStatusEnum), + string tag = default(string), + int? referSipResponseCode = default(int?), + int? notifySipResponseCode = default(int?)) + { + EventType = eventType; + EventTime = eventTime; + AccountId = accountId; + ApplicationId = applicationId; + From = from; + To = to; + Direction = direction; + CallId = callId; + CallUrl = callUrl; + StartTime = startTime; + AnswerTime = answerTime; + ReferCallStatus = referCallStatus; + Tag = tag; + ReferSipResponseCode = referSipResponseCode; + NotifySipResponseCode = notifySipResponseCode; + } + + /// + /// The event type. Always referComplete. + /// + [DataMember(Name = "eventType", EmitDefaultValue = false)] + public string EventType { get; set; } + + /// + /// The approximate UTC date and time when the event was generated by the Bandwidth server. + /// + [DataMember(Name = "eventTime", EmitDefaultValue = false)] + public DateTime EventTime { get; set; } + + /// + /// The user account associated with the call. + /// + [DataMember(Name = "accountId", EmitDefaultValue = false)] + public string AccountId { get; set; } + + /// + /// The id of the application associated with the call. + /// + [DataMember(Name = "applicationId", EmitDefaultValue = false)] + public string ApplicationId { get; set; } + + /// + /// The provided identifier of the caller. + /// + [DataMember(Name = "from", EmitDefaultValue = false)] + public string From { get; set; } + + /// + /// The phone number that received the call. + /// + [DataMember(Name = "to", EmitDefaultValue = false)] + public string To { get; set; } + + /// + /// Call direction. + /// + [DataMember(Name = "direction", EmitDefaultValue = false)] + public CallDirectionEnum? Direction { get; set; } + + /// + /// The call id associated with the event. + /// + [DataMember(Name = "callId", EmitDefaultValue = false)] + public string CallId { get; set; } + + /// + /// The URL of the call associated with the event. + /// + [DataMember(Name = "callUrl", EmitDefaultValue = false)] + public string CallUrl { get; set; } + + /// + /// Time the call was started. + /// + [DataMember(Name = "startTime", EmitDefaultValue = false)] + public DateTime StartTime { get; set; } + + /// + /// Time the call was answered. + /// + [DataMember(Name = "answerTime", EmitDefaultValue = false)] + public DateTime AnswerTime { get; set; } + + /// + /// Whether REFER succeeded or failed. + /// + [DataMember(Name = "referCallStatus", EmitDefaultValue = false)] + public ReferCallStatusEnum ReferCallStatus { get; set; } + + /// + /// Optional custom string included in callbacks. + /// + [DataMember(Name = "tag", EmitDefaultValue = true)] + public string Tag { get; set; } + + /// + /// Optional SIP response code from the REFER transaction. + /// + [DataMember(Name = "referSipResponseCode", EmitDefaultValue = true)] + public int? ReferSipResponseCode { get; set; } + + /// + /// Optional SIP response code from the NOTIFY transaction. + /// + [DataMember(Name = "notifySipResponseCode", EmitDefaultValue = true)] + public int? NotifySipResponseCode { get; set; } + + /// + /// Returns the string presentation of the object. + /// + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class ReferCompleteCallback {\n"); + sb.Append(" EventType: ").Append(EventType).Append("\n"); + sb.Append(" EventTime: ").Append(EventTime).Append("\n"); + sb.Append(" AccountId: ").Append(AccountId).Append("\n"); + sb.Append(" ApplicationId: ").Append(ApplicationId).Append("\n"); + sb.Append(" From: ").Append(From).Append("\n"); + sb.Append(" To: ").Append(To).Append("\n"); + sb.Append(" Direction: ").Append(Direction).Append("\n"); + sb.Append(" CallId: ").Append(CallId).Append("\n"); + sb.Append(" CallUrl: ").Append(CallUrl).Append("\n"); + sb.Append(" StartTime: ").Append(StartTime).Append("\n"); + sb.Append(" AnswerTime: ").Append(AnswerTime).Append("\n"); + sb.Append(" ReferCallStatus: ").Append(ReferCallStatus).Append("\n"); + sb.Append(" Tag: ").Append(Tag).Append("\n"); + sb.Append(" ReferSipResponseCode: ").Append(ReferSipResponseCode).Append("\n"); + sb.Append(" NotifySipResponseCode: ").Append(NotifySipResponseCode).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object. + /// + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance. + /// + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } +}