-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
12eddba
commit 8f02304
Showing
30 changed files
with
740 additions
and
0 deletions.
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.sln
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
|
||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio Version 17 | ||
VisualStudioVersion = 17.5.33516.290 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CallAutomation_AppointmentBooking", "CallAutomation_AppointmentBooking\CallAutomation_AppointmentBooking.csproj", "{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {31E6D6F2-4041-4119-AFF5-F575C0D8C7F6} | ||
EndGlobalSection | ||
EndGlobal |
43 changes: 43 additions & 0 deletions
43
...tomation_AppointmentBooking/CallAutomation_AppointmentBooking/AppointmentBookingConfig.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
namespace CallAutomation_AppointmentBooking | ||
{ | ||
public class AppointmentBookingConfig | ||
{ | ||
/// <summary> | ||
/// public Callback URI that will be used to answer incoming call event, | ||
/// or handle mid-call events, such as CallConnected. | ||
/// See README file for details on how to setup tunnel on your localhost to handle this. | ||
/// </summary> | ||
public Uri CallbackUri { get; init; } | ||
|
||
/// <summary> | ||
/// DirectOffered phonenumber is can be aquired from Azure Communication Service portal. | ||
/// In order to answer Incoming PSTN call or make an outbound call to PSTN number, | ||
/// Call Automation needs Directly offered PSTN number to do these actions. | ||
/// </summary> | ||
public string DirectOfferedPhonenumber { get; init; } | ||
|
||
/// <summary> | ||
/// List of all prompts from this sample's business logic. | ||
/// These recorded prompts must be uploaded to publicily available Uri endpoint. | ||
/// See README for pre-generated samples that can be used for demo. | ||
/// </summary> | ||
public Prompts AllPrompts { get; init; } | ||
|
||
public class Prompts | ||
{ | ||
public Uri MainMenu { get; init; } | ||
|
||
public Uri Retry { get; init; } | ||
|
||
public Uri Choice1 { get; init; } | ||
|
||
public Uri Choice2 { get; init; } | ||
|
||
public Uri Choice3 { get; init; } | ||
|
||
public Uri PlayRecordingStarted { get; init; } | ||
|
||
public Uri Goodbye { get; init; } | ||
} | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
...intmentBooking/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net7.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<UserSecretsId>d7d2fc43-754d-4dba-87ed-832e765c7a4d</UserSecretsId> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Azure.Communication.CallAutomation" Version="1.0.0-alpha.20230516.1" /> | ||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.16.0" /> | ||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> | ||
</ItemGroup> | ||
|
||
</Project> |
88 changes: 88 additions & 0 deletions
88
CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/CallingModules.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
using Azure.Communication; | ||
using Azure.Communication.CallAutomation; | ||
using CallAutomation_AppointmentBooking.Interfaces; | ||
|
||
namespace CallAutomation_AppointmentBooking | ||
{ | ||
/// <summary> | ||
/// Reusuable common calling actions for business needs | ||
/// </summary> | ||
public class CallingModules : ICallingModules | ||
{ | ||
private readonly CallConnection _callConnection; | ||
private readonly AppointmentBookingConfig _appointmentBookingConfig; | ||
|
||
public CallingModules( | ||
CallConnection callConnection, | ||
AppointmentBookingConfig appointmentBookingConfig) | ||
{ | ||
_callConnection = callConnection; | ||
_appointmentBookingConfig = appointmentBookingConfig; | ||
} | ||
|
||
public async Task<string> RecognizeTonesAsync( | ||
CommunicationIdentifier targetToRecognize, | ||
int minDigitToCollect, | ||
int maxDigitToCollect, | ||
Uri askPrompt, | ||
Uri retryPrompt) | ||
{ | ||
for (int i = 0; i < 3; i++) | ||
{ | ||
// prepare recognize tones | ||
CallMediaRecognizeDtmfOptions callMediaRecognizeDtmfOptions = new CallMediaRecognizeDtmfOptions(targetToRecognize, maxDigitToCollect); | ||
callMediaRecognizeDtmfOptions.Prompt = new FileSource(askPrompt); | ||
callMediaRecognizeDtmfOptions.InterruptPrompt = true; | ||
callMediaRecognizeDtmfOptions.InitialSilenceTimeout = TimeSpan.FromSeconds(10); | ||
callMediaRecognizeDtmfOptions.InterToneTimeout = TimeSpan.FromSeconds(10); | ||
callMediaRecognizeDtmfOptions.StopTones = new List<DtmfTone> { DtmfTone.Pound, DtmfTone.Asterisk }; | ||
|
||
// Send request to recognize tones | ||
StartRecognizingCallMediaResult startRecognizingResult = await _callConnection.GetCallMedia().StartRecognizingAsync(callMediaRecognizeDtmfOptions); | ||
|
||
// Wait for recognize related event... | ||
StartRecognizingEventResult recognizeEventResult = await startRecognizingResult.WaitForEventProcessorAsync(); | ||
|
||
if (recognizeEventResult.IsSuccess) | ||
{ | ||
// success recognition - return the tones detected. | ||
RecognizeCompleted recognizeCompleted = recognizeEventResult.SuccessResult; | ||
string dtmfTones = ((DtmfResult)recognizeCompleted.RecognizeResult).ConvertToString(); | ||
|
||
// check if it collected the minimum digit it collected | ||
if (dtmfTones.Length >= minDigitToCollect) | ||
{ | ||
return dtmfTones; | ||
} | ||
} | ||
else | ||
{ | ||
// failed recognition - likely timeout | ||
_ = recognizeEventResult.FailureResult; | ||
} | ||
|
||
// play retry prompt and retry again | ||
await PlayMessageThenWaitUntilItEndsAsync(retryPrompt); | ||
} | ||
|
||
throw new Exception("Retried 3 times, Failed to get tones."); | ||
} | ||
|
||
|
||
public async Task PlayMessageThenWaitUntilItEndsAsync(Uri playPrompt) | ||
{ | ||
// Play failure prompt and retry. | ||
FileSource fileSource = new FileSource(playPrompt); | ||
PlayResult playResult = await _callConnection.GetCallMedia().PlayToAllAsync(fileSource); | ||
|
||
// ... wait for play to complete, then return | ||
await playResult.WaitForEventProcessorAsync(); | ||
} | ||
|
||
public async Task TerminateCallAsync() | ||
{ | ||
// Terminate the call | ||
await _callConnection.HangUpAsync(true); | ||
} | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
...ation_AppointmentBooking/CallAutomation_AppointmentBooking/Controllers/EventController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using Azure.Communication.CallAutomation; | ||
using Azure.Messaging; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace CallAutomation_AppointmentBooking.Controllers | ||
{ | ||
/// <summary> | ||
/// This is controller where it will recieve interim events from Call automation service. | ||
/// We are utilizing event processor, this will handle events and relay to our business logic. | ||
/// </summary> | ||
[Route("api/[controller]")] | ||
[ApiController] | ||
public class EventController : ControllerBase | ||
{ | ||
private readonly ILogger<EventController> _logger; | ||
private readonly CallAutomationEventProcessor _eventProcessor; | ||
|
||
public EventController( | ||
ILogger<EventController> logger, | ||
CallAutomationClient callAutomationClient) | ||
{ | ||
_logger = logger; | ||
_eventProcessor = callAutomationClient.GetEventProcessor(); | ||
} | ||
|
||
[HttpPost] | ||
public IActionResult CallbackEvent([FromBody] CloudEvent[] cloudEvents) | ||
{ | ||
// Prase incoming event into solid base class of CallAutomationEvent. | ||
// This is useful when we want to access the properties of the event easily, such as CallConnectionId. | ||
// We are using this parsed event to log CallconnectionId of the event here. | ||
CallAutomationEventBase? parsedBaseEvent = CallAutomationEventParser.ParseMany(cloudEvents).FirstOrDefault(); | ||
_logger.LogInformation($"Event Recieved. CallConnectionId[{parsedBaseEvent?.CallConnectionId}], Type Name[{parsedBaseEvent?.GetType().Name}]"); | ||
|
||
// Utilizing evnetProcessor here to easily handle mid-call call automation events. | ||
// process event into processor, so events could be handled in CallingModule. | ||
_eventProcessor.ProcessEvents(cloudEvents); | ||
return Ok(); | ||
} | ||
} | ||
} |
106 changes: 106 additions & 0 deletions
106
...ppointmentBooking/CallAutomation_AppointmentBooking/Controllers/IncomingCallController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
using Azure.Communication; | ||
using Azure.Communication.CallAutomation; | ||
using Azure.Messaging.EventGrid; | ||
using Azure.Messaging.EventGrid.SystemEvents; | ||
using CallAutomation_AppointmentBooking.Interfaces; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace CallAutomation_AppointmentBooking.Controllers | ||
{ | ||
/// <summary> | ||
/// This is the controller for recieving an inbound call. | ||
/// See README files how to setup incoming call and its incoming call event | ||
/// </summary> | ||
[Route("api/[controller]")] | ||
[ApiController] | ||
public class IncomingCallController : ControllerBase | ||
{ | ||
private readonly ILogger<IncomingCallController> _logger; | ||
private readonly CallAutomationClient _callAutomationClient; | ||
private readonly AppointmentBookingConfig _appointmentBookingConfig; | ||
private readonly ITopLevelMenuService _topLevelMenuService; | ||
private readonly IOngoingEventHandler _ongoingEventHandler; | ||
|
||
public IncomingCallController( | ||
ILogger<IncomingCallController> logger, | ||
CallAutomationClient callAutomationClient, | ||
AppointmentBookingConfig appointmentBookingConfig, | ||
ITopLevelMenuService topLevelMenuService, | ||
IOngoingEventHandler ongoingEventHandler) | ||
{ | ||
_logger = logger; | ||
_callAutomationClient = callAutomationClient; | ||
_appointmentBookingConfig = appointmentBookingConfig; | ||
_topLevelMenuService = topLevelMenuService; | ||
_ongoingEventHandler = ongoingEventHandler; | ||
} | ||
|
||
[HttpPost] | ||
public async Task<IActionResult> IncomingCall([FromBody] object request) | ||
{ | ||
string callConnectionId = string.Empty; | ||
try | ||
{ | ||
// Parse incoming call event using eventgrid parser | ||
var httpContent = new BinaryData(request.ToString()); | ||
EventGridEvent cloudEvent = EventGridEvent.ParseMany(httpContent).First(); | ||
|
||
if (cloudEvent.EventType == SystemEventNames.EventGridSubscriptionValidation) | ||
{ | ||
// this section is for handling initial handshaking with Event webhook registration | ||
var eventData = cloudEvent.Data.ToObjectFromJson<SubscriptionValidationEventData>(); | ||
var responseData = new SubscriptionValidationResponse | ||
{ | ||
ValidationResponse = eventData.ValidationCode | ||
}; | ||
|
||
if (responseData.ValidationResponse != null) | ||
{ | ||
_logger.LogInformation($"Incoming EventGrid event: Handshake Successful."); | ||
return Ok(responseData); | ||
} | ||
} | ||
else if (cloudEvent.EventType == SystemEventNames.AcsIncomingCall) | ||
{ | ||
// parse again the data into ACS incomingCall event | ||
AcsIncomingCallEventData incomingCallEventData = cloudEvent.Data.ToObjectFromJson<AcsIncomingCallEventData>(); | ||
|
||
// Answer Incoming call with incoming call event data | ||
// IncomingCallContext can be used to answer the call | ||
AnswerCallResult answerCallResult = await _callAutomationClient.AnswerCallAsync(incomingCallEventData.IncomingCallContext, _appointmentBookingConfig.CallbackUri); | ||
callConnectionId = answerCallResult.CallConnectionProperties.CallConnectionId; | ||
|
||
_ = Task.Run(async () => | ||
{ | ||
// attaching ongoing event handler for specific events | ||
// This is useful for handling unexpected events could happen anytime (such as participants leaves the call and cal is disconnected) | ||
_ongoingEventHandler.AttachCountParticipantsInTheCall(callConnectionId); | ||
_ongoingEventHandler.AttachDisconnectedWrapup(callConnectionId); | ||
|
||
// Wait for call to be connected. | ||
// Wait for 40 seconds before throwing timeout error. | ||
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(40)); | ||
AnswerCallEventResult eventResult = await answerCallResult.WaitForEventProcessorAsync(tokenSource.Token); | ||
|
||
if (eventResult.IsSuccess) | ||
{ | ||
// call connected returned! Call is now established. | ||
// invoke top level menu now the call is connected; | ||
await _topLevelMenuService.InvokeTopLevelMenu( | ||
CommunicationIdentifier.FromRawId(incomingCallEventData.FromCommunicationIdentifier.RawId), | ||
answerCallResult.CallConnection, | ||
eventResult.SuccessResult.ServerCallId); | ||
} | ||
}); | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
// Exception! Failed to answer the call. | ||
_logger.LogError($"Exception while answer the call. CallConnectionId[{callConnectionId}], Exception[{e}]"); | ||
} | ||
|
||
return Ok(); | ||
} | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...mation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ICallingModules.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using Azure.Communication.CallAutomation; | ||
using Azure.Communication; | ||
|
||
namespace CallAutomation_AppointmentBooking.Interfaces | ||
{ | ||
public interface ICallingModules | ||
{ | ||
Task<string> RecognizeTonesAsync(CommunicationIdentifier targetToRecognize, int minDigitToCollect, int maxDigitToCollect, Uri askPrompt, Uri retryPrompt); | ||
|
||
Task PlayMessageThenWaitUntilItEndsAsync(Uri playPrompt); | ||
|
||
Task TerminateCallAsync(); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
...n_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/IOngoingEventHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using Azure.Communication; | ||
using Azure.Communication.CallAutomation; | ||
|
||
namespace CallAutomation_AppointmentBooking.Interfaces | ||
{ | ||
public interface IOngoingEventHandler | ||
{ | ||
void AttachCountParticipantsInTheCall(string callConnectionId); | ||
|
||
void AttachDisconnectedWrapup(string callConnectionId); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
...n_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ITopLevelMenuService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using Azure.Communication; | ||
using Azure.Communication.CallAutomation; | ||
|
||
namespace CallAutomation_AppointmentBooking.Interfaces | ||
{ | ||
public interface ITopLevelMenuService | ||
{ | ||
Task InvokeTopLevelMenu(CommunicationIdentifier originalTarget, CallConnection callConnection, string serverCallId); | ||
} | ||
} |
Oops, something went wrong.