Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Configuring Teams alerts *(Deprecated)*](#configuring-teams-alerts-deprecated)
- [Configuring Teams Workflow alerts](#configuring-teams-workflow-alerts)
- [Configuring Telegram alerts](#configuring-telegram-alerts)
- [Configuring Threema Gateway alerts](#configuring-threema-gateway-alerts)
- [Configuring Twilio alerts](#configuring-twilio-alerts)
- [Configuring Vonage alerts](#configuring-vonage-alerts)
- [Configuring Webex alerts](#configuring-webex-alerts)
Expand Down Expand Up @@ -861,6 +862,7 @@ endpoints:
| `alerting.teams` | Configuration for alerts of type `teams`. *(Deprecated)* <br />See [Configuring Teams alerts](#configuring-teams-alerts-deprecated). | `{}` |
| `alerting.teams-workflows` | Configuration for alerts of type `teams-workflows`. <br />See [Configuring Teams Workflow alerts](#configuring-teams-workflow-alerts). | `{}` |
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
| `alerting.threema-gateway` | Configuration for alerts of type `threema-gateway`. <br />See [Configuring Threema Gateway alerts](#configuring-threema-gateway-alerts).| `{}` |
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
| `alerting.vonage` | Configuration for alerts of type `vonage`. <br />See [Configuring Vonage alerts](#configuring-vonage-alerts). | `{}` |
| `alerting.webex` | Configuration for alerts of type `webex`. <br />See [Configuring Webex alerts](#configuring-webex-alerts). | `{}` |
Expand Down Expand Up @@ -2241,6 +2243,38 @@ Here's an example of what the notifications look like:
![Telegram notifications](.github/assets/telegram-alerts.png)


#### Configuring Threema Gateway alerts
| Parameter | Description | Default |
|:---------------------------------------------|:------------------------------------------------------------------------------------------------------------|:----------------------------|
| `alerting.threema-gateway` | Configuration for alerts of type `threema-gateway` | `{}` |
| `alerting.threema-gateway.api-base-url` | Threema Gateway API Base URL | `https://msgapi.threema.ch` |
| `alerting.threema-gateway.send-mode` | Mode used to send the alert, currently only `basic` is supported | `"basic"` |
| `alerting.threema-gateway.api-identity` | Personal Threema API Identity | Required `""` |
| `alerting.threema-gateway.recipients` | Recipients in format `[<type>:]<identifier>`, where type is `id`, `phone` or `email`. Type defaults to `id` | Required `""` |
| `alerting.threema-gateway.auth-secret` | Threema Gateway API authentication secret | Required `""` |
| `alerting.threema-gateway.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.threema-gateway.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.threema-gateway.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.threema-gateway.overrides[].*` | See `alerting.threema-gateway.*` parameters | `{}` |

```yaml
alerting:
threema-gateway:
api-identity: "*ABCDEFG"
auth-secret: "secret"
recipients:
- "THREEMA_ID"

endpoints:
- name: website
url: "https://twin.sh/health"
conditions:
- "[STATUS] == 200"
alerts:
- type: threema-gateway
```


#### Configuring Twilio alerts
| Parameter | Description | Default |
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
Expand Down
3 changes: 3 additions & 0 deletions alerting/alert/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ const (
// TypeTelegram is the Type for the telegram alerting provider
TypeTelegram Type = "telegram"

// TypeThreemaGateway is the Type for the threema-gateway alerting provider
TypeThreemaGateway Type = "threema-gateway"

// TypeTwilio is the Type for the twilio alerting provider
TypeTwilio Type = "twilio"

Expand Down
4 changes: 4 additions & 0 deletions alerting/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/teams"
"github.com/TwiN/gatus/v5/alerting/provider/teamsworkflows"
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
"github.com/TwiN/gatus/v5/alerting/provider/threemagateway"
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
"github.com/TwiN/gatus/v5/alerting/provider/vonage"
"github.com/TwiN/gatus/v5/alerting/provider/webex"
Expand Down Expand Up @@ -156,6 +157,9 @@ type Config struct {
// Telegram is the configuration for the telegram alerting provider
Telegram *telegram.AlertProvider `yaml:"telegram,omitempty"`

// ThreemaGateway is the configuration for the threema-gatway alerting provider
ThreemaGateway *threemagateway.AlertProvider `yaml:"threema-gateway,omitempty"`

// Twilio is the configuration for the twilio alerting provider
Twilio *twilio.AlertProvider `yaml:"twilio,omitempty"`

Expand Down
3 changes: 3 additions & 0 deletions alerting/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/teams"
"github.com/TwiN/gatus/v5/alerting/provider/teamsworkflows"
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
"github.com/TwiN/gatus/v5/alerting/provider/threemagateway"
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
"github.com/TwiN/gatus/v5/alerting/provider/webex"
"github.com/TwiN/gatus/v5/alerting/provider/zapier"
Expand Down Expand Up @@ -126,6 +127,7 @@ var (
_ AlertProvider = (*teams.AlertProvider)(nil)
_ AlertProvider = (*teamsworkflows.AlertProvider)(nil)
_ AlertProvider = (*telegram.AlertProvider)(nil)
_ AlertProvider = (*threemagateway.AlertProvider)(nil)
_ AlertProvider = (*twilio.AlertProvider)(nil)
_ AlertProvider = (*webex.AlertProvider)(nil)
_ AlertProvider = (*zapier.AlertProvider)(nil)
Expand Down Expand Up @@ -167,6 +169,7 @@ var (
_ Config[teams.Config] = (*teams.Config)(nil)
_ Config[teamsworkflows.Config] = (*teamsworkflows.Config)(nil)
_ Config[telegram.Config] = (*telegram.Config)(nil)
_ Config[threemagateway.Config] = (*threemagateway.Config)(nil)
_ Config[twilio.Config] = (*twilio.Config)(nil)
_ Config[webex.Config] = (*webex.Config)(nil)
_ Config[zapier.Config] = (*zapier.Config)(nil)
Expand Down
115 changes: 115 additions & 0 deletions alerting/provider/threemagateway/recipient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package threemagateway

import (
"encoding"
"errors"
"fmt"
"strings"
)

const (
defaultRecipientType = RecipientTypeId
)

var (
ErrInvalidRecipientFormat = errors.New("recipient must be in the format '[<type>:]<value>'")
ErrInvalidRecipientType = fmt.Errorf("invalid recipient type, must be one of: %v", joinKeys(validRecipientTypes, ", "))
validRecipientTypes = map[string]RecipientType{
"id": RecipientTypeId,
"phone": RecipientTypePhone,
"email": RecipientTypeEmail,
}

ErrInvalidPhoneNumberFormat = errors.New("invalid phone number: must contain only digits and may start with '+'")
ErrInvalidEmailAddressFormat = errors.New("invalid email address: must contain '@'")
)

type RecipientType int

const (
RecipientTypeInvalid RecipientType = iota
RecipientTypeId
RecipientTypePhone
RecipientTypeEmail
)

func parseRecipientType(s string) RecipientType {
if val, ok := validRecipientTypes[s]; ok {
return val
}
return RecipientTypeInvalid
}

type Recipient struct {
Value string `yaml:"-"`
Type RecipientType `yaml:"-"`
}

var _ encoding.TextUnmarshaler = (*Recipient)(nil)
var _ encoding.TextMarshaler = (*Recipient)(nil)

func (r *Recipient) UnmarshalText(text []byte) error {
parts := strings.Split(string(text), ":")
switch {
case len(parts) > 2:
return ErrInvalidRecipientFormat
case len(parts) == 2:
if r.Type = parseRecipientType(parts[0]); r.Type == RecipientTypeInvalid {
return ErrInvalidRecipientType
}
r.Value = parts[1]
default:
r.Type = defaultRecipientType
r.Value = parts[0]
}
return nil
}

func (r Recipient) MarshalText() ([]byte, error) {
if r.Type == RecipientTypeInvalid {
return []byte("invalid" + ":" + r.Value), nil
}
for key, val := range validRecipientTypes {
if val == r.Type {
return []byte(key + ":" + r.Value), nil
}
}
return nil, ErrInvalidRecipientType
}

func (r *Recipient) Validate() error {
if len(r.Value) == 0 {
return ErrInvalidRecipientFormat
}
switch r.Type {
case RecipientTypeId:
if err := validateThreemaId(r.Value); err != nil {
return err
}
case RecipientTypePhone:
r.Value = strings.TrimPrefix(r.Value, "+")
if !isValidPhoneNumber(r.Value) {
return ErrInvalidPhoneNumberFormat
}
case RecipientTypeEmail:
// Basic validation for email address // TODO#1464: improve email validation
if !strings.Contains(r.Value, "@") {
return ErrInvalidEmailAddressFormat
}
default:
return ErrInvalidRecipientType
}
return nil
}

func isValidPhoneNumber(number string) bool {
if len(number) == 0 {
return false
}
for _, ch := range number {
if ch < '0' || ch > '9' {
return false
}
}
return true
}
134 changes: 134 additions & 0 deletions alerting/provider/threemagateway/recipient_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package threemagateway

import (
"errors"
"strings"
"testing"
)

func TestRecipient_UnmarshalText_And_MarshalText(t *testing.T) {
scenarios := []struct {
name string
input string
expected Recipient
expectError error
}{
{
name: "default recipient type",
input: "individual",
expected: Recipient{Type: defaultRecipientType, Value: "individual"},
expectError: nil,
},
{
name: "empty input",
input: "",
expected: Recipient{Type: defaultRecipientType, Value: ""},
expectError: nil,
},
{
name: "invalid format",
input: "type:value:extra",
expectError: ErrInvalidRecipientFormat,
},
{
name: "invalid recipient type",
input: "unknown:value",
expectError: ErrInvalidRecipientType,
},
{
name: "valid phone recipient",
input: "phone:+1234567890",
expected: Recipient{Type: RecipientTypePhone, Value: "+1234567890"},
expectError: nil,
},
{
name: "valid email recipient",
input: "email:[email protected]",
expected: Recipient{Type: RecipientTypeEmail, Value: "[email protected]"},
expectError: nil,
},
}

for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
var recipient Recipient
err := recipient.UnmarshalText([]byte(scenario.input))

if !errors.Is(err, scenario.expectError) {
t.Fatalf("expected error for scenario '%s': %v, got: %v", scenario.name, scenario.expectError, err)
}

if scenario.expectError == nil && recipient != scenario.expected {
t.Fatalf("expected recipient for scenario '%s': %+v, got: %+v", scenario.name, scenario.expected, recipient)
}

if scenario.expectError == nil {
marshaled, err := recipient.MarshalText()
if err != nil {
t.Fatalf("unexpected error during marshaling for scenario '%s': %v", scenario.name, err)
}
expectedMarshaled := scenario.input
if strings.Contains(scenario.input, ":") == false {
expectedMarshaled = "id:" + scenario.input
}
if string(marshaled) != expectedMarshaled {
t.Fatalf("expected marshaled text for scenario '%s': %s, got: %s", scenario.name, expectedMarshaled, string(marshaled))
}
}
})
}
}

func TestRecipient_Validate(t *testing.T) {
scenarios := []struct {
name string
input Recipient
expectError error
}{
{
name: "empty recipient",
input: Recipient{Type: defaultRecipientType, Value: ""},
expectError: ErrInvalidRecipientFormat,
},
{
name: "valid id recipient",
input: Recipient{Type: RecipientTypeId, Value: "ABCDEFGH"},
expectError: nil,
},
{
name: "valid phone recipient",
input: Recipient{Type: RecipientTypePhone, Value: "+1234567890"},
expectError: nil,
},
{
name: "invalid phone recipient",
input: Recipient{Type: RecipientTypePhone, Value: "123-456-7890"},
expectError: ErrInvalidPhoneNumberFormat,
},
{
name: "valid email recipient",
input: Recipient{Type: RecipientTypeEmail, Value: "[email protected]"},
expectError: nil,
},
{
name: "invalid email recipient",
input: Recipient{Type: RecipientTypeEmail, Value: "mailtest.com"},
expectError: ErrInvalidEmailAddressFormat,
},
{
name: "invalid recipient type",
input: Recipient{Type: RecipientTypeInvalid, Value: "value"},
expectError: ErrInvalidRecipientType,
},
}

for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
err := scenario.input.Validate()

if !errors.Is(err, scenario.expectError) {
t.Fatalf("expected error for scenario '%s': %v, got: %v", scenario.name, scenario.expectError, err)
}
})
}
}
Loading