blob: 0bedb7025346376298991b383463d03f8a8aa2ec [file] [log] [blame]
// chatbot is a package for creating chatbots that interact via webhooks.
package chatbot
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/metadata"
"go.skia.org/infra/go/sklog"
)
const (
BOT_WEBHOOK_METADATA_KEY = "bot_webhooks"
)
var (
client *http.Client
botName string
)
type Sender struct {
DisplayName string `json:"displayName"`
}
type Message struct {
Name string `json:"name"`
Text string `json:"text"`
Sender Sender `json:"sender"`
}
func Init(name string) {
client = httputils.NewTimeoutClient()
botName = name
}
// Send the 'body' as a message to the given chat 'room' name.
func Send(body, room, thread string) error {
return SendUsingConfig(body, room, thread, func() string {
return metadata.ProjectGetWithDefault(BOT_WEBHOOK_METADATA_KEY, "")
})
}
// ConfigReader is a func that returns the config for SendUsingConfig.
type ConfigReader func() string
// SendUsingConfig is just like Send(), but the config retrieved is
// provided by the configReader.
func SendUsingConfig(body, room, thread string, configReader ConfigReader) error {
// First look up the chat room webhook address as stored in the config. The
// list of supported webhooks is a multiline string of the form:
//
// botname_1 webhook_url_1 \n
// botname_2 webhook_url_2 \n
// botname_3 webhook_url_3 \n
//
// Note that we load the config every time through this func, since loading
// config is very fast, and we expect the message rate to be very low. This
// ensures we always have a fresh set of bots.
if configReader == nil {
return errors.New("No configReader provided; can't send messages.")
}
botWebhooks := configReader()
if botWebhooks == "" {
return fmt.Errorf("Got empty config.")
}
lines := strings.Split(botWebhooks, "\n")
u := ""
for _, line := range lines {
parts := strings.Split(line, " ")
if len(parts) == 2 && parts[0] == room {
u = parts[1]
break
}
}
if u == "" {
return fmt.Errorf("Unknown room name: %q", room)
}
if thread != "" {
parsedUrl, err := url.Parse(u)
if err != nil {
return err
}
q := parsedUrl.Query()
q.Set("thread_key", base64.StdEncoding.EncodeToString([]byte(thread)))
parsedUrl.RawQuery = q.Encode()
u = parsedUrl.String()
}
sklog.Infof("Sending to: %q", u)
body = strings.TrimSpace(body)
if body == "" {
body = "*no message*"
}
// We've found the room, so compose the message.
msg := Message{
Text: body,
Sender: Sender{
DisplayName: botName,
},
}
b, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("Failed to encode message: %s", err)
}
buf := bytes.NewBuffer(b)
// Now send the message to the webhook.
resp, err := client.Post(u, "application/json", buf)
if err != nil {
return fmt.Errorf("Failed to send encoded message: %s", err)
}
if resp.StatusCode != 200 {
return fmt.Errorf("Wrong status code sending message: %d %s", resp.StatusCode, resp.Status)
}
return nil
}