Skip to content

Commit 8f02304

Browse files
committed
Added Readme
1 parent 12eddba commit 8f02304

30 files changed

Lines changed: 740 additions & 0 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.5.33516.290
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CallAutomation_AppointmentBooking", "CallAutomation_AppointmentBooking\CallAutomation_AppointmentBooking.csproj", "{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {31E6D6F2-4041-4119-AFF5-F575C0D8C7F6}
24+
EndGlobalSection
25+
EndGlobal
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace CallAutomation_AppointmentBooking
2+
{
3+
public class AppointmentBookingConfig
4+
{
5+
/// <summary>
6+
/// public Callback URI that will be used to answer incoming call event,
7+
/// or handle mid-call events, such as CallConnected.
8+
/// See README file for details on how to setup tunnel on your localhost to handle this.
9+
/// </summary>
10+
public Uri CallbackUri { get; init; }
11+
12+
/// <summary>
13+
/// DirectOffered phonenumber is can be aquired from Azure Communication Service portal.
14+
/// In order to answer Incoming PSTN call or make an outbound call to PSTN number,
15+
/// Call Automation needs Directly offered PSTN number to do these actions.
16+
/// </summary>
17+
public string DirectOfferedPhonenumber { get; init; }
18+
19+
/// <summary>
20+
/// List of all prompts from this sample's business logic.
21+
/// These recorded prompts must be uploaded to publicily available Uri endpoint.
22+
/// See README for pre-generated samples that can be used for demo.
23+
/// </summary>
24+
public Prompts AllPrompts { get; init; }
25+
26+
public class Prompts
27+
{
28+
public Uri MainMenu { get; init; }
29+
30+
public Uri Retry { get; init; }
31+
32+
public Uri Choice1 { get; init; }
33+
34+
public Uri Choice2 { get; init; }
35+
36+
public Uri Choice3 { get; init; }
37+
38+
public Uri PlayRecordingStarted { get; init; }
39+
40+
public Uri Goodbye { get; init; }
41+
}
42+
}
43+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net7.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<UserSecretsId>d7d2fc43-754d-4dba-87ed-832e765c7a4d</UserSecretsId>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Azure.Communication.CallAutomation" Version="1.0.0-alpha.20230516.1" />
12+
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.16.0" />
13+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
14+
</ItemGroup>
15+
16+
</Project>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using Azure.Communication;
2+
using Azure.Communication.CallAutomation;
3+
using CallAutomation_AppointmentBooking.Interfaces;
4+
5+
namespace CallAutomation_AppointmentBooking
6+
{
7+
/// <summary>
8+
/// Reusuable common calling actions for business needs
9+
/// </summary>
10+
public class CallingModules : ICallingModules
11+
{
12+
private readonly CallConnection _callConnection;
13+
private readonly AppointmentBookingConfig _appointmentBookingConfig;
14+
15+
public CallingModules(
16+
CallConnection callConnection,
17+
AppointmentBookingConfig appointmentBookingConfig)
18+
{
19+
_callConnection = callConnection;
20+
_appointmentBookingConfig = appointmentBookingConfig;
21+
}
22+
23+
public async Task<string> RecognizeTonesAsync(
24+
CommunicationIdentifier targetToRecognize,
25+
int minDigitToCollect,
26+
int maxDigitToCollect,
27+
Uri askPrompt,
28+
Uri retryPrompt)
29+
{
30+
for (int i = 0; i < 3; i++)
31+
{
32+
// prepare recognize tones
33+
CallMediaRecognizeDtmfOptions callMediaRecognizeDtmfOptions = new CallMediaRecognizeDtmfOptions(targetToRecognize, maxDigitToCollect);
34+
callMediaRecognizeDtmfOptions.Prompt = new FileSource(askPrompt);
35+
callMediaRecognizeDtmfOptions.InterruptPrompt = true;
36+
callMediaRecognizeDtmfOptions.InitialSilenceTimeout = TimeSpan.FromSeconds(10);
37+
callMediaRecognizeDtmfOptions.InterToneTimeout = TimeSpan.FromSeconds(10);
38+
callMediaRecognizeDtmfOptions.StopTones = new List<DtmfTone> { DtmfTone.Pound, DtmfTone.Asterisk };
39+
40+
// Send request to recognize tones
41+
StartRecognizingCallMediaResult startRecognizingResult = await _callConnection.GetCallMedia().StartRecognizingAsync(callMediaRecognizeDtmfOptions);
42+
43+
// Wait for recognize related event...
44+
StartRecognizingEventResult recognizeEventResult = await startRecognizingResult.WaitForEventProcessorAsync();
45+
46+
if (recognizeEventResult.IsSuccess)
47+
{
48+
// success recognition - return the tones detected.
49+
RecognizeCompleted recognizeCompleted = recognizeEventResult.SuccessResult;
50+
string dtmfTones = ((DtmfResult)recognizeCompleted.RecognizeResult).ConvertToString();
51+
52+
// check if it collected the minimum digit it collected
53+
if (dtmfTones.Length >= minDigitToCollect)
54+
{
55+
return dtmfTones;
56+
}
57+
}
58+
else
59+
{
60+
// failed recognition - likely timeout
61+
_ = recognizeEventResult.FailureResult;
62+
}
63+
64+
// play retry prompt and retry again
65+
await PlayMessageThenWaitUntilItEndsAsync(retryPrompt);
66+
}
67+
68+
throw new Exception("Retried 3 times, Failed to get tones.");
69+
}
70+
71+
72+
public async Task PlayMessageThenWaitUntilItEndsAsync(Uri playPrompt)
73+
{
74+
// Play failure prompt and retry.
75+
FileSource fileSource = new FileSource(playPrompt);
76+
PlayResult playResult = await _callConnection.GetCallMedia().PlayToAllAsync(fileSource);
77+
78+
// ... wait for play to complete, then return
79+
await playResult.WaitForEventProcessorAsync();
80+
}
81+
82+
public async Task TerminateCallAsync()
83+
{
84+
// Terminate the call
85+
await _callConnection.HangUpAsync(true);
86+
}
87+
}
88+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using Azure.Communication.CallAutomation;
2+
using Azure.Messaging;
3+
using Microsoft.AspNetCore.Mvc;
4+
5+
namespace CallAutomation_AppointmentBooking.Controllers
6+
{
7+
/// <summary>
8+
/// This is controller where it will recieve interim events from Call automation service.
9+
/// We are utilizing event processor, this will handle events and relay to our business logic.
10+
/// </summary>
11+
[Route("api/[controller]")]
12+
[ApiController]
13+
public class EventController : ControllerBase
14+
{
15+
private readonly ILogger<EventController> _logger;
16+
private readonly CallAutomationEventProcessor _eventProcessor;
17+
18+
public EventController(
19+
ILogger<EventController> logger,
20+
CallAutomationClient callAutomationClient)
21+
{
22+
_logger = logger;
23+
_eventProcessor = callAutomationClient.GetEventProcessor();
24+
}
25+
26+
[HttpPost]
27+
public IActionResult CallbackEvent([FromBody] CloudEvent[] cloudEvents)
28+
{
29+
// Prase incoming event into solid base class of CallAutomationEvent.
30+
// This is useful when we want to access the properties of the event easily, such as CallConnectionId.
31+
// We are using this parsed event to log CallconnectionId of the event here.
32+
CallAutomationEventBase? parsedBaseEvent = CallAutomationEventParser.ParseMany(cloudEvents).FirstOrDefault();
33+
_logger.LogInformation($"Event Recieved. CallConnectionId[{parsedBaseEvent?.CallConnectionId}], Type Name[{parsedBaseEvent?.GetType().Name}]");
34+
35+
// Utilizing evnetProcessor here to easily handle mid-call call automation events.
36+
// process event into processor, so events could be handled in CallingModule.
37+
_eventProcessor.ProcessEvents(cloudEvents);
38+
return Ok();
39+
}
40+
}
41+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using Azure.Communication;
2+
using Azure.Communication.CallAutomation;
3+
using Azure.Messaging.EventGrid;
4+
using Azure.Messaging.EventGrid.SystemEvents;
5+
using CallAutomation_AppointmentBooking.Interfaces;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
namespace CallAutomation_AppointmentBooking.Controllers
9+
{
10+
/// <summary>
11+
/// This is the controller for recieving an inbound call.
12+
/// See README files how to setup incoming call and its incoming call event
13+
/// </summary>
14+
[Route("api/[controller]")]
15+
[ApiController]
16+
public class IncomingCallController : ControllerBase
17+
{
18+
private readonly ILogger<IncomingCallController> _logger;
19+
private readonly CallAutomationClient _callAutomationClient;
20+
private readonly AppointmentBookingConfig _appointmentBookingConfig;
21+
private readonly ITopLevelMenuService _topLevelMenuService;
22+
private readonly IOngoingEventHandler _ongoingEventHandler;
23+
24+
public IncomingCallController(
25+
ILogger<IncomingCallController> logger,
26+
CallAutomationClient callAutomationClient,
27+
AppointmentBookingConfig appointmentBookingConfig,
28+
ITopLevelMenuService topLevelMenuService,
29+
IOngoingEventHandler ongoingEventHandler)
30+
{
31+
_logger = logger;
32+
_callAutomationClient = callAutomationClient;
33+
_appointmentBookingConfig = appointmentBookingConfig;
34+
_topLevelMenuService = topLevelMenuService;
35+
_ongoingEventHandler = ongoingEventHandler;
36+
}
37+
38+
[HttpPost]
39+
public async Task<IActionResult> IncomingCall([FromBody] object request)
40+
{
41+
string callConnectionId = string.Empty;
42+
try
43+
{
44+
// Parse incoming call event using eventgrid parser
45+
var httpContent = new BinaryData(request.ToString());
46+
EventGridEvent cloudEvent = EventGridEvent.ParseMany(httpContent).First();
47+
48+
if (cloudEvent.EventType == SystemEventNames.EventGridSubscriptionValidation)
49+
{
50+
// this section is for handling initial handshaking with Event webhook registration
51+
var eventData = cloudEvent.Data.ToObjectFromJson<SubscriptionValidationEventData>();
52+
var responseData = new SubscriptionValidationResponse
53+
{
54+
ValidationResponse = eventData.ValidationCode
55+
};
56+
57+
if (responseData.ValidationResponse != null)
58+
{
59+
_logger.LogInformation($"Incoming EventGrid event: Handshake Successful.");
60+
return Ok(responseData);
61+
}
62+
}
63+
else if (cloudEvent.EventType == SystemEventNames.AcsIncomingCall)
64+
{
65+
// parse again the data into ACS incomingCall event
66+
AcsIncomingCallEventData incomingCallEventData = cloudEvent.Data.ToObjectFromJson<AcsIncomingCallEventData>();
67+
68+
// Answer Incoming call with incoming call event data
69+
// IncomingCallContext can be used to answer the call
70+
AnswerCallResult answerCallResult = await _callAutomationClient.AnswerCallAsync(incomingCallEventData.IncomingCallContext, _appointmentBookingConfig.CallbackUri);
71+
callConnectionId = answerCallResult.CallConnectionProperties.CallConnectionId;
72+
73+
_ = Task.Run(async () =>
74+
{
75+
// attaching ongoing event handler for specific events
76+
// This is useful for handling unexpected events could happen anytime (such as participants leaves the call and cal is disconnected)
77+
_ongoingEventHandler.AttachCountParticipantsInTheCall(callConnectionId);
78+
_ongoingEventHandler.AttachDisconnectedWrapup(callConnectionId);
79+
80+
// Wait for call to be connected.
81+
// Wait for 40 seconds before throwing timeout error.
82+
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(40));
83+
AnswerCallEventResult eventResult = await answerCallResult.WaitForEventProcessorAsync(tokenSource.Token);
84+
85+
if (eventResult.IsSuccess)
86+
{
87+
// call connected returned! Call is now established.
88+
// invoke top level menu now the call is connected;
89+
await _topLevelMenuService.InvokeTopLevelMenu(
90+
CommunicationIdentifier.FromRawId(incomingCallEventData.FromCommunicationIdentifier.RawId),
91+
answerCallResult.CallConnection,
92+
eventResult.SuccessResult.ServerCallId);
93+
}
94+
});
95+
}
96+
}
97+
catch (Exception e)
98+
{
99+
// Exception! Failed to answer the call.
100+
_logger.LogError($"Exception while answer the call. CallConnectionId[{callConnectionId}], Exception[{e}]");
101+
}
102+
103+
return Ok();
104+
}
105+
}
106+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Azure.Communication.CallAutomation;
2+
using Azure.Communication;
3+
4+
namespace CallAutomation_AppointmentBooking.Interfaces
5+
{
6+
public interface ICallingModules
7+
{
8+
Task<string> RecognizeTonesAsync(CommunicationIdentifier targetToRecognize, int minDigitToCollect, int maxDigitToCollect, Uri askPrompt, Uri retryPrompt);
9+
10+
Task PlayMessageThenWaitUntilItEndsAsync(Uri playPrompt);
11+
12+
Task TerminateCallAsync();
13+
}
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Azure.Communication;
2+
using Azure.Communication.CallAutomation;
3+
4+
namespace CallAutomation_AppointmentBooking.Interfaces
5+
{
6+
public interface IOngoingEventHandler
7+
{
8+
void AttachCountParticipantsInTheCall(string callConnectionId);
9+
10+
void AttachDisconnectedWrapup(string callConnectionId);
11+
}
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Azure.Communication;
2+
using Azure.Communication.CallAutomation;
3+
4+
namespace CallAutomation_AppointmentBooking.Interfaces
5+
{
6+
public interface ITopLevelMenuService
7+
{
8+
Task InvokeTopLevelMenu(CommunicationIdentifier originalTarget, CallConnection callConnection, string serverCallId);
9+
}
10+
}

0 commit comments

Comments
 (0)