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")
+ })
+
+ })
+ })
+}