2017-12-15 16:37:01 +00:00
|
|
|
package slack
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/nlopes/slack"
|
|
|
|
|
|
|
|
"github.com/keel-hq/keel/bot"
|
|
|
|
"github.com/keel-hq/keel/constants"
|
2018-06-19 23:06:38 +00:00
|
|
|
"github.com/keel-hq/keel/version"
|
2017-12-15 16:37:01 +00:00
|
|
|
|
2018-03-03 11:32:00 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2017-12-15 16:37:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// SlackImplementer - implementes slack HTTP functionality, used to
|
|
|
|
// send messages with attachments
|
|
|
|
type SlackImplementer interface {
|
|
|
|
PostMessage(channel, text string, params slack.PostMessageParameters) (string, string, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bot - main slack bot container
|
|
|
|
type Bot struct {
|
|
|
|
id string // bot id
|
|
|
|
name string // bot name
|
|
|
|
|
|
|
|
users map[string]string
|
|
|
|
|
|
|
|
msgPrefix string
|
|
|
|
|
|
|
|
slackClient *slack.Client
|
|
|
|
slackRTM *slack.RTM
|
|
|
|
|
|
|
|
slackHTTPClient SlackImplementer
|
|
|
|
|
|
|
|
approvalsChannel string // slack approvals channel name
|
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
ctx context.Context
|
|
|
|
botMessagesChannel chan *bot.BotMessage
|
|
|
|
approvalsRespCh chan *bot.ApprovalResponse
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2017-12-27 13:46:38 +00:00
|
|
|
bot.RegisterBot("slack", &Bot{})
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
func (b *Bot) Configure(approvalsRespCh chan *bot.ApprovalResponse, botMessagesChannel chan *bot.BotMessage) bool {
|
2017-12-15 16:37:01 +00:00
|
|
|
if os.Getenv(constants.EnvSlackToken) != "" {
|
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
b.name = "keel"
|
2019-02-28 22:05:22 +00:00
|
|
|
if bootName := os.Getenv(constants.EnvSlackBotName); bootName != "" {
|
|
|
|
b.name = bootName
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
token := os.Getenv(constants.EnvSlackToken)
|
2017-12-27 13:46:38 +00:00
|
|
|
client := slack.New(token)
|
2017-12-15 16:37:01 +00:00
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
b.approvalsChannel = "general"
|
2019-02-28 22:05:22 +00:00
|
|
|
if channel := os.Getenv(constants.EnvSlackApprovalsChannel); channel != "" {
|
2019-04-16 21:51:27 +00:00
|
|
|
b.approvalsChannel = strings.TrimPrefix(channel, "#")
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
b.slackClient = client
|
|
|
|
b.slackHTTPClient = client
|
|
|
|
b.approvalsRespCh = approvalsRespCh
|
|
|
|
b.botMessagesChannel = botMessagesChannel
|
2017-12-15 16:37:01 +00:00
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
return true
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
2017-12-27 13:46:38 +00:00
|
|
|
log.Info("bot.slack.Configure(): Slack approval bot is not configured")
|
|
|
|
return false
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Start - start bot
|
|
|
|
func (b *Bot) Start(ctx context.Context) error {
|
|
|
|
// setting root context
|
|
|
|
b.ctx = ctx
|
|
|
|
|
|
|
|
users, err := b.slackClient.GetUsers()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
b.users = map[string]string{}
|
|
|
|
|
|
|
|
for _, user := range users {
|
|
|
|
switch user.Name {
|
|
|
|
case b.name:
|
|
|
|
if user.IsBot {
|
|
|
|
b.id = user.ID
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if b.id == "" {
|
|
|
|
return errors.New("could not find bot in the list of names, check if the bot is called \"" + b.name + "\" ")
|
|
|
|
}
|
|
|
|
|
|
|
|
b.msgPrefix = strings.ToLower("<@" + b.id + ">")
|
|
|
|
|
|
|
|
go b.startInternal()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Bot) startInternal() error {
|
|
|
|
b.slackRTM = b.slackClient.NewRTM()
|
|
|
|
|
|
|
|
go b.slackRTM.ManageConnection()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-b.ctx.Done():
|
|
|
|
return nil
|
|
|
|
|
|
|
|
case msg := <-b.slackRTM.IncomingEvents:
|
|
|
|
switch ev := msg.Data.(type) {
|
|
|
|
case *slack.HelloEvent:
|
|
|
|
// Ignore hello
|
|
|
|
|
|
|
|
case *slack.ConnectedEvent:
|
|
|
|
// fmt.Println("Infos:", ev.Info)
|
|
|
|
// fmt.Println("Connection counter:", ev.ConnectionCount)
|
|
|
|
// Replace #general with your Channel ID
|
|
|
|
// b.slackRTM.SendMessage(b.slackRTM.NewOutgoingMessage("Hello world", "#general"))
|
|
|
|
|
|
|
|
case *slack.MessageEvent:
|
|
|
|
b.handleMessage(ev)
|
|
|
|
case *slack.PresenceChangeEvent:
|
|
|
|
// fmt.Printf("Presence Change: %v\n", ev)
|
|
|
|
|
|
|
|
// case *slack.LatencyReport:
|
|
|
|
// fmt.Printf("Current latency: %v\n", ev.Value)
|
|
|
|
|
|
|
|
case *slack.RTMError:
|
|
|
|
fmt.Printf("Error: %s\n", ev.Error())
|
|
|
|
|
|
|
|
case *slack.InvalidAuthEvent:
|
|
|
|
fmt.Printf("Invalid credentials")
|
|
|
|
return fmt.Errorf("invalid credentials")
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
// Ignore other events..
|
|
|
|
// fmt.Printf("Unexpected: %v\n", msg.Data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Bot) postMessage(title, message, color string, fields []slack.AttachmentField) error {
|
|
|
|
params := slack.NewPostMessageParameters()
|
|
|
|
params.Username = b.name
|
|
|
|
|
|
|
|
params.Attachments = []slack.Attachment{
|
|
|
|
slack.Attachment{
|
|
|
|
Fallback: message,
|
|
|
|
Color: color,
|
|
|
|
Fields: fields,
|
2018-06-19 23:06:38 +00:00
|
|
|
Footer: fmt.Sprintf("https://keel.sh %s", version.GetKeelVersion().Version),
|
2017-12-15 16:37:01 +00:00
|
|
|
Ts: json.Number(strconv.Itoa(int(time.Now().Unix()))),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
_, _, err := b.slackHTTPClient.PostMessage(b.approvalsChannel, "", params)
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"error": err,
|
|
|
|
}).Error("bot.postMessage: failed to send message")
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(k): cache results in a map or get this info on startup. Although
|
|
|
|
// if channel was then recreated (unlikely), we would miss results
|
|
|
|
func (b *Bot) isApprovalsChannel(event *slack.MessageEvent) bool {
|
2018-06-03 11:20:52 +00:00
|
|
|
|
|
|
|
info := b.slackRTM.GetInfo()
|
|
|
|
|
|
|
|
for _, ch := range info.Channels {
|
2017-12-15 16:37:01 +00:00
|
|
|
if ch.ID == event.Channel && ch.Name == b.approvalsChannel {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2018-06-03 11:20:52 +00:00
|
|
|
|
|
|
|
// checking private channels
|
|
|
|
for _, gr := range info.Groups {
|
|
|
|
if gr.ID == event.Channel && gr.Name == b.approvalsChannel {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-15 16:37:01 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Bot) handleMessage(event *slack.MessageEvent) {
|
|
|
|
if event.BotID != "" || event.User == "" || event.SubType == "bot_message" {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"event_bot_ID": event.BotID,
|
|
|
|
"event_user": event.User,
|
2018-06-03 11:20:52 +00:00
|
|
|
"msg": event.Text,
|
2017-12-15 16:37:01 +00:00
|
|
|
"event_subtype": event.SubType,
|
2018-08-29 22:40:03 +00:00
|
|
|
}).Debug("handleMessage: ignoring message")
|
2017-12-15 16:37:01 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
eventText := strings.Trim(strings.ToLower(event.Text), " \n\r")
|
|
|
|
|
|
|
|
if !b.isBotMessage(event, eventText) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
eventText = b.trimBot(eventText)
|
|
|
|
|
|
|
|
// only accepting approvals from approvals channel
|
|
|
|
if b.isApprovalsChannel(event) {
|
2017-12-27 13:46:38 +00:00
|
|
|
approval, ok := bot.IsApproval(event.User, eventText)
|
2017-12-15 16:37:01 +00:00
|
|
|
if ok {
|
|
|
|
b.approvalsRespCh <- approval
|
|
|
|
return
|
|
|
|
}
|
2018-06-03 11:20:52 +00:00
|
|
|
} else {
|
|
|
|
log.Warnf("not approvals channel: %s", event.Channel)
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
b.botMessagesChannel <- &bot.BotMessage{
|
|
|
|
Message: eventText,
|
|
|
|
User: event.User,
|
|
|
|
Channel: event.Channel,
|
|
|
|
Name: "slack",
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
2017-12-27 13:46:38 +00:00
|
|
|
return
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
2017-12-27 13:46:38 +00:00
|
|
|
func (b *Bot) Respond(text string, channel string) {
|
|
|
|
b.slackRTM.SendMessage(b.slackRTM.NewOutgoingMessage(formatAsSnippet(text), channel))
|
2017-12-15 16:37:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Bot) isBotMessage(event *slack.MessageEvent, eventText string) bool {
|
|
|
|
prefixes := []string{
|
|
|
|
b.msgPrefix,
|
|
|
|
"keel",
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range prefixes {
|
|
|
|
if strings.HasPrefix(eventText, p) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Direct message channels always starts with 'D'
|
|
|
|
return strings.HasPrefix(event.Channel, "D")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Bot) trimBot(msg string) string {
|
|
|
|
msg = strings.Replace(msg, strings.ToLower(b.msgPrefix), "", 1)
|
|
|
|
msg = strings.TrimPrefix(msg, b.name)
|
|
|
|
msg = strings.Trim(msg, " :\n")
|
|
|
|
|
|
|
|
return msg
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatAsSnippet(response string) string {
|
|
|
|
return "```" + response + "```"
|
|
|
|
}
|