keel/bot/slack/slack.go

310 lines
7.2 KiB
Go
Raw Permalink Normal View History

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"
2018-03-03 11:32:00 +00:00
log "github.com/sirupsen/logrus"
)
// SlackImplementer - implementes slack HTTP functionality, used to
// send messages with attachments
type SlackImplementer interface {
2019-04-26 22:48:12 +00:00
PostMessage(channelID string, options ...slack.MsgOption) (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
}
func init() {
2017-12-27 13:46:38 +00:00
bot.RegisterBot("slack", &Bot{})
}
2017-12-27 13:46:38 +00:00
func (b *Bot) Configure(approvalsRespCh chan *bot.ApprovalResponse, botMessagesChannel chan *bot.BotMessage) bool {
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
}
token := os.Getenv(constants.EnvSlackToken)
2017-12-27 13:46:38 +00:00
client := slack.New(token)
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-27 13:46:38 +00:00
b.slackClient = client
b.slackHTTPClient = client
b.approvalsRespCh = approvalsRespCh
b.botMessagesChannel = botMessagesChannel
2017-12-27 13:46:38 +00:00
return true
}
2017-12-27 13:46:38 +00:00
log.Info("bot.slack.Configure(): Slack approval bot is not configured")
return false
}
// 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:
2019-06-02 10:13:33 +00:00
// nothing to do
case *slack.MessageEvent:
b.handleMessage(ev)
case *slack.PresenceChangeEvent:
2019-06-02 10:13:33 +00:00
// nothing to do
case *slack.RTMError:
2019-07-07 14:27:47 +00:00
log.Errorf("Error: %s", ev.Error())
case *slack.InvalidAuthEvent:
2019-06-02 10:13:33 +00:00
log.Error("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.IconURL = b.getBotUserIconURL()
2019-04-26 22:48:12 +00:00
attachements := []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),
Ts: json.Number(strconv.Itoa(int(time.Now().Unix()))),
},
}
2019-04-26 22:48:12 +00:00
var mgsOpts []slack.MsgOption
mgsOpts = append(mgsOpts, slack.MsgOptionPostMessageParameters(params))
mgsOpts = append(mgsOpts, slack.MsgOptionAttachments(attachements...))
_, _, err := b.slackHTTPClient.PostMessage(b.approvalsChannel, mgsOpts...)
if err != nil {
log.WithFields(log.Fields{
2019-06-02 10:13:33 +00:00
"error": err,
"approvals_channel": b.approvalsChannel,
}).Error("bot.postMessage: failed to send message")
}
return err
}
2019-04-26 22:48:12 +00:00
// checking if message was received in approvals channel
func (b *Bot) isApprovalsChannel(event *slack.MessageEvent) bool {
2019-04-26 22:48:12 +00:00
channel, err := b.slackClient.GetChannelInfo(event.Channel)
if err != nil {
2019-06-02 10:13:33 +00:00
// looking for private channel
conv, err := b.slackRTM.GetConversationInfo(event.Channel, true)
if err != nil {
log.Errorf("couldn't find amongst private conversations: %s", err)
} else if conv.Name == b.approvalsChannel {
return true
}
2019-04-26 22:48:12 +00:00
log.WithError(err).Errorf("channel with ID %s could not be retrieved", event.Channel)
return false
}
2019-04-26 22:48:12 +00:00
log.Debugf("checking if approvals channel: %s==%s", channel.Name, b.approvalsChannel)
if channel.Name == b.approvalsChannel {
return true
}
2019-04-26 22:48:12 +00:00
log.Debugf("message was received not on approvals channel (%s)", channel.Name)
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,
"msg": event.Text,
"event_subtype": event.SubType,
2018-08-29 22:40:03 +00:00
}).Debug("handleMessage: ignoring message")
return
}
eventText := strings.Trim(strings.ToLower(event.Text), " \n\r")
if !b.isBotMessage(event, eventText) {
return
}
eventText = b.trimBot(eventText)
2019-04-26 22:48:12 +00:00
approval, ok := bot.IsApproval(event.User, eventText)
// only accepting approvals from approvals channel
2019-04-26 22:48:12 +00:00
if ok && b.isApprovalsChannel(event) {
b.approvalsRespCh <- approval
return
} else if ok {
log.WithFields(log.Fields{
"received_on": event.Channel,
"approvals_chan": b.approvalsChannel,
}).Warnf("message was received not in approvals channel: %s", event.Channel)
b.Respond(fmt.Sprintf("please use approvals channel '%s'", b.approvalsChannel), event.Channel)
return
}
2017-12-27 13:46:38 +00:00
b.botMessagesChannel <- &bot.BotMessage{
Message: eventText,
User: event.User,
Channel: event.Channel,
Name: "slack",
}
}
2017-12-27 13:46:38 +00:00
func (b *Bot) Respond(text string, channel string) {
2019-04-27 09:54:58 +00:00
// if message is short, replying directly via slack RTM
if len(text) < 3000 {
b.slackRTM.SendMessage(b.slackRTM.NewOutgoingMessage(formatAsSnippet(text), channel))
return
}
// longer messages are getting uploaded as files
f := slack.FileUploadParameters{
Filename: "keel response",
Content: text,
Filetype: "text",
Channels: []string{channel},
}
_, err := b.slackClient.UploadFile(f)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("Respond: failed to send message")
}
}
func (b *Bot) isBotMessage(event *slack.MessageEvent, eventText string) bool {
prefixes := []string{
b.msgPrefix,
2019-04-27 09:54:58 +00:00
b.name,
// "kel",
}
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 + "```"
}
func (b *Bot) getBotUserIconURL() string {
res, err := b.slackClient.GetUserInfo(b.id)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"bot_id": b.id,
}).Error("bot.postMessage: failed to retrieve bot user icon url")
}
return res.Profile.ImageOriginal
}