Zoom Integration for the .NET Oxford PrizeDraw App


´╗┐Now that .NET Oxford is going virtual for the time being during the COVID-19 lockdown, we'll be using Zoom for next few meetups. The first one is next week where we have Mark Rendle talking about Roslyn!

Three years ago when .NET Oxford started, I wrote a prizedraw app in WPF which pulled attendees from Meetup RSVPs to the event using their Meetup API. The app looks like this, and is displayed on the main projector screen...

prizedraw app

There's just one selected tile (which displays the user's profile picture when selected), and it randomly moves across the different users, then slows down eventually stopping on the winner.

Now that we're using Zoom, I saw there was a Zoom REST API, so thought that this would be easy to add Zoom integration, so that instead of pulling attendees from the Meetup API, it instead pulled users that were in the Zoom meeting.

Unfortunately, after playing around with Zoom API in LINQPad, I soon discovered that the only endpoint that would allow me to get users from an active meeting required you to have a Business tier account or higher! I already have a Pro account (thanks to our sponsors, Corriculo), but that wasn't enough apparently. I assumed the only way around this was to use their webhooks, and after asking about this on their forum, they confirmed this.

Zoom webhooks are really easy to setup - all you have to do is create a Zoom web-hook only app. For my use-case, I just needed the Participant Joined and Participant Left events.

So, being a big fan of Azure, I decided to take advantage of Azure Functions and Azure Table Storage...

diagram

I setup the Zoom webhook to send an HTTP request to the Azure Function when a participant both joined and left a meeting. The Function would then just write that event straight into table storage.

Rather than only storing who is in the meeting, requiring me to remove people when they left - I decided to just store the events themselves with timestamps. Then when the prize-draw app reads this data, I can calculate at any point in time who's in the meeting (Event Sourcing kind of thing). This then gives me more information/stats after the event - eg. how many people leave mid-meetup, etc.

The Azure Function

Because Table Storage is a native output of Azure Functions, this function can be really small and simple...

[FunctionName("ZoomWebhook")]
[return: Table("Attendees")]
public static async Task RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, ILogger log)
{
    var data = JsonConvert.DeserializeObject(
        await new StreamReader(req.Body).ReadToEndAsync());

    return new Attendee([email protected])
    {
        Type = [email protected],
        MeetingUuid = [email protected],
        MeetingId = [email protected],
        MeetingName = [email protected],
        UserName = [email protected]_name,
        UserId = [email protected]_id,
        UserUuid = [email protected],
        JoinTime = [email protected]_time,
        LeaveTime = [email protected]_time,
    };
}

The above-mentioned code can be found on Github here.

The Attendee class inherits from TableEntity (which is part of the Microsoft.Azure.WebJobs.Extensions.Storage library), and specifies the Zoom meeting ID as the partition key. And that's it! Azure Functions does the rest!...

table storage

It's worth noting that the Type field sent in the Zoom webhook request seems to be 0 when joining/leaving breakout rooms. This isn't documented in the Zoom webhook documentation as far as I can see. I ignore these records when parsing the data below.

Also, the UserUuid is null if the user isn't logged into a Zoom account - ie. they've just clicked on an invite link and have joined anonymously. So I use UserId for the prize draw app instead - which is always a value. However, UserId is different for the same user across different meetings. And by different meetings, this also includes joining/leaving breakout rooms; and also joining/leaving the waiting area.

Reading from Table Storage

The next bit is the code in the prizedraw app itself that reads those events from Azure Table Storage. When I run the prize draw app, and enter a Zoom meeting ID, the code to pull all the events from table storage looks like this...

private IEnumerable GetAttendees(string meetingId) =>
    CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("PrizeDraw_AzureStorageConnectionString"))
        .CreateCloudTableClient()
        .GetTableReference("Attendees")
        .ExecuteQuery(new TableQuery()
            .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, meetingId)))
        .Where(x => x.Type != 0) // Type 0 is when user joins/leaves a breakout room
        .OrderBy(x => x.JoinTime ?? x.LeaveTime)
        .ToList();

In a real production application, I'd put more error checking around a lot of these calls, but given it's use-case - I choose to go with super-succinct. I'm happy in this case with it just throwing an exception if something fails. Adding error checking and friendly user-facing messageboxes would make this method an order of magnitude larger!

This code uses the Microsoft.Azure.CosmosDB.Table nuget package. The API is the same as CosmosDB, so shares the library - even though I'm not using Cosmos.

Then the code that calls that above method...

var dic = new Dictionary();

foreach(var attendee in GetAttendees(meetingId))
    if (attendee.JoinTime != null)
        dic[attendee.UserId] = attendee.UserName;
    else
        dic.Remove(attendee.UserId);

Creating a dictionary of users that are currently in the meeting. As mentioned above, because I'm storing events, rather than just who is in the meeting - I can get quite useful information by processing the Azure Table Storage data in different ways. Here though - the prize draw just needs a list of who's in the event at that moment in time.

The above-mentioned code can be found on Github here.

Is this useable by other user-groups?

The prize-draw app is certainly usable for other user-groups, and it now supports both Meetup and Zoom as data-sources for prize-draw participants. Being a WPF app, this is obviously only suitable for organisers using the Windows operating system though.

If you want to use the Zoom integration, you'll also have to deploy the Azure Function to an Azure subscription, and setup your Zoom webhooks to call that Azure Function. Then you'll need to create an environment variable called PrizeDraw_AzureStorageConnectionString with your Azure Storage connection string, on the machine you'll be running the prizedraw app on. Use the Azure Storage account associated with your Azure Function. When creating the Azure Function in the Azure Portal, you'll be prompted to also create a storage account (or link to an existing one).

From an organiser perspective - the UX is a little big clunky, especially when using the Meetup integration (requiring a restart after entering the meetup event id, and also has an additional window with a button saying "Download Attendees from Meetup.com" which could have just been part of the previous window. I think I'll spend a bit of time soon tidying this up a bit - especially if it might be of use to other user-groups. If you run a user-group and would like to use this prize draw app, please do let me know so I get an idea of interest, and whether it's worth me tidying up these chunky bits and bobs.