Skip to content

Commit

Permalink
Function app developed through azure portal (#27)
Browse files Browse the repository at this point in the history
* Azure function app for outbound, using azure portal.

* Incorporated review comments

* Improvements in content and code

Co-authored-by: maulinasharma <[email protected]>
  • Loading branch information
ravithanneeru and maulinasharma authored Nov 11, 2021
1 parent 1c88634 commit 9ed1a89
Show file tree
Hide file tree
Showing 13 changed files with 656 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,32 @@ namespace OutboundFunction
using System.Threading.Tasks;
public class EventDispatcher
{
public static readonly EventDispatcher Instance;
private static readonly ConcurrentDictionary<string, NotificationCallback> NotificationCallback;
private readonly object SubscriptionLock = new object();
private static readonly EventDispatcher instance;
private static readonly ConcurrentDictionary<string, NotificationCallback> notificationCallbacks;
public static EventDispatcher Instance
{
get
{
return instance;
}
}

static EventDispatcher()
{
Instance = new EventDispatcher();
NotificationCallback = new ConcurrentDictionary<string, NotificationCallback>(StringComparer.OrdinalIgnoreCase);
instance = new EventDispatcher();
notificationCallbacks = new ConcurrentDictionary<string, NotificationCallback>(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)
Expand All @@ -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);
});
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> callEstablishedTask;
private TaskCompletionSource<bool> 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();
Expand All @@ -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),
Expand Down Expand Up @@ -114,7 +111,7 @@ private void RegisterToCallStateChangeEvent(string callConnectionId)
var eventId = EventDispatcher.Instance.Subscribe(CallingServerEventType.CallConnectionStateChangedEvent.ToString(), callConnectionId, callStateChangeNotificaiton);
}

private static async Task<CommunicationUserIdentifier> CreateUser(string connectionString)
private async Task<CommunicationUserIdentifier> CreateUser()
{
var client = new CommunicationIdentityClient(connectionString);
var user = await client.CreateUserAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -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 = "",
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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); });
Expand All @@ -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;
Expand Down
80 changes: 80 additions & 0 deletions OutboundCallReminder/OutboundFunction/readme.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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)}";
}
Original file line number Diff line number Diff line change
@@ -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<string, NotificationCallback> notificationCallbacks;

public static EventDispatcher Instance
{
get
{
return instance;
}
}

static EventDispatcher()
{
instance = new EventDispatcher();
notificationCallbacks = new ConcurrentDictionary<string, NotificationCallback>(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}";
}

/// <summary>
/// Extracting event from the json.
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Azure.Communication.CallingServer;
using System;

public class NotificationCallback
{
public Action<CallingServerEventBase> Callback { get; set; }

public NotificationCallback(Action<CallingServerEventBase> callBack)
{
this.Callback = callBack;
}
}
Loading

0 comments on commit 9ed1a89

Please sign in to comment.