bot reply commands

pull/99/head
Karolis Rusenas 2017-08-21 23:44:14 +01:00
parent 9293edaa5c
commit 3ba879996f
1 changed files with 205 additions and 157 deletions

View File

@ -43,6 +43,19 @@ var (
rejectResponseKeyword = "reject"
)
// SlackImplementer - implementes slack HTTP functionality, used to
// send messages with attachments
type SlackImplementer interface {
PostMessage(channel, text string, params slack.PostMessageParameters) (string, string, error)
}
// approvalResponse - used to track approvals once vote begins
type approvalResponse struct {
User string
Status types.ApprovalStatus
Text string
}
// Bot - main slack bot container
type Bot struct {
id string // bot id
@ -55,7 +68,9 @@ type Bot struct {
slackClient *slack.Client
slackRTM *slack.RTM
approvalsCh chan *approvalResponse
slackHTTPClient SlackImplementer
approvalsRespCh chan *approvalResponse
approvalsManager approvals.Manager
@ -69,14 +84,16 @@ func New(name, token string, k8sImplementer kubernetes.Implementer) *Bot {
client := slack.New(token)
bot := &Bot{
slackClient: client,
k8sImplementer: k8sImplementer,
name: name,
approvalsCh: make(chan *approvalResponse), // don't add buffer to make it blocking
slackClient: client,
slackHTTPClient: client,
k8sImplementer: k8sImplementer,
name: name,
approvalsRespCh: make(chan *approvalResponse), // don't add buffer to make it blocking
}
// register slack bot as approval collector
approval.RegisterCollector("slack", bot)
fmt.Println("approval collector registered")
return bot
}
@ -116,8 +133,12 @@ func (b *Bot) Start(ctx context.Context) error {
b.msgPrefix = strings.ToLower("<@" + b.id + ">")
// processing messages coming from slack RTM client
go b.startInternal()
// processing slack approval responses
go b.processApprovalResponses()
return nil
}
@ -177,7 +198,7 @@ func (b *Bot) subscribeForApprovals() error {
case <-b.ctx.Done():
return nil
case a := <-approvalsCh:
err = b.request(a)
err = b.requestApproval(a)
if err != nil {
log.WithFields(log.Fields{
"error": err,
@ -190,9 +211,8 @@ func (b *Bot) subscribeForApprovals() error {
}
// Request - request approval
func (b *Bot) request(req *types.Approval) error {
err := b.postMessage(
func (b *Bot) requestApproval(req *types.Approval) error {
return b.postMessage(
"Approval required",
req.Message,
types.LevelSuccess.Color(),
@ -212,143 +232,184 @@ func (b *Bot) request(req *types.Approval) error {
Value: "0",
Short: true,
},
slack.AttachmentField{
Title: "Delta",
Value: req.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Identifier",
Value: req.Identifier,
Short: true,
},
})
if err != nil {
return err
}
collected := make(map[string]*approvalResponse)
}
func (b *Bot) processApprovalResponses() error {
voteEnds := time.Now().Add(req.Requirements.Timeout)
// start waiting for responses
for {
select {
case resp := <-b.approvalsCh:
case <-b.ctx.Done():
return nil
case resp := <-b.approvalsRespCh:
// if rejected - ending vote
if !resp.Approved {
b.postMessage(
"Change rejected",
req.Message,
types.LevelWarn.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Change rejected",
Value: "Change was manually rejected. Thanks for voting!",
Short: false,
},
slack.AttachmentField{
Title: "Required",
Value: fmt.Sprint(req.Requirements.MinimumApprovals),
Short: true,
},
slack.AttachmentField{
Title: "Current",
Value: fmt.Sprint(len(collected)),
Short: true,
},
})
return false, nil
}
collected[resp.User] = resp
if len(collected) >= req.Requirements.MinimumApprovals {
var voters []string
for k := range collected {
voters = append(voters, k)
switch resp.Status {
case types.ApprovalStatusApproved:
err := b.processApprovedResponse(resp)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("bot.processApprovalResponses: failed to process approval response message")
}
case types.ApprovalStatusRejected:
err := b.processRejectedResponse(resp)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("bot.processApprovalResponses: failed to process approval reject response message")
}
b.postMessage(
"Approval received",
"All approvals received, thanks for voting!",
types.LevelSuccess.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Update approved!",
Value: "All approvals received, thanks for voting!",
Short: false,
},
slack.AttachmentField{
Title: "Required",
Value: fmt.Sprint(req.Requirements.MinimumApprovals),
Short: true,
},
slack.AttachmentField{
Title: "Current",
Value: fmt.Sprint(len(collected)),
Short: true,
},
})
return true, nil
}
// inform about approval and how many votes required
b.postMessage(
"Approve received",
"",
types.LevelInfo.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Approve received",
Value: "All approvals received, thanks for voting!",
Short: false,
},
slack.AttachmentField{
Title: "Required",
Value: fmt.Sprint(req.Requirements.MinimumApprovals),
Short: true,
},
slack.AttachmentField{
Title: "Current",
Value: fmt.Sprint(len(collected)),
Short: true,
},
slack.AttachmentField{
Title: "Vote ends",
Value: voteEnds.Format("2006/01/02 15:04:05"),
Short: true,
},
})
continue
case <-time.After(req.Requirements.Timeout):
// inform about timeout
b.postMessage(
"Vote deadline reached!",
"",
types.LevelFatal.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Vote deadline reached!",
Value: "Deadline reached, skipping update.",
Short: false,
},
slack.AttachmentField{
Title: "Required",
Value: fmt.Sprint(req.Requirements.MinimumApprovals),
Short: true,
},
slack.AttachmentField{
Title: "Current",
Value: fmt.Sprint(len(collected)),
Short: true,
},
slack.AttachmentField{
Title: "Vote ends",
Value: voteEnds.Format("2006/01/02 15:04:05"),
Short: true,
},
})
return false, nil
}
}
}
func (b *Bot) processApprovedResponse(approvalResponse *approvalResponse) error {
trimmed := strings.TrimPrefix(approvalResponse.Text, approvalResponseKeyword)
identifiers := strings.Split(trimmed, " ")
if len(identifiers) == 0 {
return nil
}
for _, identifier := range identifiers {
fmt.Println("approving: ", identifier)
approval, err := b.approvalsManager.Approve(identifier)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: failed to approve")
continue
}
err = b.replyToApproval(approval)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: got error while replying after processing approved approval")
}
}
return nil
}
func (b *Bot) processRejectedResponse(approvalResponse *approvalResponse) error {
trimmed := strings.TrimPrefix(approvalResponse.Text, rejectResponseKeyword)
identifiers := strings.Split(trimmed, " ")
if len(identifiers) == 0 {
return nil
}
for _, identifier := range identifiers {
approval, err := b.approvalsManager.Reject(identifier)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: failed to reject")
continue
}
err = b.replyToApproval(approval)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"identifier": identifier,
}).Error("bot.processApprovedResponse: got error while replying after processing rejected approval")
}
}
return nil
}
func (b *Bot) replyToApproval(approval *types.Approval) error {
switch approval.Status() {
case types.ApprovalStatusPending:
b.postMessage(
"Vote received",
"All approvals received, thanks for voting!",
types.LevelInfo.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Vote received!",
Value: "Waiting for remaining votes to proceed with update.",
Short: false,
},
slack.AttachmentField{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
})
case types.ApprovalStatusRejected:
b.postMessage(
"Change rejected",
"Change was rejected",
types.LevelWarn.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Change rejected",
Value: "Change was rejected. Thanks for voting!",
Short: false,
},
slack.AttachmentField{
Title: "Status",
Value: approval.Status().String(),
Short: true,
},
slack.AttachmentField{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
})
case types.ApprovalStatusApproved:
b.postMessage(
"Approval received",
"All approvals received, thanks for voting!",
types.LevelSuccess.Color(),
[]slack.AttachmentField{
slack.AttachmentField{
Title: "Update approved!",
Value: "All approvals received, thanks for voting!",
Short: false,
},
slack.AttachmentField{
Title: "Delta",
Value: approval.Delta(),
Short: true,
},
slack.AttachmentField{
Title: "Votes",
Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
Short: true,
},
})
}
return nil
}
func (b *Bot) postMessage(title, message, color string, fields []slack.AttachmentField) error {
params := slack.NewPostMessageParameters()
params.Username = b.name
@ -363,7 +424,7 @@ func (b *Bot) postMessage(title, message, color string, fields []slack.Attachmen
},
}
_, _, err := b.slackClient.PostMessage("general", "", params)
_, _, err := b.slackHTTPClient.PostMessage("general", "", params)
if err != nil {
log.WithFields(log.Fields{
"error": err,
@ -372,33 +433,20 @@ func (b *Bot) postMessage(title, message, color string, fields []slack.Attachmen
return err
}
// approvalResponse - used to track approvals once vote begins
type approvalResponse struct {
User string
Approved bool // can be either approved or rejected
}
func (b *Bot) isApproval(event *slack.MessageEvent, eventText string) (resp *approvalResponse, ok bool) {
if strings.ToLower(eventText) == approvalResponseKeyword {
log.WithFields(log.Fields{
"user": event.User,
"username": event.Username,
"approved": true,
}).Info("bot.isApproval: approval received")
if strings.HasPrefix(strings.ToLower(eventText), approvalResponseKeyword) {
return &approvalResponse{
User: event.Username,
Approved: true,
User: event.Username,
Status: types.ApprovalStatusApproved,
Text: eventText,
}, true
}
if strings.ToLower(eventText) == rejectResponseKeyword {
log.WithFields(log.Fields{
"user": event.User,
"username": event.Username,
"approved": false,
}).Info("bot.isApproval: approval received")
if strings.HasPrefix(strings.ToLower(eventText), rejectResponseKeyword) {
return &approvalResponse{
User: event.Username,
Approved: false,
User: event.Username,
Status: types.ApprovalStatusRejected,
Text: eventText,
}, true
}
@ -419,7 +467,7 @@ func (b *Bot) handleMessage(event *slack.MessageEvent) {
approval, ok := b.isApproval(event, eventText)
if ok {
b.approvalsCh <- approval
b.approvalsRespCh <- approval
return
}