spoc-bot-vr/slack.go

279 lines
6.6 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"time"
"github.com/breel-render/spoc-bot-vr/model"
)
var (
ErrIrrelevantMessage = errors.New("message isnt relevant to spoc bot vr")
)
type SlackToModel struct {
pipeline Pipeline
}
type Models struct {
Event model.Event
Message model.Message
Thread model.Thread
}
func NewSlackToModelPipeline(ctx context.Context, cfg Config) (Pipeline, error) {
reader, err := NewQueue(ctx, "slack_event", cfg.driver)
if err != nil {
return Pipeline{}, err
}
writer, err := NewQueue(ctx, "new_models", cfg.driver)
if err != nil {
return Pipeline{}, err
}
return Pipeline{
writer: writer,
reader: reader,
process: newSlackToModelProcess(cfg),
}, nil
}
func newSlackToModelProcess(cfg Config) processFunc {
return func(ctx context.Context, slack []byte) ([]byte, error) {
s, err := parseSlack(slack)
if cfg.Debug {
log.Printf("%v: %s => %+v", err, slack, s)
}
if errors.Is(err, ErrIrrelevantMessage) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("failed to deserialize slack %v", err)
}
for pattern, ptr := range map[string]*string{
cfg.AssetPattern: &s.Asset,
cfg.DatacenterPattern: &s.Datacenter,
cfg.EventNamePattern: &s.EventName,
} {
*ptr = withPattern(pattern, *ptr)
}
event := model.Event{}
if s.Event != "" && s.Source != "" && s.TS > 0 && s.EventName != "" {
event = model.NewEvent(s.Event, s.Source, s.TS, s.EventName, s.Asset, s.Datacenter, s.Team, s.Resolved)
}
message := model.Message{}
if s.ID != "" && s.Source != "" && s.TS > 0 && s.Thread != "" {
message = model.NewMessage(s.ID, s.TS, s.Author, s.Plaintext, s.Thread)
}
thread := model.Thread{}
if s.Thread != "" && s.Source != "" && s.TS > 0 && s.Event != "" {
thread = model.NewThread(s.Thread, s.Source, s.TS, s.Channel, s.Event)
}
if cfg.Debug {
log.Printf("parsed slack message into models")
}
return json.Marshal(Models{
Event: event,
Message: message,
Thread: thread,
})
}
}
func withPattern(pattern string, given string) string {
r := regexp.MustCompile(pattern)
parsed := r.FindString(given)
for i, name := range r.SubexpNames() {
if i > 0 && name != "" {
parsed = r.FindStringSubmatch(given)[i]
}
}
return parsed
}
type (
parsedSlackMessage struct {
ID string
TS uint64
Source string
Channel string
Thread string
EventName string
Event string
Plaintext string
Asset string
Resolved bool
Datacenter string
Author string
Team string
}
slackMessage struct {
slackEvent
Type string
TS uint64 `json:"event_time"`
Event slackEvent
MessageTS string `json:"ts"`
}
slackEvent struct {
ID string `json:"event_ts"`
Channel string
// rewrites
Nested *slackEvent `json:"message"`
PreviousMessage *slackEvent `json:"previous_message"`
// human
ParentID string `json:"thread_ts"`
Text string
Blocks []slackBlock
User string
// bot
Bot slackBot `json:"bot_profile"`
Attachments []slackAttachment
}
slackBlock struct {
Elements []slackElement
}
slackElement struct {
Elements []slackElement
RichText string `json:"text"`
}
slackBot struct {
Name string
}
slackAttachment struct {
Color string
Title string
Text string
Fields []slackField
Actions []slackAction
}
slackField struct {
Value string
Title string
}
slackAction struct{}
)
func parseSlack(b []byte) (parsedSlackMessage, error) {
s, err := _parseSlack(b)
if err != nil {
return parsedSlackMessage{}, err
}
/*
if ch != "" {
s.Event.Channel = ch
}
*/
if s.Event.Bot.Name != "" {
if len(s.Event.Attachments) == 0 {
return parsedSlackMessage{}, ErrIrrelevantMessage
} else if !strings.Contains(s.Event.Attachments[0].Title, ": Firing: ") {
return parsedSlackMessage{}, ErrIrrelevantMessage
}
var tagsField string
var teamField string
for _, field := range s.Event.Attachments[0].Fields {
switch field.Title {
case "Tags":
tagsField = field.Value
case "Routed Teams":
teamField = field.Value
}
}
return parsedSlackMessage{
ID: fmt.Sprintf("%s/%v", s.Event.ID, s.TS),
TS: s.TS,
Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ID, ".", "")),
Channel: s.Event.Channel,
Thread: s.Event.ID,
EventName: strings.Split(s.Event.Attachments[0].Title, ": Firing: ")[1],
Event: strings.TrimPrefix(strings.Split(s.Event.Attachments[0].Title, ":")[0], "#"),
Plaintext: s.Event.Attachments[0].Text,
Asset: s.Event.Attachments[0].Text,
Resolved: !strings.HasPrefix(s.Event.Attachments[0].Color, "F"),
Datacenter: tagsField,
Author: s.Event.Bot.Name,
Team: teamField,
}, nil
}
if s.Event.ParentID == "" {
return parsedSlackMessage{}, ErrIrrelevantMessage
}
return parsedSlackMessage{
ID: fmt.Sprintf("%s/%v", s.Event.ParentID, s.TS),
TS: s.TS,
Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ParentID, ".", "")),
Channel: s.Event.Channel,
Thread: s.Event.ParentID,
EventName: "",
Event: "",
Plaintext: s.Event.Text,
Asset: "",
Datacenter: "",
Author: s.Event.User,
}, nil
}
func _parseSlack(b []byte) (slackMessage, error) {
var wrapper ChannelWrapper
if err := json.Unmarshal(b, &wrapper); err == nil && len(wrapper.V) > 0 {
b = wrapper.V
}
var result slackMessage
err := json.Unmarshal(b, &result)
switch result.Type {
case "message":
result.Event = result.slackEvent
result.TS, _ = strconv.ParseUint(strings.Split(result.MessageTS, ".")[0], 10, 64)
result.Event.ID = result.MessageTS
}
if result.Event.Nested != nil && !result.Event.Nested.Empty() {
result.Event.Blocks = result.Event.Nested.Blocks
result.Event.Bot = result.Event.Nested.Bot
result.Event.Attachments = result.Event.Nested.Attachments
result.Event.Nested = nil
}
if result.Event.PreviousMessage != nil {
if result.Event.PreviousMessage.ID != "" {
result.Event.ID = result.Event.PreviousMessage.ID
}
result.Event.PreviousMessage = nil
}
if wrapper.Channel != "" {
result.Event.Channel = wrapper.Channel
}
return result, err
}
func (this slackEvent) Empty() bool {
return fmt.Sprintf("%+v", this) == fmt.Sprintf("%+v", slackEvent{})
}
func (this parsedSlackMessage) Time() time.Time {
return time.Unix(int64(this.TS), 0)
}
type ChannelWrapper struct {
Channel string
V json.RawMessage
}