I like the revolution that has happened in the instant messaging space. Teams are trading in emails and meetings for an IM. I suppose I am predisposed as I grew up on AOL instant messenger or AIM as the cool kids called it. I have found it to be far more efficient at providing and getting answers to and from my colleagues. I have control (sorta…) over my interruptions. This turns out to be important when I find myself doing work that requires deep concentration.

Some of you might have noticed I said “revolution” earlier, and then immediately started talking about how this has been around… So what is really new here? In one word: bots.

Chat Bots

Bots are great. They change how we interact in our 1990s chat rooms (again chat rooms and IMs are not new). There is no shortage of features and integrations that bots bring. Instead of having to go find the perfect GIF to throw at your team, you can just let a bot show you a few that correspond to a phrase and… boom! You’re the hit of the channel!

Of course, you can do other things that are not as parasitic to productivity. Github issues, PRs, CI pipelines breaking and more. Bots can do it all! I think a healthy org will find that they increasingly use bots to better communicate and manage the asynchronous events. Let bots help with the chaos.

Custom Bots (in Go)

Its more than likely you will find something you want to have a bot do that is pretty unique to you or your team. Does this mean you are out of luck? Well of course not!

I started creating a bot that integrates with Google Chat (the chat service that comes with G Suite). I chose Go as that is my language of choice. Turns out less developers have gone down this path with Go, so it quickly became clear I needed to write some posts with my experience and findings.

First and foremost, I would be doing everyone a disservice if I didn’t point you towards the docs, Go library docs and some samples. Those will get you started right away.

From the best I can tell, when you think about a bot, you need to decide how you want to interact with it.

Synchronously — Question and Answer (easy) Asynchronously — Bots pushing messages on their own (harder)

Synchronous Bots

This is the normal flow for a bot. You say “hi”, and it says “hello” back. In fact this is such a normal flow that Google Chat doesn’t make a bot do anything too special for authentication. This means you can make a bot in a few lines of code that responds to HTTP POST requests.



//

// Licensed under the Apache License, Version 2.0 (the "License");

// you may not use this file except in compliance with the License.

// You may obtain a copy of the License at

//

//

//

// Unless required by applicable law or agreed to in writing,

// software

// distributed under the License is distributed on an "AS IS" BASIS,

// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or

// implied.

// See the License for the specific language governing permissions

// and limitations under the License. // Copyright 2018 Google LLC//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// http://www.apache.org/licenses/LICENSE-2.0 //// Unless required by applicable law or agreed to in writing,// software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or// implied.// See the License for the specific language governing permissions// and limitations under the License. package main import (

"encoding/json"

"fmt"

"log"

"net/http" chat "google.golang.org/api/chat/v1"

) func main() {

f := func(w http.ResponseWriter, r *http.Request) {

if r.Method != http.MethodPost {

w.WriteHeader(http.StatusMethodNotAllowed)

return

} var event chat.DeprecatedEvent

if err := json.NewDecoder(r.Body).Decode(&event); err != nil {

w.WriteHeader(http.StatusBadRequest)

w.Write([]byte(err.Error()))

return

} switch event.Type {

case "ADDED_TO_SPACE":

if event.Space.Type != "ROOM" {

break

} fmt.Fprint(w, `{"text":"thanks for adding me."}`)

case "MESSAGE":

fmt.Fprintf(w, `{"text":"you said %s"}`, event.Message.Text)

}

} log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(f))

}

This is easily deployed to app engine with the app.yaml :

runtime: go111

service: basic-chat-bot

Deploy it via

gcloud app deploy

Once you have it out there, you can toy with it with curl :

Your bot will respond as such:

{"text":"you said Hello!"}

Note: Be sure to replace <PROJECT-ID> with your GCP project ID. You can find that with:

gcloud projects list

Easy enough right? Once you are ready to publish your bot, check out Publishing bots.

Asynchronous Bots

Asynchronous interaction with someone is more complicated. For one, you now have to figure out service accounts. As of writing this post, you can not use the default service account that is available via app engine (bummer…).



//

// Licensed under the Apache License, Version 2.0 (the "License");

// you may not use this file except in compliance with the License.

// You may obtain a copy of the License at

//

//

//

// Unless required by applicable law or agreed to in writing,

// software

// distributed under the License is distributed on an "AS IS" BASIS,

// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or

// implied.

// See the License for the specific language governing permissions

// and limitations under the License. // Copyright 2018 Google LLC//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// http://www.apache.org/licenses/LICENSE-2.0 //// Unless required by applicable law or agreed to in writing,// software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or// implied.// See the License for the specific language governing permissions// and limitations under the License. package main import (

"context"

"encoding/json"

"fmt"

"io/ioutil"

"log"

"net/http"

"os"

"time" "golang.org/x/oauth2"

"golang.org/x/oauth2/google"

chat "google.golang.org/api/chat/v1"

) func main() {

// Setup client to write messages to chat.google.com

client := getOauthClient(os.Getenv("SERVICE_ACCOUNT_PATH"))

service, err := chat.New(client)

if err != nil {

log.Fatalf("failed to create chat service: %s", err)

}

msgService := chat.NewSpacesMessagesService(service) f := func(w http.ResponseWriter, r *http.Request) {

if r.Method != http.MethodPost {

w.WriteHeader(http.StatusMethodNotAllowed)

return

} var event chat.DeprecatedEvent

if err := json.NewDecoder(r.Body).Decode(&event); err != nil {

w.WriteHeader(http.StatusBadRequest)

w.Write([]byte(err.Error()))

return

} if event.Type != "MESSAGE" {

return

} d, err := time.ParseDuration(event.Message.Text)

if err != nil {

fmt.Fprintf(w, `{"text":"Not a time.Duration: %s"}`, err)

return

}

fmt.Fprintf(w, `{"text":"I will message you in %v"}`, d) // Best effort. If the instance goes away, so be it.

time.AfterFunc(d, func() {

msg := &chat.Message{

Text: fmt.Sprintf("message after %v", d),

}

_, err := msgService.Create(event.Space.Name, msg).Do()

if err != nil {

log.Printf("failed to create message: %s", err)

}

})

} log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(f))

}

ctx := context.Background()

data, err := ioutil.ReadFile(serviceAccountKeyPath)

if err != nil {

log.Fatal(err)

}

creds, err := google.CredentialsFromJSON(

ctx,

data,

"

)

if err != nil {

log.Fatal(err)

} func getOauthClient(serviceAccountKeyPath string) *http.Client {ctx := context.Background()data, err := ioutil.ReadFile(serviceAccountKeyPath)if err != nil {log.Fatal(err)creds, err := google.CredentialsFromJSON(ctx,data, https://www.googleapis.com/auth/chat.bot ",if err != nil {log.Fatal(err) return oauth2.NewClient(ctx, creds.TokenSource)

}

This is deployed to app engine with the app.yaml :

runtime: go111

service: basic-async-chat-bot

env_variables:

SERVICE_ACCOUNT_PATH: account.json

Note: This assumes that you downloaded the service account key and placed it next to your code. You should ensure you don’t accidentally check it in (e.g., add it to .gitignore ).

Testing the Asynchronous Bot

Clearly this bot is more complex, including how to test it. Where the synchronous bot is just a RESTful app that you can make all sorts of assertions against, the asynchronous bot will actually reach out to chat.google.com and do stuff. So how do you figure out if it did the right thing?

I asked around and got a few decent answers. Some were more involved than others.

One solution I liked (but am not going to discuss at length here) is configuring another bot that is listening in the same room as the asynchronous bot. When our bot we’re testing sends a message, the second bot can record that result in a database somewhere. We can then read that database and ensure all went well.

I decided that was more involved than my average use case needed. I instead found that the Go library had a variable that I could override where it sent the messages.

Service.BasePath

Therefore if we let this be configurable in our bot, then we can override it with a simple httptest.Server and assert away!

if googleApiURL := os.Getenv("GOOGLE_API_URL"); googleApiURL != ""{

service.BasePath = googleApiURL

}

More to come…

As I create bots, I will try and open source them. Hopefully they are not just good samples, but also useful enough that others will deploy them to their G Suite projects and find them useful. Happy bot building!