Working with Azure functions (part 2 – C#)

In my first blog post about Azure functions, I created an Azure function app and a function that uses Powershell to read data from RSS and writes it to Azure Table Storage. In this post, I’ll create a C# function that reads all upcoming events of my https://www.meetup.com groups and creates an iCal file out of it.
Unfortunately it’s not possible to do that at the meetup site. What you can do is, that you (manually) subscribe to each iCal calendar of each group that you have, but that results in a lot of calendars and if you join or leave a group, you also have to add/remove the calendar subscription.

Building the C# application

I’ll at first create a simple C# application in VisualStudio and move the code later on to the Azure function. The application itself is simple and does the following steps:

  1. Read data from the meetup API
  2. Transform the data to an event object
  3. Create an iCal file

To achieve that, I’ll at first add the NuGet packages “Ical.Net” and “Newtonsoft.Json” to my console application.

20161109_01_nugetpackages

Read data from meetup API

Reading the data from the meetup API is simple. The rest call to /self/calender will give us all upcoming events. The call ca be tested with the meetup API console: https://secure.meetup.com/meetup_api/console/?path=/self/calendar.

For the final REST call, we have to including the API key which results in the following URL:
https://api.meetup.com/self/calendar?&sign=true&photo-host=public&page=200&key=[MyAPIKey]
You can get your API key here: https://secure.meetup.com/meetup_api/key/

Seems easy, we just have to call that REST service in our C# application and transform the JSON result to a C# object:

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
 
namespace CodeHollow.MeetupIcal
{
    public class MeetupEvent
    {
        public DateTime Created { get; private set; }
        public DateTime Date { get; private set; }
        public DateTime EndDate { get; private set; }
        public string Group { get; private set; }
        public string Id { get; private set; }
        public string Link { get; private set; }
        public string Location { get; private set; }
        public string Title { get; private set; }
        public string Description { get; private set; }
        public Tuple<double, double> GeoLocation { get; private set; }
 
        public MeetupEvent(JToken eventInfo)
        {
            Created = eventInfo.Value<long>("created").FromUnixTime();
            Date = eventInfo.Value<long>("time").FromUnixTime();
            EndDate = Date.AddMilliseconds(eventInfo.Value<long>("duration"));
            Id = eventInfo.Value<string>("id");
            Link = eventInfo.Value<string>("link");
            Title = eventInfo.Value<string>("name");
            Description = eventInfo.Value<string>("description");
            Group = eventInfo["group"].Value<string>("name");
            var venue = eventInfo["venue"];
            if (venue != null)
            {
                Location = string.Format("{0}, {1}, {2}",
                    venue.Value<string>("name"),
                    venue.Value<string>("address_1"),
                    venue.Value<string>("city"));
 
                GeoLocation = new Tuple<double, double>(venue.Value<double>("lat"), venue.Value<double>("lon"));
            }
        }
 
        public override string ToString()
        {
            return string.Format("{0}, {1}, {2}, {3}", Group, Title, Date.ToLocalTime(), EndDate.ToLocalTime());
        }
    }
 
    public static class DateTimeExtensions
    {
        public static DateTime FromUnixTime(this long unixTime)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return epoch.AddMilliseconds(unixTime);
        }
    }
 
    class Program
    {
        const string MEETUP_URL = "https://api.meetup.com/self/calendar?&sign=true&photo-host=public&page=200&key={0}";
 
        static void Main(string[] args)
        {
            Console.WriteLine("Getting events from meetup...");
            var getEventsTask = GetEvents("[API KEY]");
            getEventsTask.Wait();
 
            if (!getEventsTask.IsFaulted)
            {
                var events = getEventsTask.Result;
                events.ForEach(x => Console.WriteLine(x.ToString()));
            }
 
            Console.WriteLine("Done! Press key to exit.");
            Console.Read();
        }
 
        public static async Task<List<MeetupEvent>> GetEvents(string apiKey)
        {
            if (String.IsNullOrEmpty(apiKey))
                throw new ArgumentException("API key is missing, please visit https://secure.meetup.com/meetup_api/key/ to get your API Key", "apiKey");
 
            var client = new HttpClient();
            client.DefaultRequestHeaders.Add("Accept", "application/json");
 
            var jsonResult = await client.GetStringAsync(string.Format(MEETUP_URL, apiKey));
            JToken token = JToken.Parse(jsonResult);
 
            var result = new List<MeetupEvent>();
 
            foreach (var item in token)
            {
                try
                {
                    result.Add(new MeetupEvent(item));
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
 
            return result;
        }
    }
}

I put everything into one file because the Azure function will also be just one file. It’s possible to create multiple functions and files in Azure functions, but it’s much easier if it’s just one.
The code above reads the next 200 upcoming events from the meetup API and writes them to the output. The output looks like:

Getting events from meetup…
Event XYZ1, 05.11.2016 09:30:00, 05.11.2016 19:30:00
Event XYZ2, 06.11.2016 10:00:00, 06.11.2016 15:00:00
Event XYZ3, 10.11.2016 09:15:00, 10.11.2016 09:15:00
Done! Press key to exit.

Creating an iCal file

I already added the NuGet package Ical.Net (thanks to Rian Stockbower) which makes creating an iCal file very easy:

public static string CreateIcal(List<MeetupEvent> events)
{
    var calendar = new Calendar();
    calendar.AddProperty("X-WR-CALNAME", "My Meetups"); // sets the calendar title
    calendar.AddProperty("X-ORIGINAL-URL", "https://arminreiter.com");
    calendar.AddProperty("METHOD", "PUBLISH");
    foreach (var item in events)
    {
        var icalevent = new Event()
        {
            DtStart = new CalDateTime(item.Date),
            DtEnd = new CalDateTime(item.EndDate),
            Created = new CalDateTime(item.Created),
            Location = item.Location,
            Summary = item.Title,
            Url = new Uri(item.Link)
        };
 
        string description = item.Description;
        if (!String.IsNullOrEmpty(item.Link))
            description = string.Format("URL: <a href=\"{0}\">{0}</a><br />{1}", item.Link, item.Description);
 
        icalevent.AddProperty("X-ALT-DESC;FMTTYPE=text/html", description); // creates an HTML description
 
        if (item.GeoLocation != null)
            icalevent.GeographicLocation = new GeographicLocation(item.GeoLocation.Item1, item.GeoLocation.Item2);
 
        calendar.Events.Add(icalevent);
    }
     
    var serializer = new CalendarSerializer(new SerializationContext());
    return serializer.SerializeToString(calendar);
}

This method creates the ical file out of my events. We just need to call it and write the output to the local file system.

The final console application

using Ical.Net;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Ical.Net.Serialization.iCalendar.Serializers;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
 
namespace CodeHollow.MeetupIcal
{
    public class MeetupEvent
    {
        public DateTime Created { get; private set; }
        public DateTime Date { get; private set; }
        public DateTime EndDate { get; private set; }
        public string Group { get; private set; }
        public string Id { get; private set; }
        public string Link { get; private set; }
        public string Location { get; private set; }
        public string Title { get; private set; }
        public string Description { get; private set; }
        public Tuple<double, double> GeoLocation { get; private set; }
 
        public MeetupEvent(JToken eventInfo)
        {
            Created = eventInfo.Value<long>("created").FromUnixTime();
            Date = eventInfo.Value<long>("time").FromUnixTime();
            EndDate = Date.AddMilliseconds(eventInfo.Value<long>("duration"));
            Id = eventInfo.Value<string>("id");
            Link = eventInfo.Value<string>("link");
            Title = eventInfo.Value<string>("name");
            Description = eventInfo.Value<string>("description");
            Group = eventInfo["group"].Value<string>("name");
            var venue = eventInfo["venue"];
            if (venue != null)
            {
                Location = string.Format("{0}, {1}, {2}",
                    venue.Value<string>("name"),
                    venue.Value<string>("address_1"),
                    venue.Value<string>("city"));
 
                GeoLocation = new Tuple<double, double>(venue.Value<double>("lat"), venue.Value<double>("lon"));
            }
        }
 
        public override string ToString()
        {
            return string.Format("{0}, {1}, {2}, {3}", Group, Title, Date.ToLocalTime(), EndDate.ToLocalTime());
        }
    }
 
    public static class DateTimeExtensions
    {
        public static DateTime FromUnixTime(this long unixTime)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return epoch.AddMilliseconds(unixTime);
        }
    }
 
    class Program
    {
        const string MEETUP_URL = "https://api.meetup.com/self/calendar?&sign=true&photo-host=public&page=200&key={0}";
 
        static void Main(string[] args)
        {
            Console.WriteLine("Getting events from meetup...");
            var getEventsTask = GetEvents("[API KEY]");
            getEventsTask.Wait();
 
            if (!getEventsTask.IsFaulted)
            {
                var events = getEventsTask.Result;
                events.ForEach(x => Console.WriteLine(x.ToString()));
 
                System.IO.File.WriteAllText("C:\\data\\mymeetups.ics", CreateIcal(events));
            }
 
            Console.WriteLine("Done! Press key to exit.");
            Console.Read();
        }
 
        public static async Task<List<MeetupEvent>> GetEvents(string apiKey)
        {
            if (String.IsNullOrEmpty(apiKey))
                throw new ArgumentException("API key is missing, please visit https://secure.meetup.com/meetup_api/key/ to get your API Key", "apiKey");
 
            var client = new HttpClient();
            client.DefaultRequestHeaders.Add("Accept", "application/json");
 
            var jsonResult = await client.GetStringAsync(string.Format(MEETUP_URL, apiKey));
            JToken token = JToken.Parse(jsonResult);
 
            var result = new List<MeetupEvent>();
 
            foreach (var item in token)
            {
                try
                {
                    result.Add(new MeetupEvent(item));
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
 
            return result;
        }
 
        public static string CreateIcal(List<MeetupEvent> events)
        {
            var calendar = new Calendar();
            calendar.AddProperty("X-WR-CALNAME", "My Meetups"); // sets the calendar title
            calendar.AddProperty("X-ORIGINAL-URL", "https://arminreiter.com");
            calendar.AddProperty("METHOD", "PUBLISH");
            foreach (var item in events)
            {
                var icalevent = new Event()
                {
                    DtStart = new CalDateTime(item.Date),
                    DtEnd = new CalDateTime(item.EndDate),
                    Created = new CalDateTime(item.Created),
                    Location = item.Location,
                    Summary = item.Title,
                    Url = new Uri(item.Link)
                };
 
                string description = item.Description;
                if (!String.IsNullOrEmpty(item.Link))
                    description = string.Format("URL: <a href=\"{0}\">{0}</a><br />{1}", item.Link, item.Description);
 
                icalevent.AddProperty("X-ALT-DESC;FMTTYPE=text/html", description); // creates an HTML description
 
                if (item.GeoLocation != null)
                    icalevent.GeographicLocation = new GeographicLocation(item.GeoLocation.Item1, item.GeoLocation.Item2);
 
                calendar.Events.Add(icalevent);
            }
             
            var serializer = new CalendarSerializer(new SerializationContext());
            return serializer.SerializeToString(calendar);
        }
    }
}

Building the Azure function

The console application works and creates the iCal file. Next step is to move everything to an Azure function. The Azure function is a C# function that runs every time when someone calls a specific URL. The URL will contain the API key and will return the iCal file. So we have to do the following things:

  1. Create Azure function with HTTP trigger
  2. Add the NuGet packages to the Azure function
  3. Move the C# code to the Azure function and adapt it

Create Azure function with HTTP trigger

20161109_02_createazurefunction

The authorization level handles who is allowed to call the function. The value Function means that you need a function key to call the URL, Admin means that you need the master key. Both keys can be found under “Manage”. I use Anonymous so that everyone can use it…let’s see if that will be a mistake :). And here is my new function:

20161109_03_csharphttpfunction

Add the NuGet packages to the Azure function

The NuGet packages for the Azure function are maintained in the project.json file of the function. So we need to get access to the file system. Fortunately that’s simple. Navigate to the “Function app settings” and open the “App service settings”

20161109_04_appservicesettings

In the upcoming window, scroll down to the “Development Tools” section and open the App Service Editor which is currently in preview.

In the app service editor, open the function and add a new file “project.json” with the following content:

{
  "frameworks": {
    "net46": {
      "dependencies": {
        "Ical.Net": "2.2.19",
        "Newtonsoft.Json": "9.0.1"
      }
    }
   }
}
20161109_05_azurefunctionsnugetpackages

The app service editor automatically saves the file. So after inserting the packages, we can go back to the Azure function and check the log file. If everythings correct, then we should see a message that all packages are restored:

20161109_06_nugetlogoutput

You can also simply add some using statements at the top of the function with namespaces that are part of the NuGet packages. When you save the function, the code is automatically compiled and you can see the output in the log window.

Move the C# code to the Azure function

The next step is to move the C# code to the Azure function. It’s not completely straight forward, but also not a big deal. I had to change the following things so that it works:

  • Replace Console.WriteLine by log.Info, log.Warning or log.Verbose
  • Removed the DateTimeExtensions class (but let the method still there so that the extension is not nested into another class)
  • Replaced the apikey by using the one from the request parameters
  • Return the iCal file as HttpResponse (instead of writing to the filesystem)

This results in the following code:

using Ical.Net;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Ical.Net.Serialization.iCalendar.Serializers;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
  
const string MEETUP_URL = "https://api.meetup.com/self/calendar?&sign=true&photo-host=public&page=200&key={0}";
  
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    try
    {
        log.Info($"C# HTTP trigger function processed a request. RequestUri={req.RequestUri}");
     
        // parse query parameter
        string apikey = req.GetQueryNameValuePairs()
            .FirstOrDefault(q => string.Compare(q.Key, "apikey", true) == 0)
            .Value;
     
        // Get request body
        dynamic data = await req.Content.ReadAsAsync<object>();
     
        // Set name to query string or body data
        apikey = apikey ?? data?.apikey;
     
        if(String.IsNullOrEmpty(apikey) || apikey.Equals("[YOURAPIKEY]", StringComparison.InvariantCultureIgnoreCase))
            return req.CreateResponse(HttpStatusCode.BadRequest, "Please pass your meetup apikey (https://secure.meetup.com/meetup_api/key/) as URL parameter: https://codehollow-functions.azurewebsites.net/api/MeetupToICal?apikey=[YOURAPIKEY]");
      
        log.Verbose("Getting events from meetup...");
     
        var getEventsTask = GetEvents(apikey, log);
        getEventsTask.Wait();
     
        if (!getEventsTask.IsFaulted)
        {
            var events = getEventsTask.Result;
            //events.ForEach(x => log.Info(x.ToString()));
            var icalContent = CreateIcal(events);
     
            // ical content to byte array and return it as attachment
            var result = new HttpResponseMessage(HttpStatusCode.OK);
            result.Content = new ByteArrayContent(System.Text.Encoding.UTF8.GetBytes(icalContent));
            result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
                { FileName = "mymeetups.ics" };
            result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
     
            return result;
        }
     
        return req.CreateResponse(HttpStatusCode.BadRequest, "An error occurred. Please contact [email protected]");
    }
    catch(Exception ex)
    {
        log.Warning(ex.ToString());
        return req.CreateResponse(HttpStatusCode.BadRequest, "An error occurred. Please check your API key or contact [email protected]");
    }
}
  
public static async Task<List<MeetupEvent>> GetEvents(string apiKey, TraceWriter log)
{
    if (String.IsNullOrEmpty(apiKey))
        throw new ArgumentException("API key is missing, please visit https://secure.meetup.com/meetup_api/key/ to get your API Key", "apiKey");
  
    var client = new HttpClient();
    client.DefaultRequestHeaders.Add("Accept", "application/json");
  
    var jsonResult = await client.GetStringAsync(string.Format(MEETUP_URL, apiKey));
    JToken token = JToken.Parse(jsonResult);
  
    var result = new List<MeetupEvent>();
  
    foreach (var item in token)
    {
        try
        {
            result.Add(new MeetupEvent(item));
        }
        catch (Exception ex)
        {
            log.Warning(ex.ToString());
        }
    }
  
    return result;
}
  
public static string CreateIcal(List<MeetupEvent> events)
{
    var calendar = new Calendar();
    calendar.AddProperty("X-WR-CALNAME", "My Meetups"); // sets the calendar title
    calendar.AddProperty("X-ORIGINAL-URL", "https://arminreiter.com");
    calendar.AddProperty("METHOD", "PUBLISH");
    foreach (var item in events)
    {
        var icalevent = new Event()
        {
            DtStart = new CalDateTime(item.Date),
            DtEnd = new CalDateTime(item.EndDate),
            Created = new CalDateTime(item.Created),
            Location = item.Location,
            Summary = item.Title,
            Url = new Uri(item.Link)
        };
  
        string description = item.Description;
        if (!String.IsNullOrEmpty(item.Link))
            description = string.Format("URL: <a href=\"{0}\">{0}</a><br />{1}", item.Link, item.Description);
  
        icalevent.AddProperty("X-ALT-DESC;FMTTYPE=text/html", description); // creates an HTML description
  
        if (item.GeoLocation != null)
            icalevent.GeographicLocation = new GeographicLocation(item.GeoLocation.Item1, item.GeoLocation.Item2);
  
        calendar.Events.Add(icalevent);
    }
      
    var serializer = new CalendarSerializer(new SerializationContext());
    return serializer.SerializeToString(calendar);
}
  
public class MeetupEvent
{
    public DateTime Created { get; private set; }
    public DateTime Date { get; private set; }
    public DateTime EndDate { get; private set; }
    public string Group { get; private set; }
    public string Id { get; private set; }
    public string Link { get; private set; }
    public string Location { get; private set; }
    public string Title { get; private set; }
    public string Description { get; private set; }
    public Tuple<double, double> GeoLocation { get; private set; }
  
    public MeetupEvent(JToken eventInfo)
    {
        Created = eventInfo.Value<long>("created").FromUnixTime();
        Date = eventInfo.Value<long>("time").FromUnixTime();
        EndDate = Date.AddMilliseconds(eventInfo.Value<long>("duration"));
        Id = eventInfo.Value<string>("id");
        Link = eventInfo.Value<string>("link");
        Title = eventInfo.Value<string>("name");
        Description = eventInfo.Value<string>("description");
        Group = eventInfo["group"].Value<string>("name");
        var venue = eventInfo["venue"];
        if (venue != null)
        {
            Location = string.Format("{0}, {1}, {2}",
                venue.Value<string>("name"),
                venue.Value<string>("address_1"),
                venue.Value<string>("city"));
  
            GeoLocation = new Tuple<double, double>(venue.Value<double>("lat"), venue.Value<double>("lon"));
        }
    }
  
    public override string ToString()
    {
        return string.Format("{0}, {1}, {2}, {3}", Group, Title, Date.ToLocalTime(), EndDate.ToLocalTime());
    }
}
  
public static DateTime FromUnixTime(this long unixTime)
{
    var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    return epoch.AddMilliseconds(unixTime);
}

Run the Azure function

Testing or running the Azure function is very easy as you just have to press the Run button. The only thing to mention here is that this button look-alike thing on the upper right side with the text “Run” is not button, it’s a tab. So click it and you’ll see that you can enter input parameters, query parameters and such stuff to your run:

20161109_07_testazurefunction

Get all upcoming meetups of all groups as iCal (ics)

The last step is to subscribe to the meetup calendar in your favorite calendar app. The URL for it is: webcal://codehollow-functions.azurewebsites.net/api/MeetupToICal?apikey=[YOURAPIKEY]

You have to replace [YOURAPIKEY] with the apikey from this page: https://secure.meetup.com/meetup_api/key/

The URL returns an iCal (.ics) file that contains the next 200 upcoming events of all your groups from https://www.meetup.com. It does not include events from the past!

I suggest you to use the webcal protocol for it (I had issues with one client), but you can also access it via https:
https://codehollow-functions.azurewebsites.net/api/MeetupToICal?apikey=[YOURAPIKEY]

Categories:

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *

About
about armin

Armin Reiter
Azure, Blockchain & IT-Security
Vienna, Austria

Reiter ITS Logo

Cryptix Logo

Legal information