From 9ed1a896faeb03aee93af61ffcba73424188879f Mon Sep 17 00:00:00 2001 From: ravithanneeru <80485423+ravithanneeru@users.noreply.github.com> Date: Thu, 11 Nov 2021 23:58:59 +0000 Subject: [PATCH] Function app developed through azure portal (#27) * Azure function app for outbound, using azure portal. * Incorporated review comments * Improvements in content and code Co-authored-by: maulinasharma --- .../EventHandler/EventDispatcher.cs | 41 ++--- .../OutboundFunction/Phonecall.cs | 12 +- .../OutboundFunction/SendNotification.cs | 8 +- .../OutboundFunction/readme.md | 80 +++++++++ .../EventHandler/EventAuthHandler.csx | 22 +++ .../EventHandler/EventDispatcher.csx | 95 ++++++++++ .../EventHandler/EventExtensions.csx | 12 ++ .../SendNotification/Logger.csx | 33 ++++ .../SendNotification/Phonecall.csx | 168 ++++++++++++++++++ .../SendNotification/Sendsms.csx | 34 ++++ .../SendNotification/function.proj | 16 ++ .../SendNotification/run.csx | 98 ++++++++++ .../OutboundFunction_AzurePortal/readme.md | 72 ++++++++ 13 files changed, 656 insertions(+), 35 deletions(-) create mode 100644 OutboundCallReminder/OutboundFunction/readme.md create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventAuthHandler.csx create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventDispatcher.csx create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventExtensions.csx create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Logger.csx create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Phonecall.csx create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Sendsms.csx create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/function.proj create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/run.csx create mode 100644 OutboundCallReminder/OutboundFunction_AzurePortal/readme.md diff --git a/OutboundCallReminder/OutboundFunction/OutboundFunction/EventHandler/EventDispatcher.cs b/OutboundCallReminder/OutboundFunction/OutboundFunction/EventHandler/EventDispatcher.cs index df89080a..8f87accd 100644 --- a/OutboundCallReminder/OutboundFunction/OutboundFunction/EventHandler/EventDispatcher.cs +++ b/OutboundCallReminder/OutboundFunction/OutboundFunction/EventHandler/EventDispatcher.cs @@ -10,34 +10,32 @@ namespace OutboundFunction using System.Threading.Tasks; public class EventDispatcher { - public static readonly EventDispatcher Instance; - private static readonly ConcurrentDictionary NotificationCallback; - private readonly object SubscriptionLock = new object(); + private static readonly EventDispatcher instance; + private static readonly ConcurrentDictionary notificationCallbacks; + public static EventDispatcher Instance + { + get + { + return instance; + } + } static EventDispatcher() { - Instance = new EventDispatcher(); - NotificationCallback = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + instance = new EventDispatcher(); + notificationCallbacks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); } public bool Subscribe(string eventType, string eventKey, NotificationCallback notificationCallback) { string eventId = BuildEventKey(eventType, eventKey); - - lock (this.SubscriptionLock) - { - return NotificationCallback.TryAdd(eventId, notificationCallback); - } + return notificationCallbacks.TryAdd(eventId, notificationCallback); } public void Unsubscribe(string eventType, string eventKey) { string eventId = BuildEventKey(eventType, eventKey); - - lock (this.SubscriptionLock) - { - NotificationCallback.TryRemove(eventId, out _); - } + notificationCallbacks.TryRemove(eventId, out _); } public void ProcessNotification(string request) @@ -46,17 +44,14 @@ public void ProcessNotification(string request) if (callEvent != null) { - lock (SubscriptionLock) + if (notificationCallbacks.TryGetValue(GetEventKey(callEvent), out NotificationCallback notificationCallback)) { - if (NotificationCallback.TryGetValue(GetEventKey(callEvent), out NotificationCallback notificationCallback)) + if (notificationCallback != null) { - if (notificationCallback != null) + Task.Run(() => { - Task.Run(() => - { - notificationCallback.Callback.Invoke(callEvent); - }); - } + notificationCallback.Callback.Invoke(callEvent); + }); } } } diff --git a/OutboundCallReminder/OutboundFunction/OutboundFunction/Phonecall.cs b/OutboundCallReminder/OutboundFunction/OutboundFunction/Phonecall.cs index b74d57b8..39ffc3d3 100644 --- a/OutboundCallReminder/OutboundFunction/OutboundFunction/Phonecall.cs +++ b/OutboundCallReminder/OutboundFunction/OutboundFunction/Phonecall.cs @@ -16,20 +16,18 @@ class Phonecall private CallingServerClient callClient; private CallConnection callConnection; private string appCallbackUrl; - + private string connectionString; private CancellationTokenSource reportCancellationTokenSource; private CancellationToken reportCancellationToken; - private TaskCompletionSource callEstablishedTask; private TaskCompletionSource callTerminatedTask; public Phonecall(string callbackUrl) { - var connectionString = Environment.GetEnvironmentVariable("Connectionstring"); + connectionString = Environment.GetEnvironmentVariable("Connectionstring"); callClient = new CallingServerClient(connectionString); appCallbackUrl = callbackUrl; } - public async void InitiatePhoneCall(string sourcePhoneNumber, string targetPhoneNumber, string audioFileUrl) { reportCancellationTokenSource = new CancellationTokenSource(); @@ -49,11 +47,10 @@ private async Task CreateCallAsync(string sourcePhoneNumber, string targetPhoneN { try { - var connectionString = Environment.GetEnvironmentVariable("Connectionstring"); string appCallbackUrl = $"{this.appCallbackUrl}outboundcall/callback?{EventAuthHandler.GetSecretQuerystring}"; //Preparing request data - var source = await CreateUser(connectionString); + var source = await CreateUser(); var target = new PhoneNumberIdentifier(targetPhoneNumber); CreateCallOptions createCallOption = new CreateCallOptions( new Uri(appCallbackUrl), @@ -114,7 +111,7 @@ private void RegisterToCallStateChangeEvent(string callConnectionId) var eventId = EventDispatcher.Instance.Subscribe(CallingServerEventType.CallConnectionStateChangedEvent.ToString(), callConnectionId, callStateChangeNotificaiton); } - private static async Task CreateUser(string connectionString) + private async Task CreateUser() { var client = new CommunicationIdentityClient(connectionString); var user = await client.CreateUserAsync().ConfigureAwait(false); @@ -144,7 +141,6 @@ private async Task PlayAudioAsync(string audioFileUrl) AudioFileUri = new Uri(audioFileUrl), OperationContext = Guid.NewGuid().ToString(), Loop = true, - CallbackUri = new Uri($"{this.appCallbackUrl}outboundcall/callback?{EventAuthHandler.GetSecretQuerystring}"), AudioFileId = "", }; diff --git a/OutboundCallReminder/OutboundFunction/OutboundFunction/SendNotification.cs b/OutboundCallReminder/OutboundFunction/OutboundFunction/SendNotification.cs index 39cc1adc..583dc60e 100644 --- a/OutboundCallReminder/OutboundFunction/OutboundFunction/SendNotification.cs +++ b/OutboundCallReminder/OutboundFunction/OutboundFunction/SendNotification.cs @@ -42,18 +42,18 @@ public static IActionResult Run( string sourceNumber = data?.SourceNumber; string targetNumber = data?.OutboundNumber; - if(!string.IsNullOrEmpty(targetNumber) && !string.IsNullOrEmpty(sourceNumber)) + if(!string.IsNullOrWhiteSpace(targetNumber) && !string.IsNullOrWhiteSpace(sourceNumber)) { string isSmsSend = data?.SMS?.Send; log.LogInformation($"sourceNumber --> {sourceNumber}"); log.LogInformation($"targerNumber --> {targetNumber}"); - if (!string.IsNullOrEmpty(isSmsSend) && isSmsSend == "true") + if (!string.IsNullOrWhiteSpace(isSmsSend) && isSmsSend.ToLower() == "true") { string message = data?.SMS?.Message; - if (!string.IsNullOrEmpty(message)) + if (!string.IsNullOrWhiteSpace(message)) { var sendSMS = new SendSMS(); Task.Run(() => { sendSMS.SendOneToOneSms(sourceNumber, targetNumber, message); }); @@ -65,7 +65,7 @@ public static IActionResult Run( } string isInitiatePhoneCall = data?.PhoneCall?.Send; - if (!string.IsNullOrEmpty(isInitiatePhoneCall) && isInitiatePhoneCall == "true") + if (!string.IsNullOrEmpty(isInitiatePhoneCall) && isInitiatePhoneCall.ToLower() == "true") { string callbackUrl = $"{req.Scheme}://{req.Host}/api/"; string audioUrl = data?.PhoneCall?.PlayAudioUrl; diff --git a/OutboundCallReminder/OutboundFunction/readme.md b/OutboundCallReminder/OutboundFunction/readme.md new file mode 100644 index 00000000..52a6f9d6 --- /dev/null +++ b/OutboundCallReminder/OutboundFunction/readme.md @@ -0,0 +1,80 @@ +--- +page_type: sample +languages: +- csharp +products: +- azure +- azure-communication-services +--- + +# Outbound Azure Function Sample + +This azure function sample send SMS, make an outbound call and play audio using ACS SMS and Server Calling SDKs on the basis of json input given in the following format. + +- `OutboundNumber`: Target phone number where we need to send notification. +- `SourceNumber`: Phone number associated with the Azure Communication Service resource. +- `SMS.Send`: value should be true/false as we want to send SMS or not. +- `SMS.Message`: Message want to send as SMS. +- `PhoneCall.Send`: value should be true/false as we want to make a phone call or not. +- `PhoneCall.PlayAudioUrl`: The wav file url which accessible by the function app. If the value is empty, then the sample will use configured url through azure portal. + +The azure function is built on .NET Framework 4.7.2. + +``` +{ + "SourceNumber":"+18xxxxxxxxxx", + "OutboundNumber": "+18xxxxxxxxxx", + "SMS": { + "Send": "true", + "Message": "message to be sent" + }, + "PhoneCall": { + "Send": "true", + "PlayAudioUrl": "audio file URL function app can able to access" + } +} +``` + +## Prerequisites + +- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/) +- [Visual Studio (2019 and above)](https://visualstudio.microsoft.com/vs/) +- [.NET Framework 4.7.2](https://dotnet.microsoft.com/download/dotnet-framework/net472) (Make sure to install version that corresponds with your visual studio instance, 32 vs 64 bit) +- Create an Azure Communication Services resource. For details, see [Create an Azure Communication Resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). You'll need to record your resource **connection string** for this sample. +- Get a phone number for your new Azure Communication Services resource. For details, see [Get a phone number](https://docs.microsoft.com/azure/communication-services/quickstarts/telephony-sms/get-phone-number?pivots=platform-azp) + +## Code structure + +- ./OutboundFunction/SendNotification.cs : Azure function to send notification using phone call and SMS. +- ./OutboundFunction/OutboundController.cs : Azure function to handle outbound callbacks +- ./OutboundFunction/Phonecall.cs : class for handling outbound phone call. +- ./OutboundFunction/SendSms.cs : class for sending SMS notification. +- ./OutboundFunction/EventHandler/*.cs : Code for managing callbacks and request authorization. + +## Before running the sample for the first time + +- Open an instance of PowerShell, Windows Terminal, Command Prompt or equivalent and navigate to the directory that you'd like to clone the sample to. +- git clone `https://github.com/Azure-Samples/communication-services-dotnet-quickstarts.git`. +- Once you get the config keys add the keys as an environment variable. + - Input your connection string in variable: `Connectionstring` + - Input you Secret/Password that would be part of callback and will be use to validate incoming requests in variable `SecretPlaceholder` + - Input URL of default wav file going to play in outbound phone call in variable `CallbackUri` + +## Locally deploying the sample app + +- Go to OutboundFunction folder and open `OutboundFunction.sln` solution in Visual Studio +- Run `OutboundFunction` project. +- Use postman or any debugging tool and use function URL - http://localhost:7071/api/SendNotification with the json request. + +## Publish to Azure + +[Publish the project to Azure](https://docs.microsoft.com/azure/azure-functions/functions-create-your-first-function-visual-studio#publish-the-project-to-azure) + +### Configuring Azure Function Sample + +After publishing your function App, add following configuration in your function App's `configuration` section. + +- Connectionstring: Azure Communication Service resource's connection string. +- SourcePhone: Phone number associated with the Azure Communication Service resource. +- SecretPlaceholder: Secret/Password that would be part of callback and will be use to validate incoming requests. +- AudioFileUrl: Url of default wav file going to play in outbound phone call which is accessible by function app. diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventAuthHandler.csx b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventAuthHandler.csx new file mode 100644 index 00000000..c1108198 --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventAuthHandler.csx @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Web; + +public class EventAuthHandler +{ + private static readonly string SecretKey = "secret"; + private static readonly string SecretValue; + + static EventAuthHandler() + { + SecretValue = Environment.GetEnvironmentVariable("SecretPlaceholder") ?? "h3llowW0rld"; + } + + public static bool Authorize(HttpRequest request) + { + string secretKeyValue = request.Query[SecretKey].ToString(); + return !string.IsNullOrEmpty(secretKeyValue) && secretKeyValue.Equals(SecretValue); + } + + public static string GetSecretQuerystring => $"{SecretKey}={HttpUtility.UrlEncode(SecretValue)}"; +} diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventDispatcher.csx b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventDispatcher.csx new file mode 100644 index 00000000..385d5aad --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventDispatcher.csx @@ -0,0 +1,95 @@ +#load "EventExtensions.csx" + +using Azure.Communication.CallingServer; +using Azure.Messaging; +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +public class EventDispatcher +{ + private static readonly EventDispatcher instance; + private static readonly ConcurrentDictionary notificationCallbacks; + + public static EventDispatcher Instance + { + get + { + return instance; + } + } + + static EventDispatcher() + { + instance = new EventDispatcher(); + notificationCallbacks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + + public bool Subscribe(string eventType, string eventKey, NotificationCallback notificationCallback) + { + string eventId = BuildEventKey(eventType, eventKey); + bool ret = notificationCallbacks.TryAdd(eventId, notificationCallback); + return ret; + } + + public void Unsubscribe(string eventType, string eventKey) + { + string eventId = BuildEventKey(eventType, eventKey); + notificationCallbacks.TryRemove(eventId, out _); + } + + public void ProcessNotification(string request) + { + var callEvent = this.ExtractEvent(request); + + if (callEvent != null) + { + if (notificationCallbacks.TryGetValue(GetEventKey(callEvent), out NotificationCallback notificationCallback)) + { + if (notificationCallback != null) + { + Task.Run(() => + { + notificationCallback.Callback.Invoke(callEvent); + }); + } + } + } + } + + public string GetEventKey(CallingServerEventBase callEventBase) + { + if (callEventBase is CallConnectionStateChangedEvent) + { + var callLegId = ((CallConnectionStateChangedEvent)callEventBase).CallConnectionId; + return BuildEventKey(CallingServerEventType.CallConnectionStateChangedEvent.ToString(), callLegId); ; + } + + return null; + } + + public string BuildEventKey(string eventType, string eventKey) + { + return $"{eventType}-{eventKey}"; + } + + /// + /// Extracting event from the json. + /// + /// + /// + public CallingServerEventBase ExtractEvent(string content) + { + CloudEvent cloudEvent = CloudEvent.Parse(BinaryData.FromString(content)); + + if (cloudEvent != null && cloudEvent.Data != null) + { + if (cloudEvent.Type.Equals(CallingServerEventType.CallConnectionStateChangedEvent.ToString())) + { + return CallConnectionStateChangedEvent.Deserialize(cloudEvent.Data.ToString()); + } + } + + return null; + } +} \ No newline at end of file diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventExtensions.csx b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventExtensions.csx new file mode 100644 index 00000000..e0c09714 --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/EventHandler/EventExtensions.csx @@ -0,0 +1,12 @@ +using Azure.Communication.CallingServer; +using System; + +public class NotificationCallback +{ + public Action Callback { get; set; } + + public NotificationCallback(Action callBack) + { + this.Callback = callBack; + } +} \ No newline at end of file diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Logger.csx b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Logger.csx new file mode 100644 index 00000000..8b6d26d8 --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Logger.csx @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; + +public static class Logger +{ + public static ILogger logger = null; + public enum MessageType + { + INFORMATION, + ERROR + } + + public static ILogger SetLoggerInstance(ILogger log) + { + if (logger == null) + { + logger = log; + } + return logger; + } + + /// + /// Log message to console + /// + /// Type of the message: Information or Error + /// Message string + public static void LogMessage(MessageType messageType, string message) + { + if(messageType == MessageType.ERROR) + logger.LogError(message); + else + logger.LogInformation(message); + } +} \ No newline at end of file diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Phonecall.csx b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Phonecall.csx new file mode 100644 index 00000000..b11d3601 --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Phonecall.csx @@ -0,0 +1,168 @@ +#load "Logger.csx" +#load "EventHandler/EventAuthHandler.csx" +#load "EventHandler/EventDispatcher.csx" +#load "EventHandler/EventExtensions.csx" + +using System; +using System.Collections.Generic; +using Azure.Communication; +using Azure.Communication.CallingServer; +using Azure.Communication.Identity; +using System.Threading; +using System.Threading.Tasks; + +class Phonecall +{ + private CallingServerClient callClient; + private CallConnection callConnection; + private string appCallbackUrl; + private string connectionString; + private CancellationTokenSource reportCancellationTokenSource; + private CancellationToken reportCancellationToken; + private TaskCompletionSource callEstablishedTask; + private TaskCompletionSource callTerminatedTask; + + public Phonecall(string callbackUrl) + { + connectionString = Environment.GetEnvironmentVariable("Connectionstring"); + callClient = new CallingServerClient(connectionString); + appCallbackUrl = callbackUrl; + } + + public async Task InitiatePhoneCall(string sourcePhoneNumber, string targetPhoneNumber, string audioFileUrl) + { + reportCancellationTokenSource = new CancellationTokenSource(); + reportCancellationToken = reportCancellationTokenSource.Token; + try + { + await CreateCallAsync(sourcePhoneNumber, targetPhoneNumber).ConfigureAwait(false); + await PlayAudioAsync(audioFileUrl).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogMessage(Logger.MessageType.ERROR, $"Call ended unexpectedly, reason: {ex.Message}"); + } + } + + private async Task CreateCallAsync(string sourcePhoneNumber, string targetPhoneNumber) + { + try + { + string appCallbackUrl = $"{this.appCallbackUrl}SendNotification?{EventAuthHandler.GetSecretQuerystring}"; + + //Preparing request data + var source = await CreateUser(); + var target = new PhoneNumberIdentifier(targetPhoneNumber); + CreateCallOptions createCallOption = new CreateCallOptions( + new Uri(appCallbackUrl), + new List { MediaType.Audio }, + new List { EventSubscriptionType.ParticipantsUpdated, EventSubscriptionType.DtmfReceived } + ); + createCallOption.AlternateCallerId = new PhoneNumberIdentifier(sourcePhoneNumber); + + Logger.LogMessage(Logger.MessageType.INFORMATION, "Performing CreateCall operation"); + + callConnection = await callClient.CreateCallConnectionAsync(source, + new List() { target }, + createCallOption, reportCancellationToken) + .ConfigureAwait(false); + + Logger.LogMessage(Logger.MessageType.INFORMATION, $"CreateCallConnectionAsync response --> {callConnection.ToString()}, Call Connection Id: { callConnection.CallConnectionId}"); + Logger.LogMessage(Logger.MessageType.INFORMATION, $"Call initiated with Call Connection id: { callConnection.CallConnectionId}"); + + RegisterToCallStateChangeEvent(callConnection.CallConnectionId); + + //Wait for operation to complete + await callEstablishedTask.Task.ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogMessage(Logger.MessageType.ERROR, string.Format("Failure occured while creating/establishing the call. Exception: {0}", ex.Message)); + throw ex; + } + } + + private void RegisterToCallStateChangeEvent(string callConnectionId) + { + callEstablishedTask = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); + reportCancellationToken.Register(() => callEstablishedTask.TrySetCanceled()); + + callTerminatedTask = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); + + //Set the callback method + var callStateChangeNotificaiton = new NotificationCallback((CallingServerEventBase callEvent) => + { + var callStateChanged = (CallConnectionStateChangedEvent)callEvent; + Logger.LogMessage(Logger.MessageType.INFORMATION, $"Call State changed to: {callStateChanged.CallConnectionState}"); + + if (callStateChanged.CallConnectionState == CallConnectionState.Connected) + { + callEstablishedTask.TrySetResult(true); + } + else if (callStateChanged.CallConnectionState == CallConnectionState.Disconnected) + { + EventDispatcher.Instance.Unsubscribe(CallingServerEventType.CallConnectionStateChangedEvent.ToString(), callConnectionId); + reportCancellationTokenSource.Cancel(); + callTerminatedTask.SetResult(true); + } + }); + + //Subscribe to the event + var eventId = EventDispatcher.Instance.Subscribe(CallingServerEventType.CallConnectionStateChangedEvent.ToString(), callConnectionId, callStateChangeNotificaiton); + } + + private async Task CreateUser() + { + var client = new CommunicationIdentityClient(connectionString); + var user = await client.CreateUserAsync().ConfigureAwait(false); + return new CommunicationUserIdentifier(user.Value.Id); + } + + private async Task PlayAudioAsync(string audioFileUrl) + { + if (reportCancellationToken.IsCancellationRequested) + { + Logger.LogMessage(Logger.MessageType.INFORMATION, "Cancellation request, PlayAudio will not be performed"); + return; + } + + try + { + if (string.IsNullOrWhiteSpace(audioFileUrl)) + { + audioFileUrl = Environment.GetEnvironmentVariable("AudioFileUrl"); + } + + // Preparing data for request + var playAudioRequest = new PlayAudioOptions() + { + AudioFileUri = new Uri(audioFileUrl), + OperationContext = Guid.NewGuid().ToString(), + Loop = true, + CallbackUri = new Uri($"{this.appCallbackUrl}outboundcall/callback?{EventAuthHandler.GetSecretQuerystring}"), + AudioFileId = "", + }; + + Logger.LogMessage(Logger.MessageType.INFORMATION, "Performing PlayAudio operation"); + var response = await callConnection.PlayAudioAsync(playAudioRequest, reportCancellationToken).ConfigureAwait(false); + + Logger.LogMessage(Logger.MessageType.INFORMATION, $"PlayAudioAsync response --> " + + $"{response.GetRawResponse()}, Id: {response.Value.OperationId}, Status: " + + $"{response.Value.Status}, OperationContext: {response.Value.OperationContext}, " + + $"ResultInfo: {response.Value.ResultInfo}"); + + if (response.Value.Status == OperationStatus.Running) + { + Logger.LogMessage(Logger.MessageType.INFORMATION, $"Play Audio state: {response.Value.Status}"); + } + } + catch (TaskCanceledException) + { + Logger.LogMessage(Logger.MessageType.ERROR, "Play audio operation cancelled"); + } + catch (Exception ex) + { + Logger.LogMessage(Logger.MessageType.ERROR, $"Failure occured while playing audio on the call. Exception: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Sendsms.csx b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Sendsms.csx new file mode 100644 index 00000000..aac198b1 --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/Sendsms.csx @@ -0,0 +1,34 @@ + +#load "Logger.csx" + +using System; +using Azure.Communication.Sms; +using System.Threading.Tasks; +using Azure; + +class SendSMS +{ + private SmsClient smsClient; + public SendSMS() + { + string connectionString = Environment.GetEnvironmentVariable("Connectionstring"); + smsClient = new SmsClient(connectionString); + } + + public async Task SendOneToOneSms(string sourcePhoneNumber, string targetPhoneNumber, string message) + { + try + { + Response sendResult = await smsClient.SendAsync( + from: sourcePhoneNumber, + to: targetPhoneNumber, + message: message + ); + Logger.LogMessage(Logger.MessageType.INFORMATION, $"Sms sent successfully with SMS-id: {sendResult.Value.MessageId}"); + } + catch (Exception ex) + { + Logger.LogMessage(Logger.MessageType.ERROR, $"Send message failed unexpectedly, reason: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/function.proj b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/function.proj new file mode 100644 index 00000000..5cbbb20e --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/function.proj @@ -0,0 +1,16 @@ + + + netstandard2.0 + + + + + + + + + + + + + \ No newline at end of file diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/run.csx b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/run.csx new file mode 100644 index 00000000..99e79e0e --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/SendNotification/run.csx @@ -0,0 +1,98 @@ +#r "Newtonsoft.Json" + +#load "Logger.csx" +#load "Phonecall.csx" +#load "Sendsms.csx" + +using System.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; + +public static IActionResult Run(HttpRequest req, ILogger log) +{ + string responseMessage = null; + + try + { + Logger.SetLoggerInstance(log); + + string requestBody = new StreamReader(req.Body).ReadToEnd(); + dynamic data = JsonConvert.DeserializeObject(requestBody); + string isSendNotification; + + try + { + isSendNotification = data?.SendNotification; + } + catch(Exception ex) + { + Logger.LogMessage(Logger.MessageType.INFORMATION, ex.Message); + isSendNotification = null; + } + + if (!string.IsNullOrWhiteSpace(isSendNotification) && isSendNotification.ToLower() == "true") + { + string sourceNumber = data?.SourceNumber; + string targetNumber = data?.OutboundNumber; + + if(!string.IsNullOrWhiteSpace(targetNumber) && !string.IsNullOrWhiteSpace(sourceNumber)) + { + string isSmsSend = data?.SMS?.Send; + Logger.LogMessage(Logger.MessageType.INFORMATION, $"sourceNumber --> {sourceNumber}"); + Logger.LogMessage(Logger.MessageType.INFORMATION, $"targerNumber --> {targetNumber}"); + + if (!string.IsNullOrWhiteSpace(isSmsSend) && isSmsSend.ToLower() == "true") + { + string message = data?.SMS?.Message; + if (!string.IsNullOrWhiteSpace(message)) + { + var sendSMS = new SendSMS(); + Task.Run(() => { sendSMS.SendOneToOneSms(sourceNumber, targetNumber, message); }); + } + else + { + responseMessage = "SMS notification request body missing message text. Please a message text to the request body."; + } + } + + string isInitiatePhoneCall = data?.PhoneCall?.Send; + if (!string.IsNullOrWhiteSpace(isInitiatePhoneCall) && isInitiatePhoneCall.ToLower() == "true") + { + string callbackUrl = $"{req.Scheme}://{req.Host}/api/"; + string audioUrl = data?.PhoneCall?.PlayAudioUrl; + var phoneCall = new Phonecall(callbackUrl); + Task.Run(() => { phoneCall.InitiatePhoneCall(sourceNumber, targetNumber, audioUrl); }); + } + + if(string.IsNullOrEmpty(responseMessage)) + responseMessage = "Notification sent successfully."; + } + else + { + responseMessage = "Notification sent failed. Pass target/source number in the request body."; + } + } + else + { + if (EventAuthHandler.Authorize(req)) + { + // handling callbacks + if (!string.IsNullOrWhiteSpace(requestBody)) + { + Task.Run(() => + { + EventDispatcher.Instance.ProcessNotification(requestBody); + }); + responseMessage = "OK"; + } + } + } + } + catch(Exception ex) + { + responseMessage = ex.Message; + } + + return new OkObjectResult(responseMessage); +} \ No newline at end of file diff --git a/OutboundCallReminder/OutboundFunction_AzurePortal/readme.md b/OutboundCallReminder/OutboundFunction_AzurePortal/readme.md new file mode 100644 index 00000000..990bc29a --- /dev/null +++ b/OutboundCallReminder/OutboundFunction_AzurePortal/readme.md @@ -0,0 +1,72 @@ +--- +page_type: sample +languages: +- csharp +products: +- azure +- azure-communication-services +--- + +# Outbound Azure Function Sample + +This azure function sample send SMS, make an outbound call and play audio using ACS SMS and Server Calling SDKs on the basis of json input given in the following format. + +- `SendNotification`: value should be `true`, for sending the notification. +- `OutboundNumber`: Target phone number where we need to send notification. +- `SourceNumber`: Phone number associated with the Azure Communication Service resource. +- `SMS.Send`: value should be true/false as we want to send SMS or not. +- `SMS.Message`: Message want to send as SMS. +- `PhoneCall.Send`: value should be true/false as we want to make a phone call or not. +- `PhoneCall.PlayAudioUrl`: The wav file url which accessible by the function app. If the value is empty, then the sample will use configured url through azure portal. + +``` +{ + "SendNotification": "true", + "SourceNumber":"+18xxxxxxxxxx", + "OutboundNumber": "+18xxxxxxxxxx", + "SMS": { + "Send": "true", + "Message": "message to be sent" + }, + "PhoneCall": { + "Send": "true", + "PlayAudioUrl": "audio file URL function app can able to access" + } +} +``` + +## Prerequisites + +- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/) +- Create an Azure Communication Services resource. For details, see [Create an Azure Communication Resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). You'll need to record your resource **connection string** for this sample. +- Get a phone number for your new Azure Communication Services resource. For details, see [Get a phone number](https://docs.microsoft.com/azure/communication-services/quickstarts/telephony-sms/get-phone-number?pivots=platform-azp) + +## Code structure + +- ./SendNotification/run.csx : Azure function to send notification using phone call and SMS and to handle outbound callbacks. +- ./SendNotification/Phonecall.csx : class for handling outbound phone call. +- ./SendNotification/SendSms.csx : class for sending SMS notification. +- ./SendNotification/EventHandler/*.csx : Code for managing callbacks and request authorization. +- ./SendNotification/function.proj : Contains nuget package references. + +## Create Function App using Azure portal + +- [Create a Function App](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-function-app-portal#create-a-function-app) +- [Create a HTTP Trigger function `SendNotification` with Authorization level `Anonymous`](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-function-app-portal#create-function). +- After creating the function edit `run.csx` and add other code files. + +## Add code files using `kudu` Tool + +- Under Function app menu, select `Advanced tool` under `Development tools` section. +- Click on the link `Go`, `Kudu` web got opened in new tab. +- Select `CMD` option under `Debug Console` menu. +- Add files under `/site/wwwroot/SendNotification/` folder. + +### Configuring Azure Function Sample + +After publishing your function App, add following configuration in your function App's `configuration` section: + +- Connectionstring: Azure Communication Service resource's connection string. +- SourcePhone: Phone number associated with the Azure Communication Service resource. +- SecretPlaceholder: Secret/Password that would be part of callback and will be use to validate incoming requests. +- AudioFileUrl: Url of default wav file going to play in outbound phone call which is accessible by function app.