1package notifiers 2 3import ( 4 "context" 5 "fmt" 6 "net/url" 7 "strings" 8 9 "github.com/grafana/grafana/pkg/bus" 10 "github.com/grafana/grafana/pkg/infra/log" 11 "github.com/grafana/grafana/pkg/models" 12 "github.com/grafana/grafana/pkg/services/alerting" 13 "github.com/grafana/grafana/pkg/setting" 14) 15 16var ( 17 threemaGwBaseURL = "https://msgapi.threema.ch/%s" 18) 19 20func init() { 21 alerting.RegisterNotifier(&alerting.NotifierPlugin{ 22 Type: "threema", 23 Name: "Threema Gateway", 24 Description: "Sends notifications to Threema using Threema Gateway (Basic IDs)", 25 Heading: "Threema Gateway settings", 26 Info: "Notifications can be configured for any Threema Gateway ID of type \"Basic\". End-to-End IDs are not currently supported." + 27 "The Threema Gateway ID can be set up at https://gateway.threema.ch/.", 28 Factory: NewThreemaNotifier, 29 Options: []alerting.NotifierOption{ 30 { 31 Label: "Gateway ID", 32 Element: alerting.ElementTypeInput, 33 InputType: alerting.InputTypeText, 34 Placeholder: "*3MAGWID", 35 Description: "Your 8 character Threema Gateway Basic ID (starting with a *).", 36 PropertyName: "gateway_id", 37 Required: true, 38 ValidationRule: "\\*[0-9A-Z]{7}", 39 }, 40 { 41 Label: "Recipient ID", 42 Element: alerting.ElementTypeInput, 43 InputType: alerting.InputTypeText, 44 Placeholder: "YOUR3MID", 45 Description: "The 8 character Threema ID that should receive the alerts.", 46 PropertyName: "recipient_id", 47 Required: true, 48 ValidationRule: "[0-9A-Z]{8}", 49 }, 50 { 51 Label: "API Secret", 52 Element: alerting.ElementTypeInput, 53 InputType: alerting.InputTypeText, 54 Description: "Your Threema Gateway API secret.", 55 PropertyName: "api_secret", 56 Required: true, 57 Secure: true, 58 }, 59 }, 60 }) 61} 62 63// ThreemaNotifier is responsible for sending 64// alert notifications to Threema. 65type ThreemaNotifier struct { 66 NotifierBase 67 GatewayID string 68 RecipientID string 69 APISecret string 70 log log.Logger 71} 72 73// NewThreemaNotifier is the constructor for the Threema notifier 74func NewThreemaNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) { 75 if model.Settings == nil { 76 return nil, alerting.ValidationError{Reason: "No Settings Supplied"} 77 } 78 79 gatewayID := model.Settings.Get("gateway_id").MustString() 80 recipientID := model.Settings.Get("recipient_id").MustString() 81 apiSecret := fn(context.Background(), model.SecureSettings, "api_secret", model.Settings.Get("api_secret").MustString(), setting.SecretKey) 82 83 // Validation 84 if gatewayID == "" { 85 return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"} 86 } 87 if !strings.HasPrefix(gatewayID, "*") { 88 return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"} 89 } 90 if len(gatewayID) != 8 { 91 return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"} 92 } 93 if recipientID == "" { 94 return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"} 95 } 96 if len(recipientID) != 8 { 97 return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"} 98 } 99 if apiSecret == "" { 100 return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"} 101 } 102 103 return &ThreemaNotifier{ 104 NotifierBase: NewNotifierBase(model), 105 GatewayID: gatewayID, 106 RecipientID: recipientID, 107 APISecret: apiSecret, 108 log: log.New("alerting.notifier.threema"), 109 }, nil 110} 111 112// Notify send an alert notification to Threema 113func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { 114 notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) 115 notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) 116 117 // Set up basic API request data 118 data := url.Values{} 119 data.Set("from", notifier.GatewayID) 120 data.Set("to", notifier.RecipientID) 121 data.Set("secret", notifier.APISecret) 122 123 // Determine emoji 124 stateEmoji := "" 125 switch evalContext.Rule.State { 126 case models.AlertStateOK: 127 stateEmoji = "\u2705 " // Check Mark Button 128 case models.AlertStateNoData: 129 stateEmoji = "\u2753\uFE0F " // Question Mark 130 case models.AlertStateAlerting: 131 stateEmoji = "\u26A0\uFE0F " // Warning sign 132 default: 133 // Handle other cases? 134 } 135 136 // Build message 137 message := fmt.Sprintf("%s%s\n\n*State:* %s\n*Message:* %s\n", 138 stateEmoji, evalContext.GetNotificationTitle(), 139 evalContext.Rule.Name, evalContext.Rule.Message) 140 ruleURL, err := evalContext.GetRuleURL() 141 if err == nil { 142 message += fmt.Sprintf("*URL:* %s\n", ruleURL) 143 } 144 if notifier.NeedsImage() && evalContext.ImagePublicURL != "" { 145 message += fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicURL) 146 } 147 data.Set("text", message) 148 149 // Prepare and send request 150 url := fmt.Sprintf(threemaGwBaseURL, "send_simple") 151 body := data.Encode() 152 headers := map[string]string{ 153 "Content-Type": "application/x-www-form-urlencoded", 154 } 155 cmd := &models.SendWebhookSync{ 156 Url: url, 157 Body: body, 158 HttpMethod: "POST", 159 HttpHeader: headers, 160 } 161 if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { 162 notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name) 163 return err 164 } 165 166 return nil 167} 168