package main import ( "context" "encoding/json" "errors" "fmt" "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_message", 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 err != nil { return nil, fmt.Errorf("failed to deserialize slack %w: %s", err, slack) } for pattern, ptr := range map[string]*string{ cfg.AssetPattern: &s.Asset, cfg.DatacenterPattern: &s.Datacenter, cfg.EventNamePattern: &s.EventName, } { r := regexp.MustCompile(pattern) parsed := r.FindString(*ptr) for i, name := range r.SubexpNames() { if i > 0 && name != "" { parsed = r.FindStringSubmatch(*ptr)[i] } } *ptr = parsed } 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, "TODO", s.Resolved) } message := model.Message{} if s.ID != "" && s.Source != "" && s.TS > 0 && s.Thread != "" { message = model.NewMessage(s.ID, s.Source, s.TS, "TODO", 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) } return json.Marshal(Models{ Event: event, Message: message, Thread: thread, }) } } 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 } 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 // 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 for _, field := range s.Event.Attachments[0].Fields { if field.Title == "Tags" { tagsField = 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, }, 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: "", }, nil } func _parseSlack(b []byte) (slackMessage, error) { 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 } 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) }