Skip to content

Commit

Permalink
Added Readme
Browse files Browse the repository at this point in the history
  • Loading branch information
fhaghbin-msft committed May 24, 2023
1 parent 12eddba commit 8f02304
Show file tree
Hide file tree
Showing 30 changed files with 740 additions and 0 deletions.
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
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; }
}
}
}
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>
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);
}
}
}
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();
}
}
}
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();
}
}
}
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();
}
}
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);
}
}
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);
}
}
Loading

0 comments on commit 8f02304

Please sign in to comment.