keel/bot/slack/slack.go

272 lines
6.1 KiB
Go

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"
"github.com/keel-hq/keel/version"
log "github.com/sirupsen/logrus"
)
// 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
ctx context.Context
botMessagesChannel chan *bot.BotMessage
approvalsRespCh chan *bot.ApprovalResponse
}
func init() {
bot.RegisterBot("slack", &Bot{})
}
func (b *Bot) Configure(approvalsRespCh chan *bot.ApprovalResponse, botMessagesChannel chan *bot.BotMessage) bool {
if os.Getenv(constants.EnvSlackToken) != "" {
b.name = "keel"
if bootName := os.Getenv(constants.EnvSlackBotName); bootName != "" {
b.name = bootName
}
token := os.Getenv(constants.EnvSlackToken)
client := slack.New(token)
b.approvalsChannel = "general"
if channel := os.Getenv(constants.EnvSlackApprovalsChannel); channel != "" {
b.approvalsChannel = strings.TrimPrefix(channel, "#")
}
b.slackClient = client
b.slackHTTPClient = client
b.approvalsRespCh = approvalsRespCh
b.botMessagesChannel = botMessagesChannel
return true
}
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:
// 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,
Footer: fmt.Sprintf("https://keel.sh %s", version.GetKeelVersion().Version),
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 {
info := b.slackRTM.GetInfo()
for _, ch := range info.Channels {
if ch.ID == event.Channel && ch.Name == b.approvalsChannel {
return true
}
}
// checking private channels
for _, gr := range info.Groups {
if gr.ID == event.Channel && gr.Name == b.approvalsChannel {
return true
}
}
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,
}).Debug("handleMessage: ignoring message")
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) {
approval, ok := bot.IsApproval(event.User, eventText)
if ok {
b.approvalsRespCh <- approval
return
}
} else {
log.Warnf("not approvals channel: %s", event.Channel)
}
b.botMessagesChannel <- &bot.BotMessage{
Message: eventText,
User: event.User,
Channel: event.Channel,
Name: "slack",
}
return
}
func (b *Bot) Respond(text string, channel string) {
b.slackRTM.SendMessage(b.slackRTM.NewOutgoingMessage(formatAsSnippet(text), channel))
}
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 + "```"
}