Skip to content

Commit

Permalink
Pushover alert support
Browse files Browse the repository at this point in the history
Closes #23
  • Loading branch information
csmith committed Sep 23, 2020
1 parent 1b10165 commit cbacb00
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 1 deletion.
40 changes: 40 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,46 @@ By default, connection attempts will be made over TCP (IPv6 or IPv4 using Fast F
If the `network` parameter is included then connection attempts will be limited to that
network. Valid options are: "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6".

=== pushover

The pushover plugin sends alerts as push notifications via https://pushover.net[Pushover].

==== Alert: pushover.message

[source,goplum]
----
alert pushover.message "example" {
token = "application-token"
key = "user-or-group-key"
devices = ["iphone", "nexus17"]
failing {
priority = 2
sound = "siren"
retry = 30s
expire = 1h
}
recovering {
priority = 1
sound = "bugle"
}
}
----

Sends a push notification via Pushover. The `token` and `key` values are required: `token`
is an application key (you will need to create one for your goplum install via the Pushover
website), and `key` is the user or group key you wish to send the alert to.

Optionally you can limit the alert to a specific device or devices by passing their names
in the `devices` option.

You can configure sounds and priorities for both failing and recovering alerts by using the
appropriate blocks. For emergency alerts (priority 2), you must also specify how often the
alert is retried (minimum: 30s), and after how long it will stop (maximum: 3h).

If the priority is not set, or the blocks are omitted entirely, the alerts are sent as
priority `0`. If sounds are not set then the default sounds configured in the Pushover
app will be used.

=== slack

The slack plugin provides alerts that send messages to Slack channels.
Expand Down
4 changes: 4 additions & 0 deletions cmd/goplumdev/goplumdev.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/csmith/goplum/plugins/debug"
"github.com/csmith/goplum/plugins/http"
"github.com/csmith/goplum/plugins/network"
"github.com/csmith/goplum/plugins/pushover"
"github.com/csmith/goplum/plugins/slack"
"github.com/csmith/goplum/plugins/twilio"
"github.com/kouhin/envflag"
Expand All @@ -23,6 +24,9 @@ var plugins = map[string]goplum.PluginLoader{
"network": func() (goplum.Plugin, error) {
return network.Plugin{}, nil
},
"pushover": func() (goplum.Plugin, error) {
return pushover.Plugin{}, nil
},
"slack": func() (goplum.Plugin, error) {
return slack.Plugin{}, nil
},
Expand Down
7 changes: 6 additions & 1 deletion config/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,14 @@ func (p *Parser) parseBlock() (map[string]interface{}, error) {
}

func (p *Parser) parseAssignment() (interface{}, error) {
if _, err := p.take(tokenAssignment); err != nil {
s, err := p.take(tokenAssignment, tokenBlockStart)
if err != nil {
return nil, err
}
if s.Class == tokenBlockStart {
p.backup()
return p.parseBlock()
}

n, err := p.take(tokenString, tokenDuration, tokenInt, tokenFloat, tokenArrayStart)
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions plugins/pushover/cmd/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"github.com/csmith/goplum"
"github.com/csmith/goplum/plugins/pushover"
)

func Plum() goplum.Plugin {
return pushover.Plugin{}
}
141 changes: 141 additions & 0 deletions plugins/pushover/pushover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package pushover

import (
"bytes"
"encoding/json"
"fmt"
"github.com/csmith/goplum"
"io/ioutil"
"net/http"
"strings"
"time"
)

var client = http.Client{Timeout: 20 * time.Second}

type Plugin struct{}

func (p Plugin) Alert(kind string) goplum.Alert {
switch kind {
case "message":
return MessageAlert{}
default:
return nil
}
}

func (p Plugin) Check(_ string) goplum.Check {
return nil
}

type PushSettings struct {
Priority int
Sound string
Retry time.Duration
Expire time.Duration
}

type MessageAlert struct {
Token string
Key string
Devices []string
Failing PushSettings
Recovering PushSettings
errored bool
}

func (m MessageAlert) Send(details goplum.AlertDetails) error {
if m.errored {
return fmt.Errorf("pushover alert disabled as a non-recoverable API error was previously returned")
}

var settings PushSettings
if details.NewState == goplum.StateFailing {
settings = m.Failing
} else {
settings = m.Recovering
}

data := struct {
Token string `json:"token"`
User string `json:"user"`
Message string `json:"message"`
Device string `json:"device,omitempty"`
Priority int `json:"priority,omitempty"`
Sound string `json:"sound,omitempty"`
Timestamp int64 `json:"timestamp"`
Retry int `json:"retry,omitempty"`
Expire int `json:"expire,omitempty"`
}{
Token: m.Token,
User: m.Key,
Message: details.Text,
Device: strings.Join(m.Devices, ","),
Priority: settings.Priority,
Sound: settings.Sound,
Retry: int(settings.Retry.Seconds()),
Expire: int(settings.Expire.Seconds()),
Timestamp: time.Now().Unix(),
}

payload, err := json.Marshal(data)

req, err := http.NewRequest(http.MethodPost, "https://api.pushover.net/1/messages.json", bytes.NewReader(payload))
if err != nil {
return err
}

req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return err
}

defer res.Body.Close()

if res.StatusCode >= 400 && res.StatusCode < 500 {
m.errored = true
body, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("error response from pushover, disabling alert: HTTP %d (%s)", res.StatusCode, body)
} else if res.StatusCode >= 500 {
return fmt.Errorf("bad response from pushover: HTTP %d", res.StatusCode)
}

return nil
}

func (m MessageAlert) Validate() error {
if len(m.Token) == 0 {
return fmt.Errorf("missing required argument: token")
}
if len(m.Key) == 0 {
return fmt.Errorf("missing required argument: key")
}
if err := m.validateSettings(m.Failing); err != nil {
return fmt.Errorf("failing block invalid: %v", err)
}
if err := m.validateSettings(m.Recovering); err != nil {
return fmt.Errorf("recovering block invalid: %v", err)
}
return nil
}

func (m MessageAlert) validateSettings(settings PushSettings) error {
if settings.Priority < -1 || settings.Priority > 2 {
return fmt.Errorf("priority must be in range -2..+2")
}

if settings.Retry != 0 && settings.Retry < 30 * time.Second {
return fmt.Errorf("retry must be at least 30 seconds")
}

if settings.Expire > 10800 * time.Second {
return fmt.Errorf("expire must be at most 10800 seconds (3 hours)")
}

if settings.Priority == 2 && (settings.Expire == 0 || settings.Retry == 0) {
return fmt.Errorf("expire and retry must be specified for emergency (2) priority")
}

return nil
}

0 comments on commit cbacb00

Please sign in to comment.