Creating a Google Chat (Hangout) Site Chat App with Microsoft Signal-R and Blazor

Creating a Google Chat (Hangout) Site Chat App with Microsoft Signal-R and Blazor

Just recently, I had a need to create an interface that website visitors can use to communicate with an agent via Google Chat (Hangout). So I carried out some research, squarely limiting my options to official libraries for Google APIs.

My solution was to create a system that works as follows:

  1. Frontend app: Microsoft Blazor Client-Side.
  2. Backend app: Asp.Net Core WebAPI 3.1.
  3. Service account and Hangout API creation on Google cloud.

Note: Working with Hangout API requires a G-Suite account.

The first step is to create the required services. It is made easy and as simple as pressing a button here: Google Developers Hangout Creation Make sure you're signed in to your G-Suite account.

Pasted File at July 28, 2020 7_12 PM.png

Service Account Creation You can choose an existing project or create a new one right away. The button-click will setup everything for you. Once the creation of services is completed, you can now download the file and save it somewhere. We'll use this file later on. Also, take note of the API key that will only be shown once. Save it in a notepad.

Visual Studio Solution Creation

  1. Create a new Visual Studio project with the ASP.Net Core WebAPI sample with name Endpoint'
  2. Once created, create folder Helpers. Inside this folder, you create a class, ChatHub which Inherit Hub interface into the class and resolve namespaces. The class should look like this:

Annotation 2020-07-30 113317.png We'll come back to this files as we'll still need to place a Hub method inside it.

  1. It's time to install 2 nugget packages. I trust you know how to do that. Install Google.Apis.Auth and Google.Apis.HangoutsChat.v1 Your nugget should look like this after installation.

Annotation 2020-07-30 114128.png

  1. Place the earlier-downloaded JSON file into the backend project root. It should look like this:

Annotation 2020-07-30 114509.png

  1. Create a static class HangService in Helpers folder. This class will store the methods to connect to Hangout API.
    In this class, you add the namespaces using Google.Apis.Auth.OAuth2;

using Google.Apis.HangoutsChat.v1;

using Google.Apis.HangoutsChat.v1.Data;

using Google.Apis.Services;

using System;

using System.Collections.Generic;

using System.IO;

Then you replace the class code with the snippet below:

```public static class HangService { private static HangoutsChatService chatService;

    private static void SetHangService()
    {
        //At least one client secrets (Installed or Web) should be set'

        if (chatService != null)
            return;
        string jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Chat-a4dbca5916f0.json");
        bool file_exist = File.Exists(jsonPath);

        GoogleCredential credential;
        using (var stream = new FileStream(jsonPath, FileMode.Open, FileAccess.Read))
        {
            string[] scopes = new[] { "https://www.googleapis.com/auth/chat.bot" };
            credential = GoogleCredential.FromStream(stream)
                .CreateScoped(scopes);
        }
        chatService = new HangoutsChatService(new BaseClientService.Initializer()
        {
            HttpClientInitializer = credential,
            ApplicationName = "Tweeter",

        });
    }

    public static Message SendMessage(string name, string threadkey, string message, bool isFirst = false)
    {
        SetHangService();
        Message msg = new Message
        {
            Text = message,
        };
        if (isFirst)
        {
            msg.Cards = new List<Card>() {
                new Card() { Header = new CardHeader() { Title = $"Chat from {name}", Subtitle = "Welcome.", } } };
        }
        var req = new SpacesResource.MessagesResource(chatService).Create(msg, "spaces/AABAIU-a3sE");
        if (!string.IsNullOrWhiteSpace(threadkey))
            req.ThreadKey = threadkey;
        return req.Execute();
    }
}

Replace the spaces below with the default space you want the chat to come to in Hangout Chat. 
How will you know the space iD? I'll show you later in this story. For now, leave the value there. 

5.  Now you need to configure Signal-R route and registration. 
Navigate to ```Startup.cs

In the ConfigureServices method, add services.AddSignalR(); after the services.AddControllers(); line. Then in Configure method, locate the app.UseEndpoints extension and add endpoints.MapHub<ChatHub>("/chatHub"); before the endpoints.MapControllers(); Overall, it should look like this:

Annotation 2020-07-30 115910.png

  1. Next is to create a Controller to listen to webhook from Google console. Right-click on Controllers from Visual Studio Solution Explorer and add a 'Controller, => Empty API Controller. Name it HookController. First, you need to bring dependency for Signal-R instance by creating a constructor and setting the Signal-R Instance.
private readonly IHubContext<ChatHub> _hubContext;
        public HookController(IHubContext<ChatHub> hubContext)
        {
            _hubContext = hubContext;
        }

Then change the controller attributes to just these:

[Produces("application/json")]
    [ApiController]

Then add a POST method to receive hook data from Google Console.

 /// <summary>
        /// Receives Chat from Google Hangout
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/v1/hook/receive")]
        public async Task<ActionResult> ReceiveHook([FromBody] HangoutHookModel message)
        {
            try
            {

                //Logger.Log($"Received Hook {message}");
                // var accessToken = Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", ""); ;
                // Logger.Log($"Got a new token: {accessToken}");
                #region Token Validation
                //bool valid = true;
                //if (accessToken != null )
                //{
                //    try
                //    {
                //        var payload = await GoogleJsonWebSignature.ValidateAsync(accessToken, new ValidationSettings() {  });
                //        if (!payload.Audience.Equals("5944214883993")) //your hangout API project key/id
                //            valid = false;
                //        if (!payload.Issuer.Equals("chat@system.gserviceaccount.com") && !payload.Issuer.Equals("https://chat@system.gserviceaccount.com"))
                //            valid = false;
                //        if (payload.ExpirationTimeSeconds == null)
                //            valid = false;
                //        else
                //        {
                //            DateTime now = DateTime.Now.ToUniversalTime();
                //            DateTime expiration = DateTimeOffset.FromUnixTimeSeconds((long)payload.ExpirationTimeSeconds).DateTime;
                //            if (now > expiration)
                //            {
                //                valid = false;
                //            }
                //        }
                //    }
                //    catch (InvalidJwtException e)
                //    {
                //        Logger.Log(e);
                //        valid = false;
                //    }
                //}
                //if (!valid)
                //    return Unauthorized();
                #endregion
                //Logger.Log($"Received Hook {message}");
                Logger.Log($"Received Hook {JsonConvert.SerializeObject(message)}");

                if (!message.type.ToLower().Contains("space"))
                    await _hubContext.Clients.All.SendAsync(message.message.thread.name, message);

                return Ok();
            }
            catch (Exception ex)
            {
                Logger.Log(ex);
                return Ok();
            }
        }

I have commented out the JWT token verification part because it wasn't working and I'll come back to it later on. You can fix it and share your code with me in the comment.

Got error with HangoutHookModel ? here is the model: You can create a new folder Models and add this class to it. Paste the code below into it.

 /// <summary>
    /// Types:REMOVED_FROM_SPACE, ADDED_TO_SPACE, MESSAGE
    /// </summary>

    public class ManualRetentionSettings
    {
        public string state { get; set; }
        public string expiryTimestamp { get; set; }

    }

    public class ManualThread
    {
        public string name { get; set; }
        public ManualRetentionSettings retentionSettings { get; set; }

    }

    public class ManualSpace
    {
        public string name { get; set; }
        public string type { get; set; }
        public bool singleUserBotDm { get; set; }

        //added to space
        public string displayName { get; set; }
        public bool threaded { get; set; }
    }

    public class ManualMessage
    {
        public string name { get; set; }
        public ManualUser sender { get; set; }
        public DateTime createTime { get; set; }
        public string text { get; set; }
        public ManualThread thread { get; set; }
        public ManualSpace space { get; set; }
        public string argumentText { get; set; }

    }

    public class ManualUser
    {
        public string name { get; set; }
        public string displayName { get; set; }
        public string avatarUrl { get; set; }
        public string email { get; set; }
        public string type { get; set; }
        public string domainId { get; set; }

    }

    public class HangoutHookModel
    {
        public string type { get; set; }
        public DateTime eventTime { get; set; }
        public ManualMessage message { get; set; }
        public ManualUser user { get; set; }
        public ManualSpace space { get; set; }
        public string configCompleteRedirectUrl { get; set; }
    }

I deliberately didn't use the Message model that came with the nugget package because another kind of notifications also come with this hook and there is no way to choose what to receive. Here are the types: REMOVED_FROM_SPACE, ADDED_TO_SPACE, MESSAGE

  1. Publish backend to Azure App service or your preferred cloud hosting provider. We need the URL when it's published. If you used Azure, in configuration, enable Socket. Also In custom domains, enable 'https' only. If you're going to use the Javascript Signal-R client, then you should enable CORS accordingly too in Azure. Setting * will allow everything to connect.

  2. How that you have the published URL to the backend, it's time to give it to google. So proceed to Google Developer API and select the project you used. Then select the 'Hangout Chat API' you created earlier using that button. Fill the required information including the link to the endpoint as bot URL. It's very good if you enable the bot to function in both private chats and in rooms. Click on Save... View the configuration again and see if you can make more changes.

Annotation 2020-07-30 120547.png

  1. It's good to implement a log so you can see the hooks. You can setup with loggly.

In summary,

  • The bot is the middleman and it connects the user (without any authentication) to the agent (on G-Suite)
  • The bot creates a new thread in a hardcoded space. It sends messages to this thread and also listens to updates from the thread alone.

I'll continue the story in another post.