diff --git a/docs/man/noti.1.md b/docs/man/noti.1.md index 62c2c02..3aaaea6 100644 --- a/docs/man/noti.1.md +++ b/docs/man/noti.1.md @@ -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 , \--pwatch : Monitor a process by PID and trigger a notification when the pid disappears. @@ -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` diff --git a/docs/noti.md b/docs/noti.md index cdfcab8..09b26f9 100644 --- a/docs/noti.md +++ b/docs/noti.md @@ -36,6 +36,7 @@ Noti can send notifications on a number of services. | Zulip | ✔ | ✔ | ✔ | | Twilio | ✔ | ✔ | ✔ | | GChat | ✔ | ✔ | ✔ | +| ntfy | ✔ | ✔ | ✔ | ## Installation @@ -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. @@ -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` @@ -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 @@ -416,6 +431,9 @@ chanify: sound: true priority: 10 interruptionLevel: 'active' +ntfy: + url: https://my.ntfy.url.com + topic: 'xxxxxxxxxxxxxxxx' ``` ## Setting up cloud accounts diff --git a/internal/command/cloud.go b/internal/command/cloud.go index ea514b5..14ac02d 100644 --- a/internal/command/cloud.go +++ b/internal/command/cloud.go @@ -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" @@ -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, + } +} diff --git a/internal/command/config.go b/internal/command/config.go index ac78809..bfeace3 100644 --- a/internal/command/config.go +++ b/internal/command/config.go @@ -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) { @@ -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{ @@ -275,6 +281,7 @@ func enabledFromSlice(defaults []string) map[string]bool { "zulip": false, "twilio": false, "chanify": false, + "ntfy": false, } for _, name := range defaults { @@ -305,6 +312,7 @@ func hasServiceFlags(flags *pflag.FlagSet) bool { "zulip": false, "twilio": false, "chanify": false, + "ntfy": false, } flags.Visit(func(f *pflag.Flag) { @@ -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. @@ -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 } diff --git a/internal/command/root.go b/internal/command/root.go index 3c4fc07..784275a 100644 --- a/internal/command/root.go +++ b/internal/command/root.go @@ -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.") diff --git a/service/ntfy/ntfy.go b/service/ntfy/ntfy.go new file mode 100644 index 0000000..76f451d --- /dev/null +++ b/service/ntfy/ntfy.go @@ -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 +} diff --git a/service/ntfy/ntfy_test.go b/service/ntfy/ntfy_test.go new file mode 100644 index 0000000..28511b1 --- /dev/null +++ b/service/ntfy/ntfy_test.go @@ -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") + } +}