From ea8124413b2c650beec5bcf1ffd1518ad9dc33db Mon Sep 17 00:00:00 2001 From: Usama Alhanaqtah Date: Thu, 10 Apr 2025 13:50:55 +0300 Subject: [PATCH 1/2] feat: mastodon service added --- go.mod | 2 ++ go.sum | 4 +++ service/mastodon/doc.go | 44 +++++++++++++++++++++++ service/mastodon/mastodon.go | 69 ++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 service/mastodon/doc.go create mode 100644 service/mastodon/mastodon.go diff --git a/go.mod b/go.mod index 81f839db..fded8d29 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/PagerDuty/go-pagerduty v1.8.0 github.com/caarlos0/go-reddit/v3 v3.0.1 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/mattn/go-mastodon v0.0.9 google.golang.org/api v0.215.0 ) @@ -88,6 +89,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/rs/zerolog v1.33.0 // indirect + github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect go.mau.fi/util v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect diff --git a/go.sum b/go.sum index 25909c6e..ddf44619 100644 --- a/go.sum +++ b/go.sum @@ -264,6 +264,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-mastodon v0.0.9 h1:zAlQF0LMumKPQLNR7dZL/YVCrvr4iP6ayyzxTR3vsSw= +github.com/mattn/go-mastodon v0.0.9/go.mod h1:8YkqetHoAVEktRkK15qeiv/aaIMfJ/Gc89etisPZtHU= github.com/mileusna/viber v1.0.1 h1:gWB6/lKoWYVxkH0Jb8jRnGIRZ/9DEM7RBZRJHRfdYWs= github.com/mileusna/viber v1.0.1/go.mod h1:Pxu/iPMnYjnHgu+bEp3SiKWHWmlf/kDp/yOX8XUdYrQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -353,6 +355,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= diff --git a/service/mastodon/doc.go b/service/mastodon/doc.go new file mode 100644 index 00000000..84321181 --- /dev/null +++ b/service/mastodon/doc.go @@ -0,0 +1,44 @@ +/* +Package mastodon provides message notification integration for the Mastodon social network. + +Usage: + + package main + + import ( + "context" + "log" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/mastodon" + ) + + func main() { + cfg := &mastodon.Config{ + ServerURL: "https://mastodon.social", + AccessToken: "your-access-token", + Visibility: mastodon.VisibilityPublic, + } + + service, err := mastodon.New(cfg) + if err != nil { + log.Fatalf("mastodon.New() failed: %s", err.Error()) + } + + notifier := notify.New() + notifier.UseServices(service) + + err = notifier.Send(context.Background(), "", "Hello from Mastodon!") + if err != nil { + log.Fatalf("notifier.Send() failed: %s", err.Error()) + } + } + +Visibility values: + + - VisibilityPublic — visible to everyone + - VisibilityUnlisted — visible to everyone but not listed + - VisibilityPrivate — visible to followers only + - VisibilityDirect — visible only to mentioned users (direct message) +*/ +package mastodon diff --git a/service/mastodon/mastodon.go b/service/mastodon/mastodon.go new file mode 100644 index 00000000..c0833d2c --- /dev/null +++ b/service/mastodon/mastodon.go @@ -0,0 +1,69 @@ +package mastodon + +import ( + "context" + "errors" + + "github.com/mattn/go-mastodon" +) + +var ErrInvalidConfParams = errors.New("server and accessToken params are required") + +const ( + VisibilityPublic = "public" + VisibilityUnlisted = "unlisted" + VisibilityPrivate = "private" + VisibilityDirect = "direct" +) + +type Mastodon struct { + accessToken string + visibility string + client *mastodon.Client +} + +type Config struct { + Visibility string + ServerURL string + AccessToken string +} + +// New creates a new Mastodon client using the provided configuration. +// It returns an error if either the ServerURL or AccessToken is missing. +func New(cfg *Config) (*Mastodon, error) { + if cfg.ServerURL == "" || cfg.AccessToken == "" { + return nil, ErrInvalidConfParams + } + + clientCfg := &mastodon.Config{ + Server: cfg.ServerURL, + AccessToken: cfg.AccessToken, + } + + return &Mastodon{ + accessToken: cfg.AccessToken, + visibility: cfg.Visibility, + client: mastodon.NewClient(clientCfg), + }, nil +} + +// ChangeVisibility updates the default visibility setting for all future posts. +// Supported values include: public, unlisted, private (followers-only), and direct. +func (m *Mastodon) ChangeVisibility(visibility string) { + m.visibility = visibility +} + +// Send posts a new status (toot) to the configured Mastodon server. +// It uses the current visibility setting and accepts the message content as input. +func (m *Mastodon) Send(ctx context.Context, _, message string) error { + status := &mastodon.Toot{ + Visibility: m.visibility, + Status: message, + } + + if _, err := m.client.PostStatus(ctx, status); err != nil { + return err + } + + return nil +} From 4e253a5e77f3896dc2426b495cf3d21619213a18 Mon Sep 17 00:00:00 2001 From: Usama Alhanaqtah Date: Thu, 10 Apr 2025 14:55:27 +0300 Subject: [PATCH 2/2] test: mastodon service tests added --- service/mastodon/mastodon.go | 7 +- service/mastodon/mastodon_test.go | 91 ++++++++++++++++++++++ service/mastodon/mock_mastodon_client.go | 96 ++++++++++++++++++++++++ service/msteams/mock_teams_client.go | 1 - service/pagerduty/pager_duty.go | 6 +- 5 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 service/mastodon/mastodon_test.go create mode 100644 service/mastodon/mock_mastodon_client.go diff --git a/service/mastodon/mastodon.go b/service/mastodon/mastodon.go index c0833d2c..cbd5483e 100644 --- a/service/mastodon/mastodon.go +++ b/service/mastodon/mastodon.go @@ -16,10 +16,15 @@ const ( VisibilityDirect = "direct" ) +//go:generate mockery --name=mastodonClient --output=. --case=underscore --inpackage +type mastodonClient interface { + PostStatus(ctx context.Context, toot *mastodon.Toot) (*mastodon.Status, error) +} + type Mastodon struct { accessToken string visibility string - client *mastodon.Client + client mastodonClient } type Config struct { diff --git a/service/mastodon/mastodon_test.go b/service/mastodon/mastodon_test.go new file mode 100644 index 00000000..1bcb5f78 --- /dev/null +++ b/service/mastodon/mastodon_test.go @@ -0,0 +1,91 @@ +package mastodon + +import ( + "context" + "errors" + "testing" + + "github.com/mattn/go-mastodon" + "github.com/stretchr/testify/assert" +) + +var errSome = errors.New("some error") + +func TestNew(t *testing.T) { + tests := []struct { + name string + cfg *Config + expectedError error + }{ + { + name: "Success", + cfg: &Config{ + ServerURL: "some-url", + AccessToken: "some-access-token", + }, + expectedError: nil, + }, + { + name: "Invalid configuration", + cfg: &Config{ + ServerURL: "", + AccessToken: "", + }, + expectedError: ErrInvalidConfParams, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := New(tc.cfg) + assert.Equal(t, tc.expectedError, err) + }) + } +} + +func TestSend(t *testing.T) { + tests := []struct { + name string + message string + setup func(*mockmastodonClient) + expectedError error + }{ + { + name: "Success", + message: "Hello world!", + setup: func(mc *mockmastodonClient) { + mc.On("PostStatus", context.Background(), &mastodon.Toot{ + Status: "Hello world!", + }). + Return(&mastodon.Status{}, nil) + }, + expectedError: nil, + }, + { + name: "Sending failed", + message: "Hello world!", + setup: func(mc *mockmastodonClient) { + mc.On("PostStatus", context.Background(), &mastodon.Toot{ + Status: "Hello world!", + }). + Return(nil, errSome) + }, + expectedError: nil, + }, + } + + client := new(mockmastodonClient) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.setup(client) + + mastodon := Mastodon{ + client: client, + } + + err := mastodon.Send(context.Background(), tc.message, tc.message) + assert.Equal(t, tc.expectedError, err) + }) + } +} diff --git a/service/mastodon/mock_mastodon_client.go b/service/mastodon/mock_mastodon_client.go new file mode 100644 index 00000000..0cb5b985 --- /dev/null +++ b/service/mastodon/mock_mastodon_client.go @@ -0,0 +1,96 @@ +// Code generated by mockery v2.44.1. DO NOT EDIT. + +package mastodon + +import ( + context "context" + + go_mastodon "github.com/mattn/go-mastodon" + mock "github.com/stretchr/testify/mock" +) + +// mockmastodonClient is an autogenerated mock type for the mastodonClient type +type mockmastodonClient struct { + mock.Mock +} + +type mockmastodonClient_Expecter struct { + mock *mock.Mock +} + +func (_m *mockmastodonClient) EXPECT() *mockmastodonClient_Expecter { + return &mockmastodonClient_Expecter{mock: &_m.Mock} +} + +// PostStatus provides a mock function with given fields: ctx, toot +func (_m *mockmastodonClient) PostStatus(ctx context.Context, toot *go_mastodon.Toot) (*go_mastodon.Status, error) { + ret := _m.Called(ctx, toot) + + if len(ret) == 0 { + panic("no return value specified for PostStatus") + } + + var r0 *go_mastodon.Status + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *go_mastodon.Toot) (*go_mastodon.Status, error)); ok { + return rf(ctx, toot) + } + if rf, ok := ret.Get(0).(func(context.Context, *go_mastodon.Toot) *go_mastodon.Status); ok { + r0 = rf(ctx, toot) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*go_mastodon.Status) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *go_mastodon.Toot) error); ok { + r1 = rf(ctx, toot) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockmastodonClient_PostStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PostStatus' +type mockmastodonClient_PostStatus_Call struct { + *mock.Call +} + +// PostStatus is a helper method to define mock.On call +// - ctx context.Context +// - toot *go_mastodon.Toot +func (_e *mockmastodonClient_Expecter) PostStatus(ctx interface{}, toot interface{}) *mockmastodonClient_PostStatus_Call { + return &mockmastodonClient_PostStatus_Call{Call: _e.mock.On("PostStatus", ctx, toot)} +} + +func (_c *mockmastodonClient_PostStatus_Call) Run(run func(ctx context.Context, toot *go_mastodon.Toot)) *mockmastodonClient_PostStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*go_mastodon.Toot)) + }) + return _c +} + +func (_c *mockmastodonClient_PostStatus_Call) Return(_a0 *go_mastodon.Status, _a1 error) *mockmastodonClient_PostStatus_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockmastodonClient_PostStatus_Call) RunAndReturn(run func(context.Context, *go_mastodon.Toot) (*go_mastodon.Status, error)) *mockmastodonClient_PostStatus_Call { + _c.Call.Return(run) + return _c +} + +// newMockmastodonClient creates a new instance of mockmastodonClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockmastodonClient(t interface { + mock.TestingT + Cleanup(func()) +}) *mockmastodonClient { + mock := &mockmastodonClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/service/msteams/mock_teams_client.go b/service/msteams/mock_teams_client.go index 69e61c92..d1483498 100644 --- a/service/msteams/mock_teams_client.go +++ b/service/msteams/mock_teams_client.go @@ -7,7 +7,6 @@ import ( http "net/http" goteamsnotify "github.com/atc0005/go-teams-notify/v2" - mock "github.com/stretchr/testify/mock" ) diff --git a/service/pagerduty/pager_duty.go b/service/pagerduty/pager_duty.go index b1d7f9e3..d422c97b 100644 --- a/service/pagerduty/pager_duty.go +++ b/service/pagerduty/pager_duty.go @@ -11,7 +11,11 @@ import ( ) type Client interface { - CreateIncidentWithContext(ctx context.Context, from string, options *pagerduty.CreateIncidentOptions) (*pagerduty.Incident, error) //nolint:lll // acceptable in this case, alternative makes the interface even less readable + CreateIncidentWithContext( + ctx context.Context, + from string, + options *pagerduty.CreateIncidentOptions, + ) (*pagerduty.Incident, error) //nolint:lll // acceptable in this case, alternative makes the interface even less readable } // Compile-time check to verify that the PagerDuty type implements the notifier.Notifier interface.