279 lines
6.6 KiB
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
|
|
}
|