Creating a Google chat bot in dotNET (C#): Tutorial for Windows and Visual Studio

Creating a Google chat bot in dotNET (C#): Tutorial for Windows and Visual Studio

·

17 min read

Introduction

I looked around everywhere trying to work out how on earth to accomplish this seemingly straightforward task however, when it comes to dotNet the Google documentation is somewhat lacking. Furthermore, there are very limited examples available online for how to accomplish certain tasks. Never fear reader, I have painstakingly navigated myself through this mire and am now in a position to pass along my new found knowledge (as well as recording it here for my own sanity the next time I need to do it). Without much more nonsense, let us begin.

On with the show

Prerequisites

You must have an understanding of how Pub/Sub works and how to set up a Project on Google Cloud for your bot. You also want to make sure you create a service account key (json format). There are few tutorials from Google (that are actually helpful) on these topics.

Getting started with the API library

Setting up a .NET environment on Google Cloud

How to Pub/Sub

If you're at the point where you have your project, service account, json secrets file all at hand but have no idea where to go from here, start paying attention.

Make sure you've created a Topic under the Pub/Sub section in the Google Cloud Platform console. Again, if you don't know what this is, you need to look at the guides above. As well as the json secrets file, you'll need the following information at your fingertips:

ProjectId - You can find this in the url of the browser or where you select your projects from. It looks like name-123456

SubscriptionId - This can be found under Subscriptions in the Pub/Sub tab on Google Cloud Platform console. It usually looks like name-sub.

Setting up the authentication Environment Variable

Many of the tutorials (and the Google guide) shows you how to set your authentication environment variable from Powershell, which is great - if you just want to run your application from that one instance. Environment variables created in the shell are valid only for that shell instance. Instead what you want to do is set your environment variable directly from System Properties.

Firstly, save your secrets.json file locally where it will be permanently accessed (remember, don't add this file to your git repo). Make a note of its location.

Open System Properties from the start bar (you may need Admin access) and click on Environment Variables under the "Advanced" tab:

System Properties

When presented with the Environment Variables window, select New... under the "System Variables" section (that's the bottom part). Set the Variable name to "GOOGLE_APPLICATION_CREDENTIALS" and the Variable value to the location of the secrets file:

Setting the Environment Variable

If you have Visual Studio running, you'll need to close it and restart as the Environment Variable won't be picked up until the application is opened again.

Now this is done, your application will get the credentials you need automatically: no need for any messy authentication rubbish in the code (mostly).

Getting the right NuGet packages

In Visual Studio (or Rider) you want to make sure you have the correct Google API packages to start developing.

As I'm creating a chat bot, the main packages I need to have are: Google Apis, Hangouts Chat, PubSub and OAuth. When you install these through NuGet, you will find a bunch of other Google api packages get installed automatically - don't worry, these are required dependencies. The installation for those specified packages will look something like this:

NuGet Packages

If you're using other APIs, such as the Calendar or Contacts APIs, you'll need to add those in as well.

Writing the code

What we want to do in this simple tutorial is subscribe to events that are fired when our chatbot is called and subsequently fire back a message to Google Chat. When someone calls your bot, the event is received by Pub/Sub and broadcast to anyone listening. So, let's make that listener.

Listening to our chat bot

We're going to be using a basic .NET C# console app and I'm not going to make it look pretty. When you do it, please follow coding best practises. I just want to help you get your project going, not implement the best design patterns for your use case.

We need to create an async method that will be responsible for listening to messages on the wire. The recommendation from Google is that you run the subscriber for 5 seconds, close it and then fire it up again. Loop that endlessly to achieve your goal. In all honesty, when you want to start processing things from the chatbot, maybe do some DB calls on the side, or access data from another listener, 5 seconds is too short a time. You can set it to whatever you wish, but I don't recommend leaving it for a vast amount of time. Do what fits your use case.

Below is the outline code we need for the async method:

private async Task<int> PullMessagesAsync(string projectId, string subscriptionId, bool acknowledge)
{         
    var subscriptionName = new SubscriptionName(projectId, subscriptionId);
    var subscriber = await SubscriberClient.CreateAsync(subscriptionName);
    // SubscriberClient runs your message handle function on multiple
    // threads to maximize throughput.
    int messageCount = 0;
    Task startTask = subscriber.StartAsync(async (PubsubMessage message, CancellationToken cancel) =>
    {
    var text = System.Text.Encoding.UTF8.GetString(message.Data.ToArray());


    Interlocked.Increment(ref messageCount);
    return await Task.FromResult(acknowledge ? SubscriberClient.Reply.Ack : SubscriberClient.Reply.Nack);
    });
    // Run for 5 seconds.
    await Task.Delay(5000);
    await subscriber.StopAsync(CancellationToken.None);
    // Lets make sure that the start task finished successfully after the call to stop.
    await startTask;
    return messageCount;
}

You can see here that we need to provide a projectId and subscriptionId in order to create the subscription name, which in turn is used by the Pub/Sub API to subscribe to events.

Let's amend our classic "Program.cs" to allow us to call this new method and keep calling it over and over again:

public class Program
{
      private const string ProjectId = "name-123456";
      private const string SubscriptionId = "name-sub";

      public static async Task Main(string[] args)
      {                 
        try
        {
            while (true)
                await PullMessagesAsync(ProjectId, SubscriptionId, true);
        }
         catch (Exception e)
        {
            Console.WriteLine(e);
        }

      }
}

If you put a breakpoint now on the PullMessagesAsync method startTask, you'll see it gets hit whenever you call your chat bot.

The data of the event is in json format, so you'll need Newsonsoft.Json (or whatever you like) to proceed - so get it installed through NuGet.

The first thing I'm going to do is take that 'text' variable I created and assigned the message data to, and convert it into a JSON object like so: var jsonObject = JObjects.Parse(text);

That will let me mess around with it a bit. There are a few things I'm going to need from it to send a message back to the client (like, who am I sending it back to for example). I would recommend parsing the entirety of the json into some classes you define so that the data is easier to read and use when you come to it. For now though, I'll do it the ugly way. If your chat bot is running in a DM the json output will be different than if it's running in a space (or room). Similarly, the message sent when a bot is added or removed is different to when someone calls it after it's already been added. Keep that in mind when you parse the data.

For my example, I'm going to keep it really simple. I'm going to assume my chat bot has been called in a room and it's already been added. The three things I really want to know at this stage are: what kind of event is this (is it a message, added or removed), the space (or room) the bot was called in, and the thread ID of the message.

We can get them like this:

var type = jsonObject["type"];
var space = jsonObject["space"]["name"];
var threadId = jsonObject["message"]["thread"]["name"];

You will undoubtedly notice there are many, many different data objects available to you here. Far too many to get through in this tutorial I'm afraid.

First let's check the Type of event. If the event is a message, we'll get the other two variables. If it's not a message, it's likely going to fail because the space and threadId may not be present.

if (type.equals("MESSAGE")) 
{
 var space = jsonObject["space"]["name"];
 var threadId = jsonObject["message"]["thread"]["name"];
}

If it's not a message, I don't want to do anything.

That's the subscription side of things completed! Now let's move onto the chat side of things.

Talking back to the client

This part is sadly going to need a little bit of annoying authentication nonsense... but I'll make it painless as possible. Let's write a new method to handle sending a message to the client, and I'll pre-fill it with the authentication you need (see, painless!):

private void SendMessage(string space, string message, string thread = null)
    {         
        try
        {
            var jsonPath = "";
            jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "C://PathToAwesomeDirectory//secrets.json");
            using (var stream = new FileStream(jsonPath, FileMode.Open, FileAccess.Read))
            {
                string[] scopes = new[] { "https://www.googleapis.com/auth/chat.bot" };
            }

            var credential = GoogleCredential.FromFile("C://PathToAwesomeDirectory//secrets.json");

            var service = new HangoutsChatService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "MyApplication"
            });

            var pubSubMessage = new Google.Apis.HangoutsChat.v1.Data.Message
            {
                Text = message,
                Thread = new Thread() { Name = thread },
                Sender = new Google.Apis.HangoutsChat.v1.Data.User() { Name = "MyApplication Bot", DisplayName = "MyApplication Bot", Type = "BOT" }
            };

            SpacesResource.MessagesResource.CreateRequest req = new SpacesResource.MessagesResource(service).Create(pubSubMessage, space);
            var result = req.Execute();            
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }

Now that's everything you need to send a message, but let's take a quick look at what's going on here. We pass in the space, the message and the threadId (if we want to reply to a thread - if we don't pass this in, our bot will reply in a new thread). Then, using the credentials we create a Hangouts chat service that we'll use to post a message using the Hangouts Chat API. We create a new message using the API filling it with the message, the threadId and the sender (which in this case is our bot), and then send it off (by creating the message in the relevant space).

All that's left is to call this method when we hit a message, so let's put that code in:

if (type.equals("MESSAGE")) 
{
        var space = jsonObject["space"]["name"];
        var threadId = jsonObject["message"]["thread"]["name"];
        SendMessage(space, "Hi there... thanks for calling me", threadId)
}

And there you have it. Now when you call your bot it will reply as part of the same thread in the same room with a message that reads: Hi there... thanks for calling me

For prettier messages, look at how to make cards. These are all JSON based, although a little tedious to create. Thankfully the classes for these all exist within the Hangouts API already. It's worth doing as the end result is far nicer to look at.

Hopefully this gives you a starting point from which to jump off and create many an amazing bot. The full code (less using statements) can be found below:

namespace MyChatBotApplication
{
    public class Program
    {
        private const string ProjectId = "name-123456";
        private const string SubscriptionId = "name-sub";

        public static async Task Main(string[] args)
        {                 
            try
            {
                while (true)
                    await PullMessagesAsync(ProjectId, SubscriptionId, true);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }         
        }
    }

    private async Task<int> PullMessagesAsync(string projectId, string subscriptionId, bool acknowledge)
    {         
        var subscriptionName = new SubscriptionName(projectId, subscriptionId);
        var subscriber = await SubscriberClient.CreateAsync(subscriptionName);
        // SubscriberClient runs your message handle function on multiple
        // threads to maximize throughput.
        int messageCount = 0;
        Task startTask = subscriber.StartAsync(async (PubsubMessage message, CancellationToken cancel) =>
        {
            var text = System.Text.Encoding.UTF8.GetString(message.Data.ToArray());    
            var jsonObject = JObjects.Parse(text);   
            var type = jsonObject["type"];       

            if (type.equals("MESSAGE")) 
            {
                var space = jsonObject["space"]["name"];
                var threadId = jsonObject["message"]["thread"]["name"];
                SendMessage(space, "Hi there... thanks for calling me", threadId)
            }      

            Interlocked.Increment(ref messageCount);
            return await Task.FromResult(acknowledge ? SubscriberClient.Reply.Ack : SubscriberClient.Reply.Nack);
        });
        // Run for 5 seconds.
        await Task.Delay(5000);
        await subscriber.StopAsync(CancellationToken.None);
        // Lets make sure that the start task finished successfully after the call to stop.
        await startTask;
        return messageCount;
    }

    private void SendMessage(string space, string message, string thread = null)
    {         
        try
        {
            var jsonPath = "";
            jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "C://PathToAwesomeDirectory//secrets.json");
            using (var stream = new FileStream(jsonPath, FileMode.Open, FileAccess.Read))
            {
                string[] scopes = new[] { "https://www.googleapis.com/auth/chat.bot" };
            }

            var credential = GoogleCredential.FromFile("C://PathToAwesomeDirectory//secrets.json");

            var service = new HangoutsChatService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "MyApplication"
            });

            var pubSubMessage = new Google.Apis.HangoutsChat.v1.Data.Message
            {
                Text = message,
                Thread = new Thread() { Name = thread },
                Sender = new Google.Apis.HangoutsChat.v1.Data.User() { Name = "MyApplication Bot", DisplayName = "MyApplication Bot", Type = "BOT" }
            };

            SpacesResource.MessagesResource.CreateRequest req = new SpacesResource.MessagesResource(service).Create(pubSubMessage, space);
            var result = req.Execute();            
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}