Building a Proactive Microsoft Teams Bot — Part 2

Chris
9 min readJan 6, 2021

--

Learning to build a Microsoft Teams Bot — Getting public holiday data into the bot

About this Series

I am (as of the date of writing) the Head of Development for several development teams which are mostly based in three offices — London, Malta and Barcelona. They have varied and wildly different public holiday schedules and we don’t officially track them. We regularly have a team(s) not in the office/not available when everyone else is. This is where this bot comes in.

I’m going to massively over-engineer a solution to inform the teams of public holidays occurring in any of the other locations.

My goal for this series is to build a Teams Bot which can inform the end-user of when the public holidays are in their region on either an on-demand or a scheduled basis. This is a POC/MVP with a longer-term goal of having the bot do more useful things later, but as mentioned previously is a bit of overkill for now.

Disclaimer: I have built neither a Bot nor a Teams App before so this is very much a “follow along while I learn” article.

3 Part Series:

  • Part 1: Learning to build a Microsoft Teams Bot — Getting the Echo Bot up and running
  • Part 2: This article
  • Part 3: Making the bot proactive

This article aims to get data from an API or flat file into the Teams bot in a way which succinctly shares the information I’m trying to get across.

If you are only interested in the actual Bot changes skip to “Putting it all into practice in our bot” as the rest of it is me prepping the data.

Data Considerations

In trying to decide what would have been the best and the fastest way to get this data in the right format for the bot, I felt I had two options:

1. Hard-code the data in a flat-file

On the pro side, this is very simple, I can put the exact data I need into a file and use it as is. I can get the data from the public governmental websites so the sourcing of it is easy.

On the cons side — I will need to do this every year and to automate it I'll need to redo this entire section.

2. Get the data from some 3rd party

This has the pro of being something I should be able to do once, and never need to touch again. (Although, as I'll go into briefly, the rules around public holidays in Barcelona mean that I was highly dubious that any data source would get it 100% correct, which I was unfortunately correct about).

On the cons side, this will certainly be some larger amount of work than just hard-coding it.

On the whole, given the above considerations, I felt it would be better if I could find a free source of data, to integrate against that and then work out how far away the data was from what I needed.

Having used one of my favourite API aggregating websites (https://rapidapi.com) I found https://calendarific.com/

They have a free tier allowing me to use up to 1000 requests a month and if I need more than that their pricing is pretty reasonable. They have good API documentation and official SDK’s in essentially every language except C#… sigh.

Signing up was easy, the token goes into the URL and its very Postman friendly. After a few minutes of reading the documentation, this is the kind of data available for Malta.

After looking at the data, I could see that I was going to need to modify it to make it fit for purpose. As such, I built a quick test harness to interact with the API and to simulate what the response would look like, at any given point of the coming year, to the question “What are the holidays coming up this week?”

A website which I have used over and over when building anything around an API has been https://json2csharp.com/. You can paste in the output from an API and it will generate your C# POCO classes automatically. I've found it a huge time saver.

Once I had the entities in place getting data from the API was as simple as:

var client = new HttpClient();
client.BaseAddress = new Uri("https://calendarific.com");
var ukTask = client.GetAsync(
"/api/v2/holidays?
&api_key=xxYourKeyHerexx
&country=uk
&year=2021
&location=gb-eng
&type=local,national");
var result = ukTask.Result;
result.EnsureSuccessStatusCode();
var content = result.Content.ReadAsStringAsync().Result;
var root = JsonConvert.DeserializeObject<Root>(content);

My next steps were to look at all the countries I was interested in, compare the data I was getting back from the API to the government websites and adjust where appropriate.

This is where the idiosyncrasies started to creep in.

UK: https://www.gov.uk/bank-holidays

If any of the 8 public holidays fall on a weekend, they get moved to the following weekday. In the data, they are represented twice, so I needed to remove the entry for the weekend. This is something I do for all locations anyway but the substitute day is already in the data, so it just works.

Malta: https://publicholidays.com.mt/2021-dates/

If any of the 14(!) public holidays fall on a weekend they don’t impact the working week. Nothing special to do here, which is nice. (Apparently, there is proposed legislation which would see a public holiday which falls on a weekend added to an employee’s leave balance. Which is a nice touch but will not concern this project, so it’s not really relevant.)

Barcelona: https://ajuntament.barcelona.cat/calendarifestius/en/

Of the 14(! again!) possible holidays… it’s a mess. Even when specifying the region of Catalonia, the data is pretty far away from where it needs to be. Some of the holidays which are celebrated in Spain are not celebrated in Catalonia (and it’s not based on any logic I can discern). There are some extra Catalonian holidays which are not celebrated in Spain and there are 2 extra holidays added by the Barcelona municipal council which are also not represented.

We can start by removing the weekend holidays as they don’t matter for our purposes. This is all of the changes required for the UK and Malta.

var weekendDays = 
new List<DayOfWeek> { DayOfWeek.Saturday, DayOfWeek.Sunday };
var weekendHolidays =
holidays
.Where(h => weekendDays.Contains(h.Date.DateTime.DayOfWeek))
.ToList();
foreach (var weekendHoliday in weekendHolidays)
{
holidays.Remove(weekendHoliday);
}

Then we need to deal with Barcelona. Brace yourself.

// In Catalonia the holiday is held on the Monday, but reflects in //the data on a Sunday
var whitSunday = holidays.FirstOrDefault(
h => h.Name == "Whit Sunday/Pentecost");
if (whitSunday != null)
{
whitSunday.Type.Clear();
whitSunday.Type.Add("Barcelona holiday");
whitSunday.Name = "Whit Monday/Pentecost Monday";
whitSunday.Date.DateTime = whitSunday.Date.DateTime.AddDays(1);
}
// Local Barcelona holiday, held on the 24th of September every
// year.
var laMerce = new Holiday()
{
Name = "La Mercè",
Description = "The annual celebration of the city of Barcelona,
celebrating the Virgin of Grace.",
Type = new List<string> { "Barcelona holiday" },
Country = new Country
{
Id = "es",
Name = "Spain"
},
Date = new Date
{
DateTime = new DateTime(whitSunday.Date.DateTime.Year, 9,
24)
}
};
// This holiday doesnt appear in the data at all for some strange reason
var allSaints = new Holiday()
{
Name = "All Saints Day",
//Description = "The annual celebration of the city of
Barcelona, celebrating the Virgin of Grace.",
Type = new List<string> { "National holiday" },
Country = new Country
{
Id = "es",
Name = "Spain"
},
Date = new Date
{
DateTime = new DateTime(whitSunday.Date.DateTime.Year, 11,
1)
}
};
holidays.Add(laMerce);
holidays.Add(allSaints);
// This holiday appears to be a spanish national holiday, but isnt // being celebrated in Barcelona
var feastOfTheAssumption =
holidays.FirstOrDefault(h => h.Name == "Assumption of Mary");
if (feastOfTheAssumption != null)
{
holidays.Remove(feastOfTheAssumption);
}
var assumptionObserved =
holidays.FirstOrDefault(h => h.Name == "Assumption observed");
if (assumptionObserved != null)
{
holidays.Remove(assumptionObserved);
}

After this step, the data can be consolidated into a single list and the test harness has reached the end of its usefulness. Time to turn this into a proper library.

For anybody who wants to have a look at exactly how this works (or play around with it themselves), I’ve uploaded this code to Github.

Consolidating the Data

I didn’t want any of the details of this API (or of my manual changes) leaking into my implementation so I wanted to wrap it all into a ServiceClient library with 2 sets of entities, the set used for interacting with Calendarific and the set used for passing data into and out of the library.

Given how much fiddling with the data I did, I have a sneaking suspicion I may end up replacing Calendarific in the future so I don’t want to have to make any changes to the downstream code. I’ve hidden all the details including the token and the URI in the CalendarificServiceClient and am using IHolidayRepository to hide even that.

internal interface IHolidayRepository
{
/// <summary>
/// Get all national holidays for the UK
/// </summary>
/// <returns></returns>
Task<List<Holiday>> GetUkHolidays(int year);
/// <summary>
/// Get all national holidays for Barcelona
/// </summary>
/// <returns></returns>
Task<List<Holiday>> GetBarcelonaHolidays(int year);
/// <summary>
/// Get all national holidays for Malta
/// </summary>
/// <returns></returns>
Task<List<Holiday>> GetMaltaHolidays(int year);
}

Finally, I can wire up my NationalHolidayRepository cleanly using DI as follows

services.AddTransient<INationalHolidayRepository, NationalHolidayRepository>();

Putting it all into practice in our bot

It is (finally) time to have the bot actually do something useful with all this effort.

Firstly, its time to rename this bot. It is no longer the EchoBot but shall be known henceforth as the NationalHolidayBot!

Ultimately, I do want this bot to send a message every Monday to a Teams Channel about the upcoming holidays (or do nothing if there are none). However, for now, I’m happy enough to piggyback off the existing messaging functionality. If a user says anything with the word “holiday” in it then the bot will respond with which holidays are coming up in the next 8 days (because it’s not useful if the public holiday is next Monday, and you only find out about on the day), otherwise, the bot will revert to the standard echo functionality.

This is fairly straightforward at this point.

For the identification of the keyword:

protected override async Task OnMessageActivityAsync(
ITurnContext<IMessageActivity> turnContext,
CancellationToken cancellationToken)
{
var replyText = $"Echo: {turnContext.Activity.Text}";
if (turnContext.Activity.Text.ToLower().Contains("holiday"))
{
replyText = await ConstructNationalHolidayMessage();
}

await turnContext.SendActivityAsync(
MessageFactory.Text(replyText, replyText), cancellationToken);
}

The logic to calculate the holidays for the next 8 days (as well as dealing with the edge case that we may be in a period where 8 days overlaps between years).

private async Task<string> ConstructNationalHolidayMessage()
{
var thisYear = await
_nationalHolidayRepository
.GetAllHolidays(DateTime.UtcNow.Year);
var holidays =
thisYear.Where(ty =>
ty.Date > DateTime.UtcNow.AddDays(-1) &&
ty.Date < DateTime.UtcNow.AddDays(8))
.ToList();
if (DateTime.UtcNow.AddDays(8) >
new DateTime(DateTime.UtcNow.Year + 1, 1, 1))
{
var nextYear = await
_nationalHolidayRepository
.GetAllHolidays(DateTime.UtcNow.Year + 1);
var validHolidaysNextYear =
nextYear.Where(ty =>
ty.Date > DateTime.UtcNow.AddDays(-1) &&
ty.Date < DateTime.UtcNow.AddDays(8))
.ToList();
holidays.AddRange(validHolidaysNextYear);
}
if (holidays.Count > 1)
{
return FormatNationalHolidayReply(holidays);
}
return "There are no public holidays coming up in any of
Barcelona, Malta or the UK in the next week";
}

And lastly, a little bit of logic just to get the formatting right between the different numbers of holidays available.

private static string FormatNationalHolidayReply(
List<NationalHolidayContainer> holidays)
{
var replyText = string.Empty;
if (holidays.Count == 1)
{
replyText = "There is one holiday in a Trading office over
the next week. ";
}
else
{
replyText = "These are the holidays in the Trading offices
over the next week. ";
}
foreach (var holiday in holidays)
{
replyText +=
$"{Environment.NewLine}{holiday.Date.DayOfWeek}
({holiday.Date.Date:dd MMM}) is {holiday.Name}.
It is a day off in : ";
foreach (var location in holiday.Locations)
{
replyText += $"{location}. ";
}
replyText += $" {Environment.NewLine}";
}
return replyText;
}

Which, when run, produces something like the following output.

Success! The next step is to work out how to make the bot do this on a schedule. This turns out to be harder than it sounds but I'll talk about it in the next article.

--

--

Chris
Chris

Written by Chris

Software: Developer, Architect, Lead, Head of

No responses yet