Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
3b2c48b5e4
commit
c0c4addb85
285 changed files with 25880 additions and 0 deletions
118
pkg/services/pushbullet/pushbullet.go
Normal file
118
pkg/services/pushbullet/pushbullet.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package pushbullet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
|
||||
)
|
||||
|
||||
// Constants.
|
||||
const (
|
||||
pushesEndpoint = "https://api.pushbullet.com/v2/pushes"
|
||||
)
|
||||
|
||||
// Static errors for push validation.
|
||||
var (
|
||||
ErrUnexpectedResponseType = errors.New("unexpected response type, expected note")
|
||||
ErrResponseBodyMismatch = errors.New("response body mismatch")
|
||||
ErrResponseTitleMismatch = errors.New("response title mismatch")
|
||||
ErrPushNotActive = errors.New("push notification is not active")
|
||||
)
|
||||
|
||||
// Service providing Pushbullet as a notification service.
|
||||
type Service struct {
|
||||
standard.Standard
|
||||
client jsonclient.Client
|
||||
Config *Config
|
||||
pkr format.PropKeyResolver
|
||||
}
|
||||
|
||||
// Initialize loads ServiceConfig from configURL and sets logger for this Service.
|
||||
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
|
||||
service.SetLogger(logger)
|
||||
|
||||
service.Config = &Config{
|
||||
Title: "Shoutrrr notification", // Explicitly set default
|
||||
}
|
||||
service.pkr = format.NewPropKeyResolver(service.Config)
|
||||
|
||||
if err := service.Config.setURL(&service.pkr, configURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.client = jsonclient.NewClient()
|
||||
service.client.Headers().Set("Access-Token", service.Config.Token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetID returns the service identifier.
|
||||
func (service *Service) GetID() string {
|
||||
return Scheme
|
||||
}
|
||||
|
||||
// Send a push notification via Pushbullet.
|
||||
func (service *Service) Send(message string, params *types.Params) error {
|
||||
config := *service.Config
|
||||
if err := service.pkr.UpdateConfigFromParams(&config, params); err != nil {
|
||||
return fmt.Errorf("updating config from params: %w", err)
|
||||
}
|
||||
|
||||
for _, target := range config.Targets {
|
||||
if err := doSend(&config, target, message, service.client); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doSend sends a push notification to a specific target and validates the response.
|
||||
func doSend(config *Config, target string, message string, client jsonclient.Client) error {
|
||||
push := NewNotePush(message, config.Title)
|
||||
push.SetTarget(target)
|
||||
|
||||
response := PushResponse{}
|
||||
if err := client.Post(pushesEndpoint, push, &response); err != nil {
|
||||
errorResponse := &ResponseError{}
|
||||
if client.ErrorResponse(err, errorResponse) {
|
||||
return fmt.Errorf("API error: %w", errorResponse)
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to push: %w", err)
|
||||
}
|
||||
|
||||
// Validate response fields
|
||||
if response.Type != "note" {
|
||||
return fmt.Errorf("%w: got %s", ErrUnexpectedResponseType, response.Type)
|
||||
}
|
||||
|
||||
if response.Body != message {
|
||||
return fmt.Errorf(
|
||||
"%w: got %s, expected %s",
|
||||
ErrResponseBodyMismatch,
|
||||
response.Body,
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
if response.Title != config.Title {
|
||||
return fmt.Errorf(
|
||||
"%w: got %s, expected %s",
|
||||
ErrResponseTitleMismatch,
|
||||
response.Title,
|
||||
config.Title,
|
||||
)
|
||||
}
|
||||
|
||||
if !response.Active {
|
||||
return ErrPushNotActive
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
95
pkg/services/pushbullet/pushbullet_config.go
Normal file
95
pkg/services/pushbullet/pushbullet_config.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package pushbullet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// Scheme is the scheme part of the service configuration URL.
|
||||
const Scheme = "pushbullet"
|
||||
|
||||
// ExpectedTokenLength is the required length for a valid Pushbullet token.
|
||||
const ExpectedTokenLength = 34
|
||||
|
||||
// ErrTokenIncorrectSize indicates that the token has an incorrect size.
|
||||
var ErrTokenIncorrectSize = errors.New("token has incorrect size")
|
||||
|
||||
// Config holds the configuration for the Pushbullet service.
|
||||
type Config struct {
|
||||
standard.EnumlessConfig
|
||||
Targets []string `url:"path"`
|
||||
Token string `url:"host"`
|
||||
Title string ` default:"Shoutrrr notification" key:"title"`
|
||||
}
|
||||
|
||||
// GetURL returns a URL representation of the Config's current field values.
|
||||
func (config *Config) GetURL() *url.URL {
|
||||
resolver := format.NewPropKeyResolver(config)
|
||||
|
||||
return config.getURL(&resolver)
|
||||
}
|
||||
|
||||
// SetURL updates the Config from a URL representation of its field values.
|
||||
func (config *Config) SetURL(url *url.URL) error {
|
||||
resolver := format.NewPropKeyResolver(config)
|
||||
|
||||
return config.setURL(&resolver, url)
|
||||
}
|
||||
|
||||
// getURL constructs a URL from the Config's fields using the provided resolver.
|
||||
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
|
||||
return &url.URL{
|
||||
Host: config.Token,
|
||||
Path: "/" + strings.Join(config.Targets, "/"),
|
||||
Scheme: Scheme,
|
||||
ForceQuery: false,
|
||||
RawQuery: format.BuildQuery(resolver),
|
||||
}
|
||||
}
|
||||
|
||||
// setURL updates the Config from a URL using the provided resolver.
|
||||
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
|
||||
path := url.Path
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
if url.Fragment != "" {
|
||||
path += "/#" + url.Fragment
|
||||
}
|
||||
|
||||
targets := strings.Split(path, "/")
|
||||
|
||||
token := url.Hostname()
|
||||
if url.String() != "pushbullet://dummy@dummy.com" {
|
||||
if err := validateToken(token); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
config.Token = token
|
||||
config.Targets = targets
|
||||
|
||||
for key, vals := range url.Query() {
|
||||
if err := resolver.Set(key, vals[0]); err != nil {
|
||||
return fmt.Errorf("setting query parameter %q to %q: %w", key, vals[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateToken checks if the token meets the expected length requirement.
|
||||
func validateToken(token string) error {
|
||||
if len(token) != ExpectedTokenLength {
|
||||
return ErrTokenIncorrectSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
74
pkg/services/pushbullet/pushbullet_json.go
Normal file
74
pkg/services/pushbullet/pushbullet_json.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package pushbullet
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var emailPattern = regexp.MustCompile(`.*@.*\..*`)
|
||||
|
||||
// PushRequest ...
|
||||
type PushRequest struct {
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
|
||||
Email string `json:"email"`
|
||||
ChannelTag string `json:"channel_tag"`
|
||||
DeviceIden string `json:"device_iden"`
|
||||
}
|
||||
|
||||
type PushResponse struct {
|
||||
Active bool `json:"active"`
|
||||
Body string `json:"body"`
|
||||
Created float64 `json:"created"`
|
||||
Direction string `json:"direction"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
Iden string `json:"iden"`
|
||||
Modified float64 `json:"modified"`
|
||||
ReceiverEmail string `json:"receiver_email"`
|
||||
ReceiverEmailNormalized string `json:"receiver_email_normalized"`
|
||||
ReceiverIden string `json:"receiver_iden"`
|
||||
SenderEmail string `json:"sender_email"`
|
||||
SenderEmailNormalized string `json:"sender_email_normalized"`
|
||||
SenderIden string `json:"sender_iden"`
|
||||
SenderName string `json:"sender_name"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type ResponseError struct {
|
||||
ErrorData struct {
|
||||
Cat string `json:"cat"`
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (err *ResponseError) Error() string {
|
||||
return err.ErrorData.Message
|
||||
}
|
||||
|
||||
func (p *PushRequest) SetTarget(target string) {
|
||||
if emailPattern.MatchString(target) {
|
||||
p.Email = target
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(target) > 0 && string(target[0]) == "#" {
|
||||
p.ChannelTag = target[1:]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.DeviceIden = target
|
||||
}
|
||||
|
||||
// NewNotePush creates a new push request.
|
||||
func NewNotePush(message, title string) *PushRequest {
|
||||
return &PushRequest{
|
||||
Type: "note",
|
||||
Title: title,
|
||||
Body: message,
|
||||
}
|
||||
}
|
248
pkg/services/pushbullet/pushbullet_test.go
Normal file
248
pkg/services/pushbullet/pushbullet_test.go
Normal file
|
@ -0,0 +1,248 @@
|
|||
package pushbullet_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/testutils"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/pushbullet"
|
||||
)
|
||||
|
||||
func TestPushbullet(t *testing.T) {
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
ginkgo.RunSpecs(t, "Shoutrrr Pushbullet Suite")
|
||||
}
|
||||
|
||||
var (
|
||||
service *pushbullet.Service
|
||||
envPushbulletURL *url.URL
|
||||
_ = ginkgo.BeforeSuite(func() {
|
||||
service = &pushbullet.Service{}
|
||||
envPushbulletURL, _ = url.Parse(os.Getenv("SHOUTRRR_PUSHBULLET_URL"))
|
||||
})
|
||||
)
|
||||
|
||||
var _ = ginkgo.Describe("the pushbullet service", func() {
|
||||
ginkgo.When("running integration tests", func() {
|
||||
ginkgo.It("should not error out", func() {
|
||||
if envPushbulletURL.String() == "" {
|
||||
return
|
||||
}
|
||||
|
||||
serviceURL, _ := url.Parse(envPushbulletURL.String())
|
||||
err := service.Initialize(serviceURL, testutils.TestLogger())
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
err = service.Send("This is an integration test message", nil)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
ginkgo.It("returns the correct service identifier", func() {
|
||||
gomega.Expect(service.GetID()).To(gomega.Equal("pushbullet"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("the pushbullet config", func() {
|
||||
ginkgo.When("generating a config object", func() {
|
||||
ginkgo.It("should set token", func() {
|
||||
pushbulletURL, _ := url.Parse("pushbullet://tokentokentokentokentokentokentoke")
|
||||
config := pushbullet.Config{}
|
||||
err := config.SetURL(pushbulletURL)
|
||||
|
||||
gomega.Expect(config.Token).To(gomega.Equal("tokentokentokentokentokentokentoke"))
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
|
||||
ginkgo.It("should set the device from path", func() {
|
||||
pushbulletURL, _ := url.Parse(
|
||||
"pushbullet://tokentokentokentokentokentokentoke/test",
|
||||
)
|
||||
config := pushbullet.Config{}
|
||||
err := config.SetURL(pushbulletURL)
|
||||
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(config.Targets).To(gomega.HaveLen(1))
|
||||
gomega.Expect(config.Targets).To(gomega.ContainElements("test"))
|
||||
})
|
||||
|
||||
ginkgo.It("should set the channel from path", func() {
|
||||
pushbulletURL, _ := url.Parse(
|
||||
"pushbullet://tokentokentokentokentokentokentoke/foo#bar",
|
||||
)
|
||||
config := pushbullet.Config{}
|
||||
err := config.SetURL(pushbulletURL)
|
||||
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(config.Targets).To(gomega.HaveLen(2))
|
||||
gomega.Expect(config.Targets).To(gomega.ContainElements("foo", "#bar"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("parsing the configuration URL", func() {
|
||||
ginkgo.It("should be identical after de-/serialization", func() {
|
||||
testURL := "pushbullet://tokentokentokentokentokentokentoke/device?title=Great+News"
|
||||
|
||||
config := &pushbullet.Config{}
|
||||
err := config.SetURL(testutils.URLMust(testURL))
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "verifying")
|
||||
|
||||
outputURL := config.GetURL()
|
||||
gomega.Expect(outputURL.String()).To(gomega.Equal(testURL))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("building the payload", func() {
|
||||
ginkgo.It("Email target should only populate one the correct field", func() {
|
||||
push := pushbullet.PushRequest{}
|
||||
push.SetTarget("iam@email.com")
|
||||
gomega.Expect(push.Email).To(gomega.Equal("iam@email.com"))
|
||||
gomega.Expect(push.DeviceIden).To(gomega.BeEmpty())
|
||||
gomega.Expect(push.ChannelTag).To(gomega.BeEmpty())
|
||||
})
|
||||
|
||||
ginkgo.It("Device target should only populate one the correct field", func() {
|
||||
push := pushbullet.PushRequest{}
|
||||
push.SetTarget("device")
|
||||
gomega.Expect(push.Email).To(gomega.BeEmpty())
|
||||
gomega.Expect(push.DeviceIden).To(gomega.Equal("device"))
|
||||
gomega.Expect(push.ChannelTag).To(gomega.BeEmpty())
|
||||
})
|
||||
|
||||
ginkgo.It("Channel target should only populate one the correct field", func() {
|
||||
push := pushbullet.PushRequest{}
|
||||
push.SetTarget("#channel")
|
||||
gomega.Expect(push.Email).To(gomega.BeEmpty())
|
||||
gomega.Expect(push.DeviceIden).To(gomega.BeEmpty())
|
||||
gomega.Expect(push.ChannelTag).To(gomega.Equal("channel"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("sending the payload", func() {
|
||||
var err error
|
||||
targetURL := "https://api.pushbullet.com/v2/pushes"
|
||||
ginkgo.BeforeEach(func() {
|
||||
httpmock.Activate()
|
||||
})
|
||||
|
||||
ginkgo.AfterEach(func() {
|
||||
httpmock.DeactivateAndReset()
|
||||
})
|
||||
|
||||
ginkgo.It("should not report an error if the server accepts the payload", func() {
|
||||
err = initService()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
response := pushbullet.PushResponse{
|
||||
Type: "note",
|
||||
Body: "Message",
|
||||
Title: "Shoutrrr notification", // Matches default
|
||||
Active: true,
|
||||
}
|
||||
responder, _ := httpmock.NewJsonResponder(200, &response)
|
||||
httpmock.RegisterResponder("POST", targetURL, responder)
|
||||
|
||||
err = service.Send("Message", nil)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
|
||||
ginkgo.It("should not panic if an error occurs when sending the payload", func() {
|
||||
err = initService()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
httpmock.RegisterResponder(
|
||||
"POST",
|
||||
targetURL,
|
||||
httpmock.NewErrorResponder(errors.New("")),
|
||||
)
|
||||
|
||||
err = service.Send("Message", nil)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
})
|
||||
|
||||
ginkgo.It("should return an error if the response type is incorrect", func() {
|
||||
err = initService()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
response := pushbullet.PushResponse{
|
||||
Type: "link", // Incorrect type
|
||||
Body: "Message",
|
||||
Title: "Shoutrrr notification",
|
||||
Active: true,
|
||||
}
|
||||
responder, _ := httpmock.NewJsonResponder(200, &response)
|
||||
httpmock.RegisterResponder("POST", targetURL, responder)
|
||||
|
||||
err = service.Send("Message", nil)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(err.Error()).To(gomega.ContainSubstring("unexpected response type"))
|
||||
})
|
||||
|
||||
ginkgo.It("should return an error if the response body does not match", func() {
|
||||
err = initService()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
response := pushbullet.PushResponse{
|
||||
Type: "note",
|
||||
Body: "Wrong message",
|
||||
Title: "Shoutrrr notification",
|
||||
Active: true,
|
||||
}
|
||||
responder, _ := httpmock.NewJsonResponder(200, &response)
|
||||
httpmock.RegisterResponder("POST", targetURL, responder)
|
||||
|
||||
err = service.Send("Message", nil)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(err.Error()).To(gomega.ContainSubstring("response body mismatch"))
|
||||
})
|
||||
|
||||
ginkgo.It("should return an error if the response title does not match", func() {
|
||||
err = initService()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
response := pushbullet.PushResponse{
|
||||
Type: "note",
|
||||
Body: "Message",
|
||||
Title: "Wrong Title",
|
||||
Active: true,
|
||||
}
|
||||
responder, _ := httpmock.NewJsonResponder(200, &response)
|
||||
httpmock.RegisterResponder("POST", targetURL, responder)
|
||||
|
||||
err = service.Send("Message", nil)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(err.Error()).To(gomega.ContainSubstring("response title mismatch"))
|
||||
})
|
||||
|
||||
ginkgo.It("should return an error if the push is not active", func() {
|
||||
err = initService()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
response := pushbullet.PushResponse{
|
||||
Type: "note",
|
||||
Body: "Message",
|
||||
Title: "Shoutrrr notification", // Matches default
|
||||
Active: false,
|
||||
}
|
||||
responder, _ := httpmock.NewJsonResponder(200, &response)
|
||||
httpmock.RegisterResponder("POST", targetURL, responder)
|
||||
|
||||
err = service.Send("Message", nil)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(err.Error()).
|
||||
To(gomega.ContainSubstring("push notification is not active"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// initService initializes the service with a fixed test configuration.
|
||||
func initService() error {
|
||||
serviceURL, err := url.Parse("pushbullet://tokentokentokentokentokentokentoke/test")
|
||||
gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
return service.Initialize(serviceURL, testutils.TestLogger())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue