Skip to content

Commit

Permalink
Add ntfy integration (#154)
Browse files Browse the repository at this point in the history
Adds (unauthenticated) support for ntfy (https://ntfy.sh/). It should also
support alternative ntfy servers (set using the NOTI_NTFY_URL environment
variable) but I don't have an instance to test with. Topic to send
notifications to is set with NOTI_NTFY_TOPIC.
  • Loading branch information
n-rosati authored Jul 26, 2023
1 parent 6421a41 commit ad536d6
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 1 deletion.
5 changes: 5 additions & 0 deletions docs/man/noti.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ when it's done. You can receive messages on your computer or phone.
--twilio
: Trigger a Twilio notification. This requires `twilio.authToken`, `twilio.accountSid`, `twilio.numberFrom` and `twilio.numberTo` to be set.

--ntfy
: Trigger a ntfy notification. This requires `ntfy.topic` be set. Optionally, `ntfy.url` can also be set to use a different Ntfy server.

-w <pid>, \--pwatch <pid>
: Monitor a process by PID and trigger a notification when the pid disappears.

Expand Down Expand Up @@ -95,6 +98,8 @@ when it's done. You can receive messages on your computer or phone.
* `NOTI_KEYBASE_CHANNEL`
* `NOTI_KEYBASE_PUBLIC`
* `NOTI_KEYBASE_EXPLODINGLIFETIME`
* `NOTI_NTFY_TOPIC`
* `NOTI_NTFY_URL`
* `NOTI_PUSHBULLET_ACCESSTOKEN`
* `NOTI_PUSHBULLET_DEVICEIDEN`
* `NOTI_PUSHOVER_APITOKEN`
Expand Down
18 changes: 18 additions & 0 deletions docs/noti.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Noti can send notifications on a number of services.
| Zulip | ✔ | ✔ | ✔ |
| Twilio | ✔ | ✔ | ✔ |
| GChat | ✔ | ✔ | ✔ |
| ntfy | ✔ | ✔ | ✔ |
## Installation
Expand Down Expand Up @@ -122,6 +123,10 @@ curl -L $(curl -s https://api.github.com/repos/variadico/noti/releases/latest |
Trigger a Twilio notification. This requires twilio.authToken, twilio.accountSid,
twilio.numberFrom and twilio.numberTo to be set.

--ntfy
Trigger a ntfy notification. This requires `ntfy.topic` be set. Optionally,
`ntfy.url` can also be set to use a different ntfy server.

-w , --pwatch
Monitor a process by PID and trigger a notification when the pid
disappears.
Expand Down Expand Up @@ -152,6 +157,8 @@ curl -L $(curl -s https://api.github.com/repos/variadico/noti/releases/latest |
* `NOTI_KEYBASE_CHANNEL`
* `NOTI_KEYBASE_PUBLIC`
* `NOTI_KEYBASE_EXPLODINGLIFETIME`
* `NOTI_NTFY_TOPIC`
* `NOTI_NTFY_URL`
* `NOTI_PUSHBULLET_ACCESSTOKEN`
* `NOTI_PUSHBULLET_DEVICEIDEN`
* `NOTI_PUSHOVER_APITOKEN`
Expand Down Expand Up @@ -339,6 +346,14 @@ interruptionLevel
How the notification appears. Show in DnD or not.
Use active, passive, time-sensitive

NTFY

url
ntfy server URL. Defaults to https://ntfy.sh/

topic
Topic ID to send messages to

```
## Examples
Expand Down Expand Up @@ -416,6 +431,9 @@ chanify:
sound: true
priority: 10
interruptionLevel: 'active'
ntfy:
url: https://my.ntfy.url.com
topic: 'xxxxxxxxxxxxxxxx'
```
## Setting up cloud accounts
Expand Down
11 changes: 11 additions & 0 deletions internal/command/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/variadico/noti/service/gchat"
"github.com/variadico/noti/service/keybase"
"github.com/variadico/noti/service/mattermost"
"github.com/variadico/noti/service/ntfy"
"github.com/variadico/noti/service/pushbullet"
"github.com/variadico/noti/service/pushover"
"github.com/variadico/noti/service/pushsafer"
Expand Down Expand Up @@ -179,3 +180,13 @@ func getChanify(title, message string, v *viper.Viper) notification {
Client: httpClient,
}
}

func getNtfy(title, message string, v *viper.Viper) notification {
return &ntfy.Notification{
URL: v.GetString("ntfy.url"),
Topic: v.GetString("ntfy.topic"),
Title: title,
Message: message,
Client: httpClient,
}
}
13 changes: 13 additions & 0 deletions internal/command/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ var baseDefaults = map[string]interface{}{
"chanify.sound": false,
"chanify.priority": 10,
"chanify.interruptionLevel": "active",

"ntfy.url": "https://ntfy.sh/",
"ntfy.topic": "",
}

func setNotiDefaults(v *viper.Viper) {
Expand Down Expand Up @@ -151,6 +154,9 @@ var keyEnvBindings = map[string]string{
"chanify.sound": "NOTI_CHANIFY_SOUND",
"chanify.priority": "NOTI_CHANIFY_PRIORITY",
"chanify.interruptionLevel": "NOTI_CHANIFY_INTERUPTIONLEVEL",

"ntfy.url": "NOTI_NTFY_URL",
"ntfy.topic": "NOTI_NTFY_TOPIC",
}

var keyEnvBindingsDeprecated = map[string]string{
Expand Down Expand Up @@ -275,6 +281,7 @@ func enabledFromSlice(defaults []string) map[string]bool {
"zulip": false,
"twilio": false,
"chanify": false,
"ntfy": false,
}

for _, name := range defaults {
Expand Down Expand Up @@ -305,6 +312,7 @@ func hasServiceFlags(flags *pflag.FlagSet) bool {
"zulip": false,
"twilio": false,
"chanify": false,
"ntfy": false,
}

flags.Visit(func(f *pflag.Flag) {
Expand Down Expand Up @@ -338,6 +346,7 @@ func enabledFromFlags(flags *pflag.FlagSet) map[string]bool {
"zulip": false,
"twilio": false,
"chanify": false,
"ntfy": false,
}

// Visit flags that have been set.
Expand Down Expand Up @@ -446,5 +455,9 @@ func getNotifications(v *viper.Viper, services map[string]struct{}) []notificati
notis = append(notis, getChanify(title, message, v))
}

if _, ok := services["ntfy"]; ok {
notis = append(notis, getNtfy(title, message, v))
}

return notis
}
3 changes: 2 additions & 1 deletion internal/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ func InitFlags(flags *pflag.FlagSet) {
flags.BoolP("telegram", "g", false, "Trigger a Telegram notification")
flags.BoolP("zulip", "z", false, "Trigger a Zulip notification")
flags.Bool("twilio", false, "Trigger a twilio SMS notification")
flags.IntP("pwatch", "w", -1, "Monitor a process by PID and trigger a notification when the pid disappears.")
flags.Bool("ntfy", false, "Trigger a Ntfy notification")

flags.IntP("pwatch", "w", -1, "Monitor a process by PID and trigger a notification when the pid disappears.")
flags.StringP("file", "f", "", "Path to noti.yaml configuration file.")
flags.BoolVar(&vbsEnabled, "verbose", false, "Enable verbose mode.")
flags.BoolP("version", "v", false, "Print noti version and exit.")
Expand Down
76 changes: 76 additions & 0 deletions service/ntfy/ntfy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package ntfy

import (
"bytes"
"encoding/json"
"errors"
"net/http"
)

type apiResponse struct {
// Message identifier
Id string `json:"id"`

// Message date time as Unix time stamp
Time uint64 `json:"time"`

// Unix time stamp for when the message will be deleted
Expires uint64 `json:"expires"`

// Message type
Event string `json:"event"`

// Topic ID
Topic string `json:"topic"`

// Message title
Title string `json:"title"`

// Message body
Message string `json:"message"`
}

type Notification struct {
// Base Ntfy URL
URL string

// Ntfy topic to publish to
Topic string `json:"topic"`

// Message body
Message string `json:"message"`

// Message title
Title string `json:"title"`

Client *http.Client `json:"-"`
}

func (n *Notification) Send() error {
if n.URL == "" {
return errors.New("missing Ntfy url")
}

if n.Topic == "" {
return errors.New("missing topic id")
}

payload, err := json.Marshal(n)
if err != nil {
return err
}

resp, err := n.Client.Post(n.URL, "application/json", bytes.NewReader(payload))
if err != nil {
return err
}

defer resp.Body.Close()

var r apiResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return err
}

return nil
}
58 changes: 58 additions & 0 deletions service/ntfy/ntfy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package ntfy

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestSend(t *testing.T) {
n := Notification{
URL: "https://ntfy.sh/",
Topic: "topic",
Message: "Message body test",
Title: "Test Message",
Client: &http.Client{Timeout: 3 * time.Second},
}

var mockResp apiResponse
var hitServer bool

ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
hitServer = true

if r.Method != "POST" {
t.Error("HTTP method should be POST")
}

var req Notification
json.NewDecoder(r.Body).Decode(&req)

if req.Topic == "" {
t.Error("missing topic")
}

if req.Message == "" {
t.Error("missing message")
}

if req.Title == "" {
t.Error("missing title")
}

json.NewEncoder(rw).Encode(mockResp)
}))
defer ts.Close()

n.URL = ts.URL

if err := n.Send(); err != nil {
t.Error(err)
}

if !hitServer {
t.Error("didn't reach server")
}
}

0 comments on commit ad536d6

Please sign in to comment.