diff --git a/README.md b/README.md index 41575deb..628349cc 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our | [Plivo](https://www.plivo.com) | [service/plivo](service/plivo) | [plivo/plivo-go](https://github.com/plivo/plivo-go) | :heavy_check_mark: | | [Pushover](https://pushover.net/) | [service/pushover](service/pushover) | [gregdel/pushover](https://github.com/gregdel/pushover) | :heavy_check_mark: | | [Pushbullet](https://www.pushbullet.com) | [service/pushbullet](service/pushbullet) | [cschomburg/go-pushbullet](https://github.com/cschomburg/go-pushbullet) | :heavy_check_mark: | +| [Reddit](https://www.reddit.com) | [service/reddit](service/reddit) | [vartanbeno/go-reddit](https://github.com/vartanbeno/go-reddit) | :heavy_check_mark: | | [RocketChat](https://rocket.chat) | [service/rocketchat](service/rocketchat) | [RocketChat/Rocket.Chat.Go.SDK](https://github.com/RocketChat/Rocket.Chat.Go.SDK) | :heavy_check_mark: | | [SendGrid](https://sendgrid.com) | [service/sendgrid](service/sendgrid) | [sendgrid/sendgrid-go](https://github.com/sendgrid/sendgrid-go) | :heavy_check_mark: | | [Slack](https://slack.com) | [service/slack](service/slack) | [slack-go/slack](https://github.com/slack-go/slack) | :heavy_check_mark: | diff --git a/go.mod b/go.mod index fceb8ed8..cb27dee6 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,8 @@ require ( maunium.net/go/mautrix v0.12.3 ) +require github.com/vartanbeno/go-reddit/v2 v2.0.1 + require ( github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect diff --git a/go.sum b/go.sum index d4254e90..3db3f450 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20220903135808-56c5346a1a28 h1:OJe0G++TYGhE525XnkrF9KF15D1WtQdyrk19SFwRrKk= @@ -108,6 +109,7 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -233,6 +235,8 @@ github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWL github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs= github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE= +github.com/vartanbeno/go-reddit/v2 v2.0.1 h1:P6ITpf5YHjdy7DHZIbUIDn/iNAoGcEoDQnMa+L4vutw= +github.com/vartanbeno/go-reddit/v2 v2.0.1/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -243,7 +247,9 @@ golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -255,10 +261,12 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c h1:q3gFqPqH7NVofKo3c3yETAP//pPI+G5mvB7qqj1Y5kY= golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20171101214715-fd80eb99c8f6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -296,6 +304,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/service/reddit/README.md b/service/reddit/README.md new file mode 100644 index 00000000..c83a4b19 --- /dev/null +++ b/service/reddit/README.md @@ -0,0 +1,16 @@ +# Reddit Usage + +Ensure that you have already navigated to your GOPATH and installed the following packages: + +* `go get -u github.com/nikoksr/notify` + +## Steps for Reddit notifications + +These are general and very high level instructions + +1. Log into Reddit create a new "script" type by visiting [here](https://www.reddit.com/prefs/apps/) +2. The "redirect uri" parameter doesn't matter in this case and can just be set to `http://localhost:8080` +2. Copy the *client id* and *client secret* for usage below +4. Now you should be good to use the code detailed in [doc.go](doc.go) + +**NOTE**: You may have difficulties using your user's password if you have 2FA enabled. You can disable it by going [here](https://www.reddit.com/prefs/update/) but be aware of the security implications and ensure you have a strong password set. \ No newline at end of file diff --git a/service/reddit/doc.go b/service/reddit/doc.go new file mode 100644 index 00000000..8433a86f --- /dev/null +++ b/service/reddit/doc.go @@ -0,0 +1,37 @@ +/* +Package reddit implements a Reddit notifier, allowing messages to be sent to multiple recipients + +Usage: + + package main + + import ( + "context" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/reddit" + ) + + func main() { + + notifier := notify.New() + + // Provide your Reddit app credentials and username/password + redditService := reddit.New("clientID", "clientSecret", "username", "password") + + // Pass the usernames for where to send the messages + pushoverService.AddReceivers("User1", "User2") + + // Tell our notifier to use the Reddit service. You can repeat the above process + // for as many services as you like and just tell the notifier to use them. + notifier.UseServices(redditService) + + // Send a message + _ = notifier.Send( + context.Background(), + "Hello!", + "I am a bot written in Go!", + ) + } +*/ +package reddit diff --git a/service/reddit/mock_reddit_message_client.go b/service/reddit/mock_reddit_message_client.go new file mode 100644 index 00000000..903f69ca --- /dev/null +++ b/service/reddit/mock_reddit_message_client.go @@ -0,0 +1,53 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package reddit + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v2reddit "github.com/vartanbeno/go-reddit/v2/reddit" +) + +// mockRedditMessageClient is an autogenerated mock type for the redditMessageClient type +type mockRedditMessageClient struct { + mock.Mock +} + +// Send provides a mock function with given fields: _a0, _a1 +func (_m *mockRedditMessageClient) Send(_a0 context.Context, _a1 *v2reddit.SendMessageRequest) (*v2reddit.Response, error) { + ret := _m.Called(_a0, _a1) + + var r0 *v2reddit.Response + if rf, ok := ret.Get(0).(func(context.Context, *v2reddit.SendMessageRequest) *v2reddit.Response); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v2reddit.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *v2reddit.SendMessageRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTnewMockRedditMessageClient interface { + mock.TestingT + Cleanup(func()) +} + +// newMockRedditMessageClient creates a new instance of mockRedditMessageClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func newMockRedditMessageClient(t mockConstructorTestingTnewMockRedditMessageClient) *mockRedditMessageClient { + mock := &mockRedditMessageClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/service/reddit/reddit.go b/service/reddit/reddit.go new file mode 100644 index 00000000..3227c2ea --- /dev/null +++ b/service/reddit/reddit.go @@ -0,0 +1,87 @@ +// Package reddit implements a Reddit notifier, allowing messages to be sent to multiple recipients +package reddit + +import ( + "context" + "crypto/tls" + "net/http" + + "github.com/pkg/errors" + "github.com/vartanbeno/go-reddit/v2/reddit" +) + +//go:generate mockery --name=redditMessageClient --output=. --case=underscore --inpackage +type redditMessageClient interface { + Send(context.Context, *reddit.SendMessageRequest) (*reddit.Response, error) +} + +// Compile-time check to ensure that reddit.MessageService implements the redditMessageClient interface. +var _ redditMessageClient = new(reddit.MessageService) + +// Reddit struct holds necessary data to communicate with the Reddit API. +type Reddit struct { + client redditMessageClient + recipients []string +} + +// New returns a new instance of a Reddit notification service. +// For more information on obtaining client credentials: +// +// -> https://github.com/reddit-archive/reddit/wiki/OAuth2 +func New(clientID, clientSecret, username, password string) (*Reddit, error) { + // Disable HTTP2 in http client + // Details: https://www.reddit.com/r/redditdev/comments/t8e8hc/getting_nothing_but_429_responses_when_using_go/i18yga2/ + h := http.Client{ + Transport: &http.Transport{ + TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{}, + }, + } + rClient, err := reddit.NewClient( + reddit.Credentials{ + ID: clientID, + Secret: clientSecret, + Username: username, + Password: password, + }, + reddit.WithHTTPClient(&h), + reddit.WithUserAgent("github.com/nikoksr/notify"), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to instantiate base Reddit client") + } + + r := &Reddit{ + client: rClient.Message, + recipients: []string{}, + } + + return r, nil +} + +// AddReceivers takes Reddit usernames and adds them to the internal recipient list. The Send method will send +// a given message to all of those users. +func (r *Reddit) AddReceivers(recipients ...string) { + r.recipients = append(r.recipients, recipients...) +} + +// Send takes a message subject and a message body and sends them to all previously set recipients. +func (r *Reddit) Send(ctx context.Context, subject, message string) error { + for i := range r.recipients { + select { + case <-ctx.Done(): + return ctx.Err() + default: + m := reddit.SendMessageRequest{ + To: r.recipients[i], + Subject: subject, + Text: message, + } + + _, err := r.client.Send(ctx, &m) + if err != nil { + return errors.Wrapf(err, "failed to send message to Reddit recipient '%s'", r.recipients[i]) + } + } + } + return nil +} diff --git a/service/reddit/reddit_test.go b/service/reddit/reddit_test.go new file mode 100644 index 00000000..322afa54 --- /dev/null +++ b/service/reddit/reddit_test.go @@ -0,0 +1,76 @@ +package reddit + +import ( + context "context" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/vartanbeno/go-reddit/v2/reddit" +) + +func TestReddit_New(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + service, err := New("id", "secret", "user", "password") + assert.NotNil(service) + assert.NoError(err) +} + +func TestReddit_AddReceivers(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + service, err := New("id", "secret", "user", "password") + assert.NotNil(service) + assert.NoError(err) + + service.AddReceivers("") + assert.Len(service.recipients, 1) + + service.AddReceivers("", "") + assert.Len(service.recipients, 3) +} + +func TestReddit_Send(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + service, err := New("id", "secret", "user", "password") + assert.NotNil(service) + assert.NoError(err) + + // No receivers added + ctx := context.Background() + err = service.Send(ctx, "subject", "message") + assert.Nil(err) + + // Test error response + mockClient := newMockRedditMessageClient(t) + mockClient. + On("Send", ctx, mock.AnythingOfType("*reddit.SendMessageRequest")). + Return(&reddit.Response{}, errors.New("some error")) + + service.client = mockClient + service.AddReceivers("1234") + err = service.Send(ctx, "subject", "message") + assert.NotNil(err) + mockClient.AssertExpectations(t) + + // Test success response + mockClient = newMockRedditMessageClient(t) + mockClient. + On("Send", ctx, mock.AnythingOfType("*reddit.SendMessageRequest")). + Return(&reddit.Response{}, nil) + + service.client = mockClient + service.AddReceivers("5678") + err = service.Send(ctx, "subject", "message") + assert.Nil(err) + mockClient.AssertExpectations(t) +}