Skip to content

Commit

Permalink
feat: add ntfy service (#308)
Browse files Browse the repository at this point in the history
  • Loading branch information
piksel authored Jan 22, 2023
1 parent 09d3f33 commit a514bf7
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 18 deletions.
7 changes: 7 additions & 0 deletions docs/services/ntfy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Ntfy

Upstream docs: https://docs.ntfy.sh/publish/

## URL Format

--8<-- "docs/services/ntfy/config.md"
1 change: 1 addition & 0 deletions docs/services/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Click on the service for a more thorough explanation. <!-- @formatter:off -->
| [Join](./join.md) | *join://shoutrrr:__`api-key`__@join/?devices=__`device1`__[,__`device2`__, ...][&icon=__`icon`__][&title=__`title`__]* |
| [Mattermost](./mattermost.md) | *mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__]* |
| [Matrix](./matrix.md) | *matrix://__`username`__:__`password`__@__`host`__:__`port`__/[?rooms=__`!roomID1`__[,__`roomAlias2`__]]* |
| [Ntfy](./ntfy.md) | *ntfy://__`username`__:__`password`__@ntfy.sh/__`topic`__* |
| [OpsGenie](./opsgenie.md) | *opsgenie://__`host`__/token?responders=__`responder1`__[,__`responder2`__]* |
| [Pushbullet](./pushbullet.md) | *pushbullet://__`api-token`__[/__`device`__/#__`channel`__/__`email`__]* |
| [Pushover](./pushover.md) | *pushover://shoutrrr:__`apiToken`__@__`userKey`__/?devices=__`device1`__[,__`device2`__, ...]* |
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ nav:
- Join: 'services/join.md'
- Mattermost: 'services/mattermost.md'
- Matrix: 'services/matrix.md'
- Ntfy: 'services/ntfy.md'
- OpsGenie: 'services/opsgenie.md'
- Pushbullet: 'services/pushbullet.md'
- Pushover: 'services/pushover.md'
Expand Down
27 changes: 23 additions & 4 deletions pkg/format/enum_formatter.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
package format

import (
"github.com/containrrr/shoutrrr/pkg/types"
"strings"

"github.com/containrrr/shoutrrr/pkg/types"
)

// EnumInvalid is the constant value that an enum gets assigned when it could not be parsed
const EnumInvalid = -1

// EnumFormatter is the helper methods for enum-like types
type EnumFormatter struct {
names []string
names []string
firstOffset int
aliases map[string]int
}

// Names is the list of the valid Enum string values
func (ef EnumFormatter) Names() []string {
return ef.names
return ef.names[ef.firstOffset:]
}

// Print takes a enum mapped int and returns it's string representation or "Invalid"
Expand All @@ -34,12 +37,28 @@ func (ef EnumFormatter) Parse(s string) int {
return index
}
}
if index, found := ef.aliases[s]; found {
return index
}
return EnumInvalid
}

// CreateEnumFormatter creates a EnumFormatter struct
func CreateEnumFormatter(names []string) types.EnumFormatter {
func CreateEnumFormatter(names []string, optAliases ...map[string]int) types.EnumFormatter {
aliases := map[string]int{}
if len(optAliases) > 0 {
aliases = optAliases[0]
}
firstOffset := 0
for i, name := range names {
if name != "" {
firstOffset = i
break
}
}
return &EnumFormatter{
names,
firstOffset,
aliases,
}
}
14 changes: 10 additions & 4 deletions pkg/format/field_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type FieldInfo struct {
Title bool
Base int
Keys []string
ItemSeparator rune
}

// IsEnum returns whether a EnumFormatter has been assigned to the field and that it is of a suitable type
Expand Down Expand Up @@ -54,10 +55,11 @@ func getStructFieldInfo(structType r.Type, enums map[string]types.EnumFormatter)
}

info := FieldInfo{
Name: fieldDef.Name,
Type: fieldDef.Type,
Required: true,
Title: false,
Name: fieldDef.Name,
Type: fieldDef.Type,
Required: true,
Title: false,
ItemSeparator: ',',
}

if util.IsNumeric(fieldDef.Type.Kind()) {
Expand Down Expand Up @@ -94,6 +96,10 @@ func getStructFieldInfo(structType r.Type, enums map[string]types.EnumFormatter)
info.Keys = strings.Split(tag, ",")
}

if tag, ok := fieldDef.Tag.Lookup("sep"); ok {
info.ItemSeparator = rune(tag[0])
}

if ef, isEnum := enums[fieldDef.Name]; isEnum {
info.EnumFormatter = ef
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/format/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package format
import (
"errors"
"fmt"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/shoutrrr/pkg/util"
r "reflect"
"strconv"
"strings"
"unsafe"

"github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/shoutrrr/pkg/util"
)

// GetServiceConfig returns the inner config of a service
Expand Down Expand Up @@ -139,7 +140,7 @@ func SetConfigField(config r.Value, field FieldInfo, inputValue string) (valid b
return false, errors.New("field format is not supported")
}

values := strings.Split(inputValue, ",")
values := strings.Split(inputValue, string(field.ItemSeparator))

var value r.Value
if elemKind == r.Struct {
Expand Down
8 changes: 5 additions & 3 deletions pkg/format/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package format

import (
"fmt"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/shoutrrr/pkg/util"
r "reflect"
"sort"
"strconv"
"strings"

"github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/shoutrrr/pkg/util"
)

// NodeTokenType is used to represent the type of value that a node has for syntax highlighting
Expand Down Expand Up @@ -268,6 +269,7 @@ func getValueNodeValue(fieldValue r.Value, fieldInfo *FieldInfo) (string, NodeTo
}

func getContainerValueString(fieldValue r.Value, fieldInfo *FieldInfo) string {
itemSep := fieldInfo.ItemSeparator
sliceLen := fieldValue.Len()
var mapKeys []r.Value
if fieldInfo.Type.Kind() == r.Map {
Expand All @@ -282,7 +284,7 @@ func getContainerValueString(fieldValue r.Value, fieldInfo *FieldInfo) string {
for i := 0; i < sliceLen; i++ {
var itemValue r.Value
if i > 0 {
sb.WriteRune(',')
sb.WriteRune(itemSep)
}

if mapKeys != nil {
Expand Down
2 changes: 2 additions & 0 deletions pkg/router/servicemap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/containrrr/shoutrrr/pkg/services/logger"
"github.com/containrrr/shoutrrr/pkg/services/matrix"
"github.com/containrrr/shoutrrr/pkg/services/mattermost"
"github.com/containrrr/shoutrrr/pkg/services/ntfy"
"github.com/containrrr/shoutrrr/pkg/services/opsgenie"
"github.com/containrrr/shoutrrr/pkg/services/pushbullet"
"github.com/containrrr/shoutrrr/pkg/services/pushover"
Expand All @@ -35,6 +36,7 @@ var serviceMap = map[string]func() t.Service{
"logger": func() t.Service { return &logger.Service{} },
"matrix": func() t.Service { return &matrix.Service{} },
"mattermost": func() t.Service { return &mattermost.Service{} },
"ntfy": func() t.Service { return &ntfy.Service{} },
"opsgenie": func() t.Service { return &opsgenie.Service{} },
"pushbullet": func() t.Service { return &pushbullet.Service{} },
"pushover": func() t.Service { return &pushover.Service{} },
Expand Down
2 changes: 1 addition & 1 deletion pkg/services/bark/bark_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/containrrr/shoutrrr/pkg/types"
)

// Config for use within the telegram plugin
// Config for use within the bark service
type Config struct {
standard.EnumlessConfig
Title string `key:"title" default:"" desc:"Notification title, optionally set by the sender"`
Expand Down
93 changes: 93 additions & 0 deletions pkg/services/ntfy/ntfy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Package ntfy implements Ntfy as a shoutrrr service
package ntfy

import (
"fmt"
"net/http"
"net/url"
"strings"

"github.com/containrrr/shoutrrr/internal/meta"
"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/util/jsonclient"

"github.com/containrrr/shoutrrr/pkg/services/standard"
"github.com/containrrr/shoutrrr/pkg/types"
)

// Service sends notifications Ntfy
type Service struct {
standard.Standard
config *Config
pkr format.PropKeyResolver
}

// Send a notification message to Ntfy
func (service *Service) Send(message string, params *types.Params) error {
config := service.config

if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
return err
}

if err := service.sendAPI(config, message); err != nil {
return fmt.Errorf("failed to send ntfy notification: %w", err)
}

return nil
}

// Initialize loads ServiceConfig from configURL and sets logger for this Service
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
service.Logger.SetLogger(logger)
service.config = &Config{}
service.pkr = format.NewPropKeyResolver(service.config)

_ = service.pkr.SetDefaultProps(service.config)

return service.config.setURL(&service.pkr, configURL)

}

func (service *Service) sendAPI(config *Config, message string) error {
response := apiResponse{}
request := message
jsonClient := jsonclient.NewClient()

headers := jsonClient.Headers()
headers.Del("Content-Type")
headers.Set("User-Agent", "shoutrrr/"+meta.Version)
addHeaderIfNotEmpty(&headers, "Title", config.Title)
addHeaderIfNotEmpty(&headers, "Priority", config.Priority.String())
addHeaderIfNotEmpty(&headers, "Tags", strings.Join(config.Tags, ","))
addHeaderIfNotEmpty(&headers, "Delay", config.Delay)
addHeaderIfNotEmpty(&headers, "Actions", strings.Join(config.Actions, ";"))
addHeaderIfNotEmpty(&headers, "Click", config.Click)
addHeaderIfNotEmpty(&headers, "Attach", config.Attach)
addHeaderIfNotEmpty(&headers, "X-Icon", config.Icon)
addHeaderIfNotEmpty(&headers, "Filename", config.Filename)
addHeaderIfNotEmpty(&headers, "Email", config.Email)

if !config.Cache {
headers.Add("Cache", "no")
}
if !config.Firebase {
headers.Add("Firebase", "no")
}

if err := jsonClient.Post(config.GetAPIURL(), request, &response); err != nil {
if jsonClient.ErrorResponse(err, &response) {
// apiResponse implements Error
return &response
}
return err
}

return nil
}

func addHeaderIfNotEmpty(headers *http.Header, key string, value string) {
if value != "" {
headers.Add(key, value)
}
}
Loading

0 comments on commit a514bf7

Please sign in to comment.