Skip to content

Commit

Permalink
feat(mattermost): Add emoji support (#1028)
Browse files Browse the repository at this point in the history
  • Loading branch information
kissken authored May 23, 2024
1 parent 6866623 commit b40375d
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 62 deletions.
55 changes: 55 additions & 0 deletions senders/emoji_provider/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package emoji_provider

import (
"fmt"
"maps"

"github.com/moira-alert/moira"
)

var defaultStateEmoji = map[moira.State]string{
moira.StateOK: ":moira-state-ok:",
moira.StateWARN: ":moira-state-warn:",
moira.StateERROR: ":moira-state-error:",
moira.StateNODATA: ":moira-state-nodata:",
moira.StateEXCEPTION: ":moira-state-exception:",
moira.StateTEST: ":moira-state-test:",
}

// emojiProvider is struct for get emoji by trigger State.
type emojiProvider struct {
defaultValue string
stateEmojiMap map[moira.State]string
}

// StateEmojiGetter is interface for emojiProvider.
type StateEmojiGetter interface {
GetStateEmoji(subjectState moira.State) string
}

// NewEmojiProvider is construct for emojiProvider.
func NewEmojiProvider(defaultValue string, stateEmojiMap map[string]string) (StateEmojiGetter, error) {
emojiMap := maps.Clone(defaultStateEmoji)

for state, emoji := range stateEmojiMap {
converted := moira.State(state)
if _, ok := emojiMap[converted]; !ok {
return nil, fmt.Errorf("undefined Moira's state: %s", state)
}
emojiMap[converted] = emoji
}

return &emojiProvider{
defaultValue: defaultValue,
stateEmojiMap: emojiMap,
}, nil
}

// GetStateEmoji returns corresponding state emoji.
func (em *emojiProvider) GetStateEmoji(subjectState moira.State) string {
if emoji, ok := em.stateEmojiMap[subjectState]; ok {
return emoji
}

return em.defaultValue
}
67 changes: 67 additions & 0 deletions senders/emoji_provider/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package emoji_provider

import (
"testing"

"github.com/moira-alert/moira"

. "github.com/smartystreets/goconvey/convey"
)

func TestNewEmojiProvider(t *testing.T) {
Convey("Successful creation, no errors", t, func() {
em, err := NewEmojiProvider(
"default",
map[string]string{
"OK": "super_ok",
"WARN": "super_warn",
"ERROR": "super_error",
"TEST": "super_test",
"EXCEPTION": "super_exception",
"NODATA": "super_nodata",
},
)
So(err, ShouldBeNil)
expected := &emojiProvider{
defaultValue: "default",
stateEmojiMap: map[moira.State]string{
"OK": "super_ok",
"WARN": "super_warn",
"ERROR": "super_error",
"TEST": "super_test",
"EXCEPTION": "super_exception",
"NODATA": "super_nodata",
},
}
So(em, ShouldResemble, expected)
})

Convey("Unsuccessful creation, has error", t, func() {
em, err := NewEmojiProvider(
"default",
map[string]string{
"OK": "super_ok",
"dfgdf": "super_warn",
"ERROR": "super_error",
"TEST": "super_test",
"EXCEPTION": "super_exception",
"NODATA": "super_nodata",
},
)
So(err.Error(), ShouldResemble, "undefined Moira's state: dfgdf")
So(em, ShouldBeNil)
})
}

func TestEmojiProvider_GetStateEmoji(t *testing.T) {
Convey("Check state emoji", t, func() {
em := &emojiProvider{stateEmojiMap: defaultStateEmoji, defaultValue: "default_value"}
So(em.GetStateEmoji(moira.StateOK), ShouldResemble, ":moira-state-ok:")
So(em.GetStateEmoji(moira.StateWARN), ShouldResemble, ":moira-state-warn:")
So(em.GetStateEmoji(moira.StateERROR), ShouldResemble, ":moira-state-error:")
So(em.GetStateEmoji(moira.StateNODATA), ShouldResemble, ":moira-state-nodata:")
So(em.GetStateEmoji(moira.StateEXCEPTION), ShouldResemble, ":moira-state-exception:")
So(em.GetStateEmoji(moira.StateTEST), ShouldResemble, ":moira-state-test:")
So(em.GetStateEmoji("dfdfdf"), ShouldResemble, "default_value")
})
}
37 changes: 27 additions & 10 deletions senders/mattermost/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,33 @@ import (

"github.com/moira-alert/moira"
"github.com/moira-alert/moira/senders"
"github.com/moira-alert/moira/senders/emoji_provider"

"github.com/mattermost/mattermost/server/public/model"
"github.com/mitchellh/mapstructure"
)

// Structure that represents the Mattermost configuration in the YAML file.
type config struct {
Url string `mapstructure:"url"`
InsecureTLS bool `mapstructure:"insecure_tls"`
APIToken string `mapstructure:"api_token"`
FrontURI string `mapstructure:"front_uri"`
Url string `mapstructure:"url"`
InsecureTLS bool `mapstructure:"insecure_tls"`
APIToken string `mapstructure:"api_token"`
FrontURI string `mapstructure:"front_uri"`
UseEmoji bool `mapstructure:"use_emoji"`
DefaultEmoji string `mapstructure:"default_emoji"`
EmojiMap map[string]string `mapstructure:"emoji_map"`
}

// Sender posts messages to Mattermost chat.
// It implements moira.Sender.
// You must call Init method before SendEvents method.
type Sender struct {
frontURI string
logger moira.Logger
location *time.Location
client Client
frontURI string
useEmoji bool
emojiProvider emoji_provider.StateEmojiGetter
logger moira.Logger
location *time.Location
client Client
}

const (
Expand Down Expand Up @@ -70,7 +76,14 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
if cfg.FrontURI == "" {
return fmt.Errorf("can not read Mattermost front_uri from config")
}

emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap)
if err != nil {
return fmt.Errorf("cannot initialize mattermost sender, err: %w", err)
}
sender.emojiProvider = emojiProvider
sender.frontURI = cfg.FrontURI
sender.useEmoji = cfg.UseEmoji
sender.location = location
sender.logger = logger

Expand Down Expand Up @@ -101,7 +114,6 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.

func (sender *Sender) buildMessage(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) string {
var message strings.Builder

title := sender.buildTitle(events, trigger, throttled)
titleLen := len([]rune(title))

Expand Down Expand Up @@ -137,7 +149,12 @@ func (sender *Sender) buildDescription(trigger moira.TriggerData) string {

func (sender *Sender) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) string {
state := events.GetCurrentState(throttled)
title := fmt.Sprintf("**%s**", state)
title := ""
if sender.useEmoji {
title += sender.emojiProvider.GetStateEmoji(state) + " "
}

title += fmt.Sprintf("**%s**", state)
triggerURI := trigger.GetTriggerURI(sender.frontURI)
if triggerURI != "" {
title += fmt.Sprintf(" [%s](%s)", trigger.Name, triggerURI)
Expand Down
53 changes: 18 additions & 35 deletions senders/slack/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@ import (
slackdown "github.com/moira-alert/blackfriday-slack"
"github.com/moira-alert/moira"
"github.com/moira-alert/moira/senders"
"github.com/moira-alert/moira/senders/emoji_provider"

slack_client "github.com/slack-go/slack"
)

const (
okEmoji = ":moira-state-ok:"
warnEmoji = ":moira-state-warn:"
errorEmoji = ":moira-state-error:"
nodataEmoji = ":moira-state-nodata:"
exceptionEmoji = ":moira-state-exception:"
testEmoji = ":moira-state-test:"

messageMaxCharacters = 4000

// see errors https://api.slack.com/methods/chat.postMessage
Expand All @@ -31,29 +25,23 @@ const (
quotes = "```"
)

var stateEmoji = map[moira.State]string{
moira.StateOK: okEmoji,
moira.StateWARN: warnEmoji,
moira.StateERROR: errorEmoji,
moira.StateNODATA: nodataEmoji,
moira.StateEXCEPTION: exceptionEmoji,
moira.StateTEST: testEmoji,
}

// Structure that represents the Slack configuration in the YAML file.
type config struct {
APIToken string `mapstructure:"api_token"`
UseEmoji bool `mapstructure:"use_emoji"`
FrontURI string `mapstructure:"front_uri"`
APIToken string `mapstructure:"api_token"`
UseEmoji bool `mapstructure:"use_emoji"`
FrontURI string `mapstructure:"front_uri"`
DefaultEmoji string `mapstructure:"default_emoji"`
EmojiMap map[string]string `mapstructure:"emoji_map"`
}

// Sender implements moira sender interface via slack.
type Sender struct {
frontURI string
useEmoji bool
logger moira.Logger
location *time.Location
client *slack_client.Client
frontURI string
useEmoji bool
emojiProvider emoji_provider.StateEmojiGetter
logger moira.Logger
location *time.Location
client *slack_client.Client
}

// Init read yaml config.
Expand All @@ -67,6 +55,11 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
if cfg.APIToken == "" {
return fmt.Errorf("can not read slack api_token from config")
}
emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap)
if err != nil {
return fmt.Errorf("cannot initialize mattermost sender, err: %w", err)
}
sender.emojiProvider = emojiProvider
sender.useEmoji = cfg.UseEmoji
sender.logger = logger
sender.frontURI = cfg.FrontURI
Expand All @@ -81,7 +74,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.
useDirectMessaging := useDirectMessaging(contact.Value)

state := events.GetCurrentState(throttled)
emoji := sender.getStateEmoji(state)
emoji := sender.emojiProvider.GetStateEmoji(state)

channelID, threadTimestamp, err := sender.sendMessage(message, contact.Value, trigger.ID, useDirectMessaging, emoji)
if err != nil {
Expand Down Expand Up @@ -252,16 +245,6 @@ func (sender *Sender) sendPlots(plots [][]byte, channelID, threadTimestamp, trig
return nil
}

// getStateEmoji returns corresponding state emoji.
func (sender *Sender) getStateEmoji(subjectState moira.State) string {
if sender.useEmoji {
if emoji, ok := stateEmoji[subjectState]; ok {
return emoji
}
}
return slack_client.DEFAULT_MESSAGE_ICON_EMOJI
}

// useDirectMessaging returns true if user contact is provided.
func useDirectMessaging(contactValue string) bool {
return len(contactValue) > 0 && contactValue[0:1] == "@"
Expand Down
17 changes: 0 additions & 17 deletions senders/slack/slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,6 @@ func TestUseDirectMessaging(t *testing.T) {
})
}

func TestGetStateEmoji(t *testing.T) {
sender := Sender{}
Convey("Use emoji is false", t, func() {
So(sender.getStateEmoji(moira.StateERROR), ShouldResemble, "")
})

Convey("Use emoji is true", t, func() {
sender := Sender{useEmoji: true}
So(sender.getStateEmoji(moira.StateOK), ShouldResemble, okEmoji)
So(sender.getStateEmoji(moira.StateWARN), ShouldResemble, warnEmoji)
So(sender.getStateEmoji(moira.StateERROR), ShouldResemble, errorEmoji)
So(sender.getStateEmoji(moira.StateNODATA), ShouldResemble, nodataEmoji)
So(sender.getStateEmoji(moira.StateEXCEPTION), ShouldResemble, exceptionEmoji)
So(sender.getStateEmoji(moira.StateTEST), ShouldResemble, testEmoji)
})
}

func TestBuildMessage(t *testing.T) {
location, _ := time.LoadLocation("UTC")
sender := Sender{location: location, frontURI: "http://moira.url"}
Expand Down

0 comments on commit b40375d

Please sign in to comment.