Skip to content
Open
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
25 changes: 25 additions & 0 deletions CallAutomation_Playground/CallAutomation_Playground.sln
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_Playground", "CallAutomation_Playground\CallAutomation_Playground.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,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.20230608.1" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.16.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

</Project>
206 changes: 206 additions & 0 deletions CallAutomation_Playground/CallAutomation_Playground/CallingModules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using Azure.Communication;
using Azure.Communication.CallAutomation;
using CallAutomation_Playground.Interfaces;

namespace CallAutomation_Playground
{
/// <summary>
/// Reusuable common calling actions for business needs
/// </summary>
public class CallingModules : ICallingModules
{
private readonly CallConnection _callConnection;
private readonly PlaygroundConfigs _playgroundConfig;
private readonly ILogger<CallingModules> _logger;
CallInvite callInvite;

public CallingModules(
CallConnection callConnection,
PlaygroundConfigs playgroundConfig)
{
_callConnection = callConnection;
_playgroundConfig = playgroundConfig;
}

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 AddParticipantAsync(
CommunicationIdentifier targetToAdd,
Uri successPrompt,
Uri failurePrompt,
Uri musicPrompt)
{
// Send add participant request


PhoneNumberIdentifier source = new PhoneNumberIdentifier(_playgroundConfig.DirectOfferedPhonenumber);

if (targetToAdd.GetType() == typeof(PhoneNumberIdentifier))
{
callInvite = new CallInvite((PhoneNumberIdentifier)targetToAdd, source);
}
else if (targetToAdd.GetType() == typeof(CommunicationUserIdentifier))
{
callInvite = new CallInvite((CommunicationUserIdentifier)targetToAdd);
}
AddParticipantResult addParticipantResult = await _callConnection.AddParticipantAsync(callInvite);

// Play hold music while the participant is joining
await PlayHoldMusicInLoopAsync(musicPrompt);

// give maximum 60 seconds timeout for other party to answer the call,
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
AddParticipantEventResult addParticipantEventResult = await addParticipantResult.WaitForEventProcessorAsync(tokenSource.Token);

// As soon as event comesback (or the timeout happens)
// stop paying the music above
await _callConnection.GetCallMedia().CancelAllMediaOperationsAsync();

if (addParticipantEventResult.IsSuccess)
{
// Success joining - play message and return
_ = addParticipantEventResult.SuccessResult;
await PlayMessageThenWaitUntilItEndsAsync(successPrompt);
_logger.LogInformation("Participant List after addparticipant -------->");
List<CallParticipant> participantsList = (await _callConnection.GetParticipantsAsync()).Value.ToList();
foreach (var participant in participantsList)
{
_logger.LogInformation($"{participant.Identifier.RawId}");
}
}
else
{
// failed joining - play message and return
_ = addParticipantEventResult.FailureResult;
await PlayMessageThenWaitUntilItEndsAsync(failurePrompt);
}

}

public async Task RemoveParticipantAsync(CommunicationIdentifier targetToRemove)
{
await _callConnection.RemoveParticipantAsync(targetToRemove);
return;
}
public async Task RemoveAllParticipantExceptCallerAsync(CommunicationIdentifier originalCaller)
{
// List all participants in the call
IReadOnlyList<CallParticipant> participantsList = (await _callConnection.GetParticipantsAsync()).Value;

// go through the list and remove each one
foreach (var participant in participantsList)
{
// Remove all participants that is not the original caller
if (participant.Identifier.RawId != originalCaller.RawId)
{
await _callConnection.RemoveParticipantAsync(participant.Identifier);
}
}

return;
}

public async Task<bool> TransferCallAsync(
CommunicationIdentifier transferTo,
Uri failurePrompt)
{
TransferCallToParticipantResult transferCallToParticipantResult = null;
if (transferTo.GetType() == typeof(PhoneNumberIdentifier))
{
transferCallToParticipantResult = await _callConnection.TransferCallToParticipantAsync((PhoneNumberIdentifier)transferTo);
}
else if (transferTo.GetType() == typeof(CommunicationUserIdentifier))
{
transferCallToParticipantResult = await _callConnection.TransferCallToParticipantAsync((CommunicationUserIdentifier)transferTo);
}
// send transfer request
transferCallToParticipantResult = await _callConnection.TransferCallToParticipantAsync(transferTo);
TransferCallToParticipantEventResult transferCallToParticipantEventResult = await transferCallToParticipantResult.WaitForEventProcessorAsync();

if (transferCallToParticipantEventResult.IsSuccess)
{
// successful transfer
return true;
}
else
{
// failed transfer - play message and return
_ = transferCallToParticipantEventResult.FailureResult;
await PlayMessageThenWaitUntilItEndsAsync(failurePrompt);

return false;
}
}

public async Task PlayHoldMusicInLoopAsync(Uri musicPrompt)
{
// Play Hold Music in Loop, until cancelled with CancelAllMediaOperation
FileSource fileSource = new FileSource(musicPrompt);
PlayToAllOptions playOptions = new PlayToAllOptions(fileSource);
playOptions.Loop = true;
await _callConnection.GetCallMedia().PlayToAllAsync(playOptions);
}

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,72 @@
using Azure.Communication;
using CallAutomation_Playground.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace CallAutomation_Playground.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 AddParticipantController : ControllerBase
{
private readonly ILogger<AddParticipantController> _logger;
private readonly PlaygroundConfigs _playgroundConfig;
CommunicationIdentifier _target;

public AddParticipantController(
ILogger<AddParticipantController> logger,
PlaygroundConfigs playgroundConfig)
{
_logger = logger;
_playgroundConfig = playgroundConfig;
}

[HttpPost]
public async Task<IActionResult> CreateCall([FromQuery] string target)
{
string callConnectionId = string.Empty;
try
{
if (!string.IsNullOrEmpty(target))
{
var addparticipants = target.Split(',');
foreach (var addparticipant in addparticipants)
{
if (!string.IsNullOrEmpty(addparticipant))
{
var identifierKind = Tools.GetIdentifierKind(addparticipant);

if (identifierKind == Tools.CommunicationIdentifierKind.PhoneIdentity)
{
PhoneNumberIdentifier pstntarget = new PhoneNumberIdentifier(Tools.FormatPhoneNumbers(addparticipant));
_target = pstntarget;
}
else if (identifierKind == Tools.CommunicationIdentifierKind.UserIdentity)
{
CommunicationUserIdentifier communicationIdentifier = new CommunicationUserIdentifier(addparticipant);
_target = communicationIdentifier;
}
_logger.LogInformation($"Adding Participant [{_target}]");

ICallingModules callingModule = new CallingModules(callConnectionConfig.callConnection, _playgroundConfig);
await callingModule.AddParticipantAsync(_target,
_playgroundConfig.AllPrompts.AddParticipantSuccess,
_playgroundConfig.AllPrompts.AddParticipantFailure,
_playgroundConfig.AllPrompts.Music);
}
}
}
}
catch (Exception e)
{
// Exception! likely the call was never established due to other party not answering.
_logger.LogError($"Exception while doing outbound call. CallConnectionId[{callConnectionId}], Exception[{e}]");
}

return Ok(callConnectionId);
}
}
}
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_Playground.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();
}
}
}
Loading