How to integrate Azure Functions, GIPHY and Microsoft Teams?

Today I have pretty funny topic to show you all - we'll try to create a simple Azure Function, which will... find a random GIF image and post in on a channel in Microsoft Teams! Why such article? Well, we can't always do serious things :)

To bo honest, such triangle shows easily how to achieve integration on a corporate level using minimal resources. 

Setup

To start you have to have a Microsoft Teams channel created. I won't go into details of creating one(since it's pretty basic stuff) and just assume, you already have one. What we're interested in are the Connectors available:

When you click on the menu item, you'll see a window with many available connectors. We're searching for a particular one named Incoming Webhook:

When you click Configure, you'll see a simple wizard where you can insert a name of a WebHook and select an image. Once you click Create, a webhook URL will be provided so copy and save it. It'll look like this:

https://outlook.office.com/webhook/.../IncomingWebhook/.../...

Now we have to create a function.

Function

I decided to create a function, which will be triggered from Monday to Friday at 9:30 AM. To do so selected TimerTrigger with the following signature:

[TimerTrigger("0 30 9 * * 1-5")]TimerInfo myTimer

The whole code looks like this:

/
public static class GiphyTrigger
{
	private const string WebhookUrl = "https://outlook.office.com/webhook/.../IncomingWebhook/.../...";

	private static Lazy<HttpClient> HttpClient = new Lazy<HttpClient>(() => new HttpClient());

	[FunctionName("GiphyTrigger")]
	public static async Task Run([TimerTrigger("0 30 9 * * 1-5")]TimerInfo myTimer, TraceWriter log)
	{
		log.Info($"C# Timer trigger function executed at: {DateTime.Now}");

		var randomGif = await HttpClient.Value.GetAsync("https://api.giphy.com/v1/gifs/random?api_key=...&tag=&rating=PG-13");
		var content = await randomGif.Content.ReadAsStringAsync();
		var model = JsonConvert.DeserializeObject<GiphyModel>(content);

		log.Info($"Sending GIF to Microsoft Teams");
		var result = await HttpClient.Value.PostAsync(WebhookUrl, new StringContent($"{{\"@type\": \"MessageCard\",\"@context\": \"http://schema.org/extensions\",\"summary\": \"This is GIF\",\"themeColor\": \"0075FF\",\"sections\": [{{\"startGroup\": true,\"title\": \"**GIPHY says:**\",\"text\": \"![Text]({model.Data.Url})\"}}]}}"));

		result.EnsureSuccessStatusCode();
		log.Info($"Result is {result.StatusCode}");
	}       
}

public sealed class GiphyModel 
{
		public GiphyDataModel Data {get;set;}
}

public class GiphyDataModel
{
	public string Title {get;set;}
	[JsonProperty("image_url")]
	public string Url {get;set;}
}

In general it's more or less generic code, which calls GIPHY API and obtains a random gif. One thing is important however - when calling a webhook, the body of a request is a valid Actionable message, which is a special schema used within Office 365. You can find more info here.

Result

When a function is triggered, you may see following result:

I named my webhook Squirrel Commando, and now it greets me everyday with a random GIF. Of course you can use Incoming Webhooks in Microsoft Teams for more serious tasks(like some reports, alerts or notifications) and integrate all using Azure Functions(with Consumption Plan it'll cost you almost nothing...) - with such generic functionality, only the sky is the limit.

Beware of TimerTrigger in Azure Functions

TimerTrigger is one of the easiest triggers in Azure Functions to handle. It has however an interesting "issue", which is described in the documentation - you should not use the same id property for different Function App. Why is that?

Just as planned

Let's assume you have two identical functions triggered using TimerTrigger:

/
[FunctionName("TimerTrigger")]
public static void Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, TraceWriter log)
{
	log.Info($"TIMER trigger function executed at: {DateTime.Now}");
}

Assuming our host.json file is empty, after we deploy two Function Apps, we'll get following results:

As you can see both are working correctly(I changed displayed text a little bit so you can see the difference). Now let's abuse things a little - I forced two Function Apps to work under the same id. To do so I changed my host.json file:

/
{
  "id": "9f4ea53c5136457d883d685e57164f08"
}

With this change something broke:

As we can read in documentation:

If you share a Storage account across multiple function apps, make sure that each function app has a different id in host.json. You can omit the id property or manually set each function app's id to a different value. The timer trigger uses a storage lock to ensure that there will be only one timer instance when a function app scales out to multiple instances. If two function apps share the same id and each uses a timer trigger, only one timer will run.

What is the cause of such behaviour?

Underlying functionality

If you check the underlying storage powering Azure Function, you'll find azure-webjobs-host container, which stores e.g. locks from TimerTrigger. Mine looks like this:

As you can see I have 3 different locks - timertriggertest come from the previous version of my functionality, where I didn't have id explicitely set. The very first folder is named using id property in host.json file.

We can find also in the source code. There's a class PrimaryHostCoordinator, which is used to determine which host instance is the primary one. It is initialized at the very beginning after functions has been loaded and tries to aquire a lock on a blob every 5 seconds. If it fails, a function won't be triggered. Remember not to set id if you don't have to and if you do, try to avoid using functions tiggered by TimerTrigger in different Function Apps.