From 67b2d011b81142a0f2df16e1eb5ef904276eabfb Mon Sep 17 00:00:00 2001 From: Prashanth Pai Date: Sat, 6 Feb 2021 14:30:42 +0530 Subject: [PATCH] feat(service): Add Plivo service Added support for Plivo as a backend service with unit tests and documentation. Signed-off-by: Prashanth Pai --- README.md | 7 +- go.mod | 4 ++ go.sum | 5 ++ service/plivo/README.md | 49 +++++++++++++ service/plivo/doc.go | 41 +++++++++++ service/plivo/mock_plivoMsgClient.go | 36 ++++++++++ service/plivo/plivo.go | 102 +++++++++++++++++++++++++++ service/plivo/plivo_test.go | 78 ++++++++++++++++++++ 8 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 service/plivo/README.md create mode 100644 service/plivo/doc.go create mode 100644 service/plivo/mock_plivoMsgClient.go create mode 100644 service/plivo/plivo.go create mode 100644 service/plivo/plivo_test.go diff --git a/README.md b/README.md index b978ca6..8d305e2 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,11 @@ _ = notifier.Send( - *Discord* - *Email* - *Microsoft Teams* -- *Slack* -- *Twitter* -- *Telegram* +- *Plivo* - *Pushbullet* +- *Slack* +- *Telegram* +- *Twitter* ## Roadmap diff --git a/go.mod b/go.mod index 5ae64bb..4ba4c56 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,13 @@ require ( github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d github.com/dghubble/oauth1 v0.7.0 github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible + github.com/google/go-querystring v1.0.0 // indirect github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/pkg/errors v0.9.1 + github.com/plivo/plivo-go v5.5.1+incompatible + github.com/sirupsen/logrus v1.7.0 // indirect github.com/slack-go/slack v0.8.0 + github.com/stretchr/testify v1.7.0 github.com/technoweenie/multipartstreamer v1.0.1 // indirect golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect golang.org/x/sync v0.0.0-20201207232520-09787c993a3a diff --git a/go.sum b/go.sum index 5ba431b..85e934c 100644 --- a/go.sum +++ b/go.sum @@ -29,10 +29,15 @@ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/plivo/plivo-go v5.5.1+incompatible h1:LtZaUNHjSrNzBCHAe/IdDBnLGlyZB+WX18Dr+dnlVzE= +github.com/plivo/plivo-go v5.5.1+incompatible/go.mod h1:OhnI9crdl6O+D94Lp1lvuwJoA3KUH39J6IM+j3HwCBE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/slack-go/slack v0.8.0 h1:ANyLY5KHLV+MxLJDQum2IuHTLwbCbDtaWY405X1EU9U= github.com/slack-go/slack v0.8.0/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/service/plivo/README.md b/service/plivo/README.md new file mode 100644 index 0000000..9b00244 --- /dev/null +++ b/service/plivo/README.md @@ -0,0 +1,49 @@ +# Plivo + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/nikoksr/notify/service/plivo) + +## Prerequisites + +You will need to have a [Plivo](https://www.plivo.com/) account and the +following things: + +1. `Auth ID` and `Auth Token` from Plivo [console](https://console.plivo.com/dashboard/). +1. An active rented Plivo [phone number](https://console.plivo.com/active-phone-numbers/). + +## Usage + +```go +package main + +import ( + "log" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/plivo" +) + +func main() { + plivoSvc, err := plivo.New( + &plivo.ClientOptions{ + AuthID: "", + AuthToken: "", + }, &plivo.MessageOptions{ + Source: "", + }) + if err != nil { + log.Fatalf("plivo.New() failed: %s", err.Error()) + } + + plivoSvc.AddReceivers("Destination1") + + notifier := notify.New() + notifier.UseServices(plivoSvc) + + err = notifier.Send("subject", "message") + if err != nil { + log.Fatalf("notifier.Send() failed: %s", err.Error()) + } + + log.Printf("notification sent") +} +``` diff --git a/service/plivo/doc.go b/service/plivo/doc.go new file mode 100644 index 0000000..7009d40 --- /dev/null +++ b/service/plivo/doc.go @@ -0,0 +1,41 @@ +/* +Package plivo provides message notification integration for Plivo. + +Usage: + + package main + + import ( + "log" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/plivo" + ) + + func main() { + plivoSvc, err := plivo.New( + &plivo.ClientOptions{ + AuthID: "", + AuthToken: "", + }, &plivo.MessageOptions{ + Source: "", + }) + if err != nil { + log.Fatalf("plivo.New() failed: %s", err.Error()) + } + + plivoSvc.AddReceivers("Destination1") + + notifier := notify.New() + notifier.UseServices(plivoSvc) + + err = notifier.Send("subject", "message") + if err != nil { + log.Fatalf("notifier.Send() failed: %s", err.Error()) + } + + log.Printf("notification sent") + } + +*/ +package plivo diff --git a/service/plivo/mock_plivoMsgClient.go b/service/plivo/mock_plivoMsgClient.go new file mode 100644 index 0000000..c0ec80b --- /dev/null +++ b/service/plivo/mock_plivoMsgClient.go @@ -0,0 +1,36 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package plivo + +import ( + plivo "github.com/plivo/plivo-go" + mock "github.com/stretchr/testify/mock" +) + +// mockPlivoMsgClient is an autogenerated mock type for the plivoMsgClient type +type mockPlivoMsgClient struct { + mock.Mock +} + +// Create provides a mock function with given fields: _a0 +func (_m *mockPlivoMsgClient) Create(_a0 plivo.MessageCreateParams) (*plivo.MessageCreateResponseBody, error) { + ret := _m.Called(_a0) + + var r0 *plivo.MessageCreateResponseBody + if rf, ok := ret.Get(0).(func(plivo.MessageCreateParams) *plivo.MessageCreateResponseBody); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plivo.MessageCreateResponseBody) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(plivo.MessageCreateParams) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/service/plivo/plivo.go b/service/plivo/plivo.go new file mode 100644 index 0000000..2eccf10 --- /dev/null +++ b/service/plivo/plivo.go @@ -0,0 +1,102 @@ +package plivo + +import ( + "fmt" + "net/http" + "strings" + + plivo "github.com/plivo/plivo-go" +) + +// ClientOptions allow you to configure a Plivo SDK client. +type ClientOptions struct { + AuthID string // If empty, env variable PLIVO_AUTH_ID will be used + AuthToken string // If empty, env variable PLIVO_AUTH_TOKEN will be used + + // Optional + HTTPClient *http.Client // Bring Your Own Client +} + +// MessageOptions allow you to configure options for sending a message. +type MessageOptions struct { + Source string // a Plivo source phone number or a Plivo Powerpack UUID + + // Optional + CallbackURL string // URL to which status update callbacks for the message should be sent + CallbackMethod string // The HTTP method to be used when calling CallbackURL - GET or POST(default) +} + +// plivoMsgClient abstracts Plivo SDK for writing unit tests +type plivoMsgClient interface { + Create(plivo.MessageCreateParams) (*plivo.MessageCreateResponseBody, error) +} + +// Service is a Plivo client +type Service struct { + client plivoMsgClient + mopts MessageOptions + destinations []string +} + +// New creates a new instance of plivo service. +func New(cOpts *ClientOptions, mOpts *MessageOptions) (*Service, error) { + if cOpts == nil { + return nil, fmt.Errorf("client-options cannot be nil") + } + + if mOpts == nil { + return nil, fmt.Errorf("message-options cannot be nil") + } + + if mOpts.Source == "" { + return nil, fmt.Errorf("source cannot be empty") + } + + client, err := plivo.NewClient( + cOpts.AuthID, + cOpts.AuthToken, + &plivo.ClientOptions{ + HttpClient: cOpts.HTTPClient, + }, + ) + if err != nil { + return nil, fmt.Errorf("plivo.NewClient() failed: %w", err) + } + + return &Service{ + client: client.Messages, + mopts: *mOpts, + }, nil +} + +// AddReceivers adds the given destination phone numbers to the notifier. +func (s *Service) AddReceivers(phoneNumbers ...string) { + s.destinations = append(s.destinations, phoneNumbers...) +} + +// Send sends a SMS via Plivo to all previously added receivers. +func (s *Service) Send(subject, message string) error { + text := subject + "\n" + message + + var dst string + switch len(s.destinations) { + case 0: + return fmt.Errorf("no receivers added") + case 1: + dst = s.destinations[0] + default: + // multiple destinations, use bulk message syntax + // see: https://www.plivo.com/docs/sms/api/message#bulk-messaging + dst = strings.Join(s.destinations, "<") + } + + _, err := s.client.Create(plivo.MessageCreateParams{ + Dst: dst, + Text: text, + Src: s.mopts.Source, + URL: s.mopts.CallbackURL, + Method: s.mopts.CallbackMethod, + }) + + return err +} diff --git a/service/plivo/plivo_test.go b/service/plivo/plivo_test.go new file mode 100644 index 0000000..6f7893a --- /dev/null +++ b/service/plivo/plivo_test.go @@ -0,0 +1,78 @@ +package plivo + +import ( + "errors" + "testing" + + plivo "github.com/plivo/plivo-go" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + assert := require.New(t) + + // nil ClientOptions + svc, err := New(nil, &MessageOptions{}) + assert.NotNil(err) + assert.Nil(svc) + + // nil MessageOptions + svc, err = New(&ClientOptions{}, nil) + assert.NotNil(err) + assert.Nil(svc) + + // empty source + svc, err = New(&ClientOptions{}, &MessageOptions{}) + assert.NotNil(err) + assert.Nil(svc) + + // success + svc, err = New(&ClientOptions{}, &MessageOptions{Source: "12345"}) + assert.Nil(err) + assert.NotNil(svc) +} + +func TestAddReceivers(t *testing.T) { + assert := require.New(t) + + svc, err := New(&ClientOptions{}, &MessageOptions{Source: "12345"}) + assert.Nil(err) + assert.NotNil(svc) + + nums := []string{"1", "2", "3", "4", "5"} + svc.AddReceivers(nums...) + + assert.Equal(svc.destinations, nums) +} + +func TestSend(t *testing.T) { + assert := require.New(t) + + svc, err := New(&ClientOptions{}, &MessageOptions{Source: "12345"}) + assert.Nil(err) + assert.NotNil(svc) + + // no receivers added + err = svc.Send("message", "test") + assert.NotNil(err) + + // test plivo client returning error + mockClient := new(mockPlivoMsgClient) + mockClient.On("Create", plivo.MessageCreateParams{Src: "12345", Dst: "67890", Text: "message\ntest"}). + Return(nil, errors.New("some error")) + svc.client = mockClient + svc.AddReceivers("67890") + err = svc.Send("message", "test") + assert.NotNil(err) + mockClient.AssertExpectations(t) + + // test success and multiple receivers + mockClient = new(mockPlivoMsgClient) + mockClient.On("Create", plivo.MessageCreateParams{Src: "12345", Dst: "67890<09876", Text: "message\ntest"}). + Return(nil, nil) + svc.client = mockClient + svc.AddReceivers("09876") + err = svc.Send("message", "test") + assert.Nil(err) + mockClient.AssertExpectations(t) +}