diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index e60a967f3fa..c23a53009a9 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -49,6 +49,7 @@ var ( M_Alerting_Notification_Sent_Victorops Counter M_Alerting_Notification_Sent_OpsGenie Counter M_Alerting_Notification_Sent_Telegram Counter + M_Alerting_Notification_Sent_Threema Counter M_Alerting_Notification_Sent_Sensu Counter M_Alerting_Notification_Sent_Pushover Counter M_Aws_CloudWatch_GetMetricStatistics Counter @@ -119,6 +120,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram") + M_Alerting_Notification_Sent_Threema = RegCounter("alerting.notifications_sent", "type", "threema") M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu") M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE") M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover") diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go new file mode 100644 index 00000000000..a710d686d28 --- /dev/null +++ b/pkg/services/alerting/notifiers/threema.go @@ -0,0 +1,159 @@ +package notifiers + +import ( + "fmt" + "net/url" + "strings" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +var ( + threemaGwBaseURL = "https://msgapi.threema.ch/%s" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "threema", + Name: "Threema Gateway", + Description: "Sends notifications to Threema using the Threema Gateway", + Factory: NewThreemaNotifier, + OptionsTemplate: ` +

Threema Gateway settings

+

+ Notifications can be configured for any Threema Gateway ID of type + "Basic". End-to-End IDs are not currently supported. +

+

+ The Threema Gateway ID can be set up at + https://gateway.threema.ch/. +

+
+ Gateway ID + + + + Your 8 character Threema Gateway ID (starting with a *) + +
+
+ Recipient ID + + + + The 8 character Threema ID that should receive the alerts + +
+
+ API Secret + + + + Your Threema Gateway API secret + +
+ `, + }) + +} + +type ThreemaNotifier struct { + NotifierBase + GatewayID string + RecipientID string + APISecret string + log log.Logger +} + +func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + if model.Settings == nil { + return nil, alerting.ValidationError{Reason: "No Settings Supplied"} + } + + gatewayID := model.Settings.Get("gateway_id").MustString() + recipientID := model.Settings.Get("recipient_id").MustString() + apiSecret := model.Settings.Get("api_secret").MustString() + + // Validation + if gatewayID == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"} + } + if !strings.HasPrefix(gatewayID, "*") { + return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"} + } + if len(gatewayID) != 8 { + return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"} + } + if recipientID == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"} + } + if len(recipientID) != 8 { + return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"} + } + if apiSecret == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"} + } + + return &ThreemaNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + GatewayID: gatewayID, + RecipientID: recipientID, + APISecret: apiSecret, + log: log.New("alerting.notifier.threema"), + }, nil +} + +func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { + notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) + notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) + metrics.M_Alerting_Notification_Sent_Threema.Inc(1) + + // Set up basic API request data + data := url.Values{} + data.Set("from", notifier.GatewayID) + data.Set("to", notifier.RecipientID) + data.Set("secret", notifier.APISecret) + + // Build message + message := fmt.Sprintf("%s\n\n*State:* %s\n*Message:* %s\n", + evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) + ruleURL, err := evalContext.GetRuleUrl() + if err == nil { + message = message + fmt.Sprintf("*URL:* %s\n", ruleURL) + } + if evalContext.ImagePublicUrl != "" { + message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicUrl) + } + data.Set("text", message) + + // Prepare and send request + url := fmt.Sprintf(threemaGwBaseURL, "send_simple") + body := data.Encode() + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + cmd := &m.SendWebhookSync{ + Url: url, + Body: body, + HttpMethod: "POST", + HttpHeader: headers, + } + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/threema_test.go b/pkg/services/alerting/notifiers/threema_test.go new file mode 100644 index 00000000000..3f23730a249 --- /dev/null +++ b/pkg/services/alerting/notifiers/threema_test.go @@ -0,0 +1,119 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestThreemaNotifier(t *testing.T) { + Convey("Threema notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + _, err := NewThreemaNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("valid settings should be parsed successfully", func() { + json := ` + { + "gateway_id": "*3MAGWID", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(err, ShouldBeNil) + threemaNotifier := not.(*ThreemaNotifier) + + So(err, ShouldBeNil) + So(threemaNotifier.Name, ShouldEqual, "threema_testing") + So(threemaNotifier.Type, ShouldEqual, "threema") + So(threemaNotifier.GatewayID, ShouldEqual, "*3MAGWID") + So(threemaNotifier.RecipientID, ShouldEqual, "ECHOECHO") + So(threemaNotifier.APISecret, ShouldEqual, "1234") + }) + + Convey("invalid Threema Gateway IDs should be rejected (prefix)", func() { + json := ` + { + "gateway_id": "ECHOECHO", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must start with a *") + }) + + Convey("invalid Threema Gateway IDs should be rejected (length)", func() { + json := ` + { + "gateway_id": "*ECHOECHO", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must be 8 characters long") + }) + + Convey("invalid Threema Recipient IDs should be rejected (length)", func() { + json := ` + { + "gateway_id": "*3MAGWID", + "recipient_id": "ECHOECH", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long") + }) + + }) + }) +}