Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(example): add handling of incoming call event #56

Merged
merged 4 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions examples/WebApi/Controllers/HandleIncomingIceEventController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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("event")]
public IActionResult HandleEvent([FromBody] IVoiceEvent incomingEvent)
{
switch (incomingEvent)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally Voice should provide a ValidateAuthenticationHeader function (like Conversation domains: https://github.com/sinch/sinch-sdk-dotnet/blob/main/src/Sinch/Conversation/Webhooks/Webhooks.cs#L72)

Calling ValidateAuthenticationHeader here and because this sample is a tutorial, it will help to highlight endpoint is having to be "protected" against unsolicited requests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, blocked by #62

{
case AnsweredCallEvent answeredCallEvent:
break;
case DisconnectedCallEvent disconnectedCallEvent:
break;
case IncomingCallEvent incomingCallEvent:
var response = new CallEventResponse()
{
Action = new Hangup(),
Instructions = new List<IInstruction>()
{
new Say()
{
Text = "Thank you for calling Sinch! This call will now end.",
Locale = "en-US"
}
}
};
return Ok(response);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: like the OK/BadRequest; need to think about having same "helper" for Java SDK to hide/help the response serialization requirements

case NotificationEvent notificationEvent:
break;
case PromptInputEvent promptInputEvent:
break;
default:
return BadRequest();
}

return BadRequest();
}
}
}
7 changes: 4 additions & 3 deletions src/Sinch/Voice/Hooks/AnsweredCallEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class AnsweredCallEvent
public class AnsweredCallEvent : IVoiceEvent
{
/// <summary>
/// Must have the value ace.
/// </summary>
[JsonPropertyName("event")]
[JsonInclude]
public string? Event { get; private set; }
public EventType? Event { get; set; }


/// <summary>
Expand Down Expand Up @@ -64,5 +63,7 @@ public class AnsweredCallEvent
/// </summary>
[JsonPropertyName("amd")]
public Amd? Amd { get; set; }

public EventType? EventType { get; }
Copy link

@JPPortier JPPortier Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems this field is not present from specs. Could be removed?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This PR was merged, but this field is still defined

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯 I was sure I fixed that, I'll open PR with a fix soon

}
}
4 changes: 2 additions & 2 deletions src/Sinch/Voice/Hooks/DisconnectedCallEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class DisconnectedCallEvent
public class DisconnectedCallEvent : IVoiceEvent
{
/// <summary>
/// Must have the value &#x60;dice&#x60;.
/// </summary>
[JsonPropertyName("event")]
public string? Event { get; set; }
public EventType? Event { get; set; }


/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Sinch/Voice/Hooks/EventType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
using Sinch.Core;

namespace Sinch.Voice.Hooks
{
[JsonConverter(typeof(EnumRecordJsonConverter<EventType>))]
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");
}
}
80 changes: 80 additions & 0 deletions src/Sinch/Voice/Hooks/IVoiceEvent.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Marker interface for event types of voice.
/// </summary>
[JsonConverter(typeof(InterfaceConverter<IVoiceEvent>))]
public interface IVoiceEvent
{
[JsonPropertyName("event")]
public EventType? Event { get; }
}

public class VoiceEventConverter : JsonConverter<IVoiceEvent>
{
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<NotificationEvent>(options);
}

if (type == EventType.IncomingCallEvent.Value)
{
return elem.Deserialize<IncomingCallEvent>(options);
}

if (type == EventType.DisconnectedCallEvent.Value)
{
return elem.Deserialize<DisconnectedCallEvent>(options);
}

if (type == EventType.AnsweredCallEvent.Value)
{
return elem.Deserialize<AnsweredCallEvent>(options);
}

if (type == EventType.PromptInputEvent.Value)
{
return elem.Deserialize<PromptInputEvent>(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)}");
}
}
}
}
4 changes: 2 additions & 2 deletions src/Sinch/Voice/Hooks/IncomingCallEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class IncomingCallEvent
public class IncomingCallEvent : IVoiceEvent
{
/// <summary>
/// Must have the value ice.
/// </summary>
[JsonPropertyName("event")]
public string? Event { get; set; }
public EventType? Event { get; set; }


/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Sinch/Voice/Hooks/NotificationEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ namespace Sinch.Voice.Hooks
/// <br /><br />
/// If there is no response to the callback within the timeout period, the notification is discarded.
/// </summary>
public class NotificationEvent
public class NotificationEvent : IVoiceEvent
{
/// <summary>
/// Must have the value notify.
/// </summary>
[JsonPropertyName("event")]
public string? Event { get; set; }
public EventType? Event { get; set; }

/// <summary>
/// The unique ID assigned to this call.
Expand Down
4 changes: 2 additions & 2 deletions src/Sinch/Voice/Hooks/PromtInputEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ namespace Sinch.Voice.Hooks
/// [SVAML](https://developers.sinch.com/docs/voice/api-reference/svaml/) logic.<br /><br />
/// Note: PIE callbacks are not issued for DATA Calls, only PSTN and SIP calls.
/// </summary>
public class PromptInputEvent
public class PromptInputEvent : IVoiceEvent
{
/// <summary>
/// Must have the value pie.
/// </summary>
[JsonPropertyName("event")]
public string? Event { get; set; }
public EventType? Event { get; set; }

/// <summary>
/// The unique ID assigned to this call.
Expand Down
Loading