|  | // 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 | 
|  | } |