From 4c6adf05885f725ac7e1b508350be9cf3ae20592 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Mon, 22 Apr 2024 10:48:04 +0200 Subject: [PATCH 1/2] feat(example): add handling of incoming call event --- .../HandleIncomingIceEventController.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 examples/WebApi/Controllers/HandleIncomingIceEventController.cs diff --git a/examples/WebApi/Controllers/HandleIncomingIceEventController.cs b/examples/WebApi/Controllers/HandleIncomingIceEventController.cs new file mode 100644 index 00000000..a71bf42e --- /dev/null +++ b/examples/WebApi/Controllers/HandleIncomingIceEventController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using Sinch; +using Sinch.Voice.Calls.Actions; +using Sinch.Voice.Calls.Instructions; +using Sinch.Voice.Hooks; + +namespace WebApiExamples.Controllers +{ + [ApiController] + [Route("voice")] + public class HandleIncomingIceEventController : ControllerBase + { + private readonly ISinchClient _sinchClient; + + public HandleIncomingIceEventController(ISinchClient sinchClient) + { + _sinchClient = sinchClient; + } + + [HttpPost] + [Route("ice-event")] + public IActionResult HandleEvent([FromBody] IncomingCallEvent incomingCallEvent) + { + var response = new CallEventResponse() + { + Action = new Hangup(), + Instructions = new List() + { + new Say() + { + Text = "Thank you for calling Sinch! This call will now end.", + Locale = "en-US" + } + } + }; + return Ok(response); + } + } +} From 15b1b61fbd6130116564e0483f3b0523e2fda0b0 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Tue, 23 Apr 2024 19:03:22 +0200 Subject: [PATCH 2/2] refactor(voice/hooks): add strong typed event type and marker interface for pattern matching --- .../HandleIncomingIceEventController.cs | 42 +++++++--- src/Sinch/Voice/Hooks/AnsweredCallEvent.cs | 7 +- .../Voice/Hooks/DisconnectedCallEvent.cs | 4 +- src/Sinch/Voice/Hooks/EventType.cs | 15 ++++ src/Sinch/Voice/Hooks/IVoiceEvent.cs | 80 +++++++++++++++++++ src/Sinch/Voice/Hooks/IncomingCallEvent.cs | 4 +- src/Sinch/Voice/Hooks/NotificationEvent.cs | 4 +- src/Sinch/Voice/Hooks/PromtInputEvent.cs | 4 +- 8 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 src/Sinch/Voice/Hooks/EventType.cs create mode 100644 src/Sinch/Voice/Hooks/IVoiceEvent.cs diff --git a/examples/WebApi/Controllers/HandleIncomingIceEventController.cs b/examples/WebApi/Controllers/HandleIncomingIceEventController.cs index a71bf42e..e1c8cfd4 100644 --- a/examples/WebApi/Controllers/HandleIncomingIceEventController.cs +++ b/examples/WebApi/Controllers/HandleIncomingIceEventController.cs @@ -18,22 +18,38 @@ public HandleIncomingIceEventController(ISinchClient sinchClient) } [HttpPost] - [Route("ice-event")] - public IActionResult HandleEvent([FromBody] IncomingCallEvent incomingCallEvent) + [Route("event")] + public IActionResult HandleEvent([FromBody] IVoiceEvent incomingEvent) { - var response = new CallEventResponse() + switch (incomingEvent) { - Action = new Hangup(), - Instructions = new List() - { - new Say() + case AnsweredCallEvent answeredCallEvent: + break; + case DisconnectedCallEvent disconnectedCallEvent: + break; + case IncomingCallEvent incomingCallEvent: + var response = new CallEventResponse() { - Text = "Thank you for calling Sinch! This call will now end.", - Locale = "en-US" - } - } - }; - return Ok(response); + Action = new Hangup(), + Instructions = new List() + { + new Say() + { + Text = "Thank you for calling Sinch! This call will now end.", + Locale = "en-US" + } + } + }; + return Ok(response); + case NotificationEvent notificationEvent: + break; + case PromptInputEvent promptInputEvent: + break; + default: + return BadRequest(); + } + + return BadRequest(); } } } diff --git a/src/Sinch/Voice/Hooks/AnsweredCallEvent.cs b/src/Sinch/Voice/Hooks/AnsweredCallEvent.cs index 20921da5..22cd01c9 100644 --- a/src/Sinch/Voice/Hooks/AnsweredCallEvent.cs +++ b/src/Sinch/Voice/Hooks/AnsweredCallEvent.cs @@ -14,14 +14,13 @@ namespace Sinch.Voice.Hooks /// enabled, the amd object will also be present on ACE callbacks. /// Note: ACE Callbacks are not issued for InApp Calls (destination: username), only PSTN and SIP calls. /// - public class AnsweredCallEvent + public class AnsweredCallEvent : IVoiceEvent { /// /// Must have the value ace. /// [JsonPropertyName("event")] - [JsonInclude] - public string? Event { get; private set; } + public EventType? Event { get; set; } /// @@ -64,5 +63,7 @@ public class AnsweredCallEvent /// [JsonPropertyName("amd")] public Amd? Amd { get; set; } + + public EventType? EventType { get; } } } diff --git a/src/Sinch/Voice/Hooks/DisconnectedCallEvent.cs b/src/Sinch/Voice/Hooks/DisconnectedCallEvent.cs index deb886ff..08ffcfb6 100644 --- a/src/Sinch/Voice/Hooks/DisconnectedCallEvent.cs +++ b/src/Sinch/Voice/Hooks/DisconnectedCallEvent.cs @@ -12,13 +12,13 @@ namespace Sinch.Voice.Hooks /// This event doesn't support instructions and only supports the /// [hangup](https://developers.sinch.com/docs/voice/api-reference/svaml/actions/#hangup) action. /// - public class DisconnectedCallEvent + public class DisconnectedCallEvent : IVoiceEvent { /// /// Must have the value `dice`. /// [JsonPropertyName("event")] - public string? Event { get; set; } + public EventType? Event { get; set; } /// diff --git a/src/Sinch/Voice/Hooks/EventType.cs b/src/Sinch/Voice/Hooks/EventType.cs new file mode 100644 index 00000000..aceabe52 --- /dev/null +++ b/src/Sinch/Voice/Hooks/EventType.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using Sinch.Core; + +namespace Sinch.Voice.Hooks +{ + [JsonConverter(typeof(EnumRecordJsonConverter))] + public record EventType(string Value) : EnumRecord(Value) + { + public static readonly EventType AnsweredCallEvent = new EventType("ace"); + public static readonly EventType DisconnectedCallEvent = new EventType("dice"); + public static readonly EventType IncomingCallEvent = new EventType("ice"); + public static readonly EventType NotificationEvent = new EventType("notify"); + public static readonly EventType PromptInputEvent = new EventType("pie"); + } +} diff --git a/src/Sinch/Voice/Hooks/IVoiceEvent.cs b/src/Sinch/Voice/Hooks/IVoiceEvent.cs new file mode 100644 index 00000000..07696796 --- /dev/null +++ b/src/Sinch/Voice/Hooks/IVoiceEvent.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Sinch.Core; + +namespace Sinch.Voice.Hooks +{ + /// + /// Marker interface for event types of voice. + /// + [JsonConverter(typeof(InterfaceConverter))] + public interface IVoiceEvent + { + [JsonPropertyName("event")] + public EventType? Event { get; } + } + + public class VoiceEventConverter : JsonConverter + { + public override IVoiceEvent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var elem = JsonElement.ParseValue(ref reader); + var descriptor = elem.EnumerateObject().FirstOrDefault(x => x.Name == "event"); + var type = descriptor.Value.GetString(); + + if (type == EventType.NotificationEvent.Value) + { + return elem.Deserialize(options); + } + + if (type == EventType.IncomingCallEvent.Value) + { + return elem.Deserialize(options); + } + + if (type == EventType.DisconnectedCallEvent.Value) + { + return elem.Deserialize(options); + } + + if (type == EventType.AnsweredCallEvent.Value) + { + return elem.Deserialize(options); + } + + if (type == EventType.PromptInputEvent.Value) + { + return elem.Deserialize(options); + } + + throw new JsonException($"Failed to match verification method object, got {descriptor.Name}"); + } + + public override void Write(Utf8JsonWriter writer, IVoiceEvent value, JsonSerializerOptions options) + { + switch (value) + { + case AnsweredCallEvent answeredCallEvent: + JsonSerializer.Serialize(writer, answeredCallEvent, options); + break; + case DisconnectedCallEvent disconnectedCallEvent: + JsonSerializer.Serialize(writer, disconnectedCallEvent, options); + break; + case IncomingCallEvent incomingCallEvent: + JsonSerializer.Serialize(writer, incomingCallEvent, options); + break; + case NotificationEvent notificationEvent: + JsonSerializer.Serialize(writer, notificationEvent, options); + break; + case PromptInputEvent promptInputEvent: + JsonSerializer.Serialize(writer, promptInputEvent, options); + break; + default: + throw new ArgumentOutOfRangeException(nameof(value), + $"Cannot find a matching class for the interface {nameof(IVoiceEvent)}"); + } + } + } +} diff --git a/src/Sinch/Voice/Hooks/IncomingCallEvent.cs b/src/Sinch/Voice/Hooks/IncomingCallEvent.cs index c4d7eee9..80a2e791 100644 --- a/src/Sinch/Voice/Hooks/IncomingCallEvent.cs +++ b/src/Sinch/Voice/Hooks/IncomingCallEvent.cs @@ -15,13 +15,13 @@ namespace Sinch.Voice.Hooks /// If there is no response to the callback within the timeout period, an error message is played, and the call is /// disconnected. /// - public class IncomingCallEvent + public class IncomingCallEvent : IVoiceEvent { /// /// Must have the value ice. /// [JsonPropertyName("event")] - public string? Event { get; set; } + public EventType? Event { get; set; } /// diff --git a/src/Sinch/Voice/Hooks/NotificationEvent.cs b/src/Sinch/Voice/Hooks/NotificationEvent.cs index cb0e7fe3..753be775 100644 --- a/src/Sinch/Voice/Hooks/NotificationEvent.cs +++ b/src/Sinch/Voice/Hooks/NotificationEvent.cs @@ -7,13 +7,13 @@ namespace Sinch.Voice.Hooks ///

/// If there is no response to the callback within the timeout period, the notification is discarded. ///
- public class NotificationEvent + public class NotificationEvent : IVoiceEvent { /// /// Must have the value notify. /// [JsonPropertyName("event")] - public string? Event { get; set; } + public EventType? Event { get; set; } /// /// The unique ID assigned to this call. diff --git a/src/Sinch/Voice/Hooks/PromtInputEvent.cs b/src/Sinch/Voice/Hooks/PromtInputEvent.cs index e9a7b6dc..fe813fc8 100644 --- a/src/Sinch/Voice/Hooks/PromtInputEvent.cs +++ b/src/Sinch/Voice/Hooks/PromtInputEvent.cs @@ -13,13 +13,13 @@ namespace Sinch.Voice.Hooks /// [SVAML](https://developers.sinch.com/docs/voice/api-reference/svaml/) logic.

/// Note: PIE callbacks are not issued for DATA Calls, only PSTN and SIP calls. ///
- public class PromptInputEvent + public class PromptInputEvent : IVoiceEvent { /// /// Must have the value pie. /// [JsonPropertyName("event")] - public string? Event { get; set; } + public EventType? Event { get; set; } /// /// The unique ID assigned to this call.