diff --git a/changelog/unreleased/enhancement-mail-notifications-filtering.md b/changelog/unreleased/enhancement-mail-notifications-filtering.md new file mode 100644 index 000000000..c1585000f --- /dev/null +++ b/changelog/unreleased/enhancement-mail-notifications-filtering.md @@ -0,0 +1,6 @@ +Enhancement: Part III: Filtering of mail notifications + +Part III: Mail notifications are now filtered based on the notification preferences in the user settings + +https://github.com/owncloud/ocis/pull/10792 +https://github.com/owncloud/ocis/issues/10790 diff --git a/services/notifications/pkg/service/filter.go b/services/notifications/pkg/service/filter.go new file mode 100644 index 000000000..cde3fd19a --- /dev/null +++ b/services/notifications/pkg/service/filter.go @@ -0,0 +1,61 @@ +package service + +import ( + "context" + "errors" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/ocis-pkg/middleware" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + micrometadata "go-micro.dev/v4/metadata" +) + +type notificationFilter struct { + log log.Logger + valueClient settingssvc.ValueService +} + +func newNotificationFilter(l log.Logger, vc settingssvc.ValueService) *notificationFilter { + return ¬ificationFilter{log: l, valueClient: vc} +} + +// execute removes users who have disabled mail notifications for the event +func (nf notificationFilter) execute(ctx context.Context, users []*user.User, settingId string) []*user.User { + var filteredUsers []*user.User + + for _, u := range users { + userId := u.GetId().GetOpaqueId() + enabled, err := getSetting(ctx, nf.valueClient, userId, settingId) + if err != nil { + nf.log.Error().Err(err).Str("userId", userId).Str("settingId", settingId).Msg("cannot get user event setting") + continue + } + if enabled { + filteredUsers = append(filteredUsers, u) + } + } + + return filteredUsers +} + +func getSetting(ctx context.Context, vc settingssvc.ValueService, userId string, settingId string) (bool, error) { + resp, err := vc.GetValueByUniqueIdentifiers( + micrometadata.Set(ctx, middleware.AccountID, userId), + &settingssvc.GetValueByUniqueIdentifiersRequest{ + AccountUuid: userId, + SettingId: settingId, + }, + ) + + if err != nil { + return false, err + } + + val := resp.GetValue().GetValue().GetCollectionValue().GetValues() + for _, option := range val { + if option.GetKey() == "mail" { + return option.GetBoolValue(), nil + } + } + return false, errors.New("no setting found") +} diff --git a/services/notifications/pkg/service/filter_test.go b/services/notifications/pkg/service/filter_test.go new file mode 100644 index 000000000..c910ead66 --- /dev/null +++ b/services/notifications/pkg/service/filter_test.go @@ -0,0 +1,79 @@ +package service + +import ( + "context" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "go-micro.dev/v4/client" + "testing" +) + +var testLogger = log.NewLogger() + +func TestNotificationFilter_execute(t *testing.T) { + type args struct { + ctx context.Context + users []*user.User + settingId string + } + tests := []struct { + name string + vc settings.ValueService + args args + want []*user.User + }{ + {"no connection to ValueService", settings.MockValueService{ + GetValueByUniqueIdentifiersFunc: func(ctx context.Context, req *settings.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settings.GetValueResponse, error) { + return nil, errors.New("no connection to ValueService") + }, + }, args{users: []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, settingId: "bar", ctx: context.TODO()}, []*user.User(nil)}, + {"no setting in ValueService response", settings.MockValueService{ + GetValueByUniqueIdentifiersFunc: func(ctx context.Context, req *settings.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settings.GetValueResponse, error) { + return &settings.GetValueResponse{}, nil + }, + }, args{users: []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, settingId: "bar", ctx: context.TODO()}, []*user.User(nil)}, + {"ValueService nil response", settings.MockValueService{ + GetValueByUniqueIdentifiersFunc: func(ctx context.Context, req *settings.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settings.GetValueResponse, error) { + return nil, nil + }, + }, args{users: []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, settingId: "bar", ctx: context.TODO()}, []*user.User(nil)}, + {"Event enabled", setupMockValueService(true), args{users: []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, settingId: "bar", ctx: context.TODO()}, []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}}, + {"Event disabled", setupMockValueService(false), args{users: []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, settingId: "bar", ctx: context.TODO()}, []*user.User(nil)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ulf := notificationFilter{ + log: testLogger, + valueClient: tt.vc, + } + assert.Equal(t, tt.want, ulf.execute(tt.args.ctx, tt.args.users, tt.args.settingId)) + }) + } +} + +func setupMockValueService(mail bool) settings.ValueService { + return settings.MockValueService{ + GetValueByUniqueIdentifiersFunc: func(ctx context.Context, req *settings.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settings.GetValueResponse, error) { + return &settings.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_CollectionValue{ + CollectionValue: &settingsmsg.CollectionValue{ + Values: []*settingsmsg.CollectionOption{ + { + Key: "mail", + Option: &settingsmsg.CollectionOption_BoolValue{BoolValue: mail}, + }, + }, + }, + }, + }, + }, + }, nil + }, + } +} diff --git a/services/notifications/pkg/service/service.go b/services/notifications/pkg/service/service.go index 6eadebe54..4016249e9 100644 --- a/services/notifications/pkg/service/service.go +++ b/services/notifications/pkg/service/service.go @@ -65,6 +65,7 @@ func NewEventsNotifier( defaultLanguage: defaultLanguage, ocisURL: ocisURL, translationPath: translationPath, + filter: newNotificationFilter(logger, valueService), } } @@ -81,6 +82,7 @@ type eventsNotifier struct { ocisURL string serviceAccountID string serviceAccountSecret string + filter *notificationFilter } func (s eventsNotifier) Run() error { diff --git a/services/notifications/pkg/service/service_test.go b/services/notifications/pkg/service/service_test.go index abed14f99..e2ab94e13 100644 --- a/services/notifications/pkg/service/service_test.go +++ b/services/notifications/pkg/service/service_test.go @@ -2,6 +2,7 @@ package service_test import ( "context" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -68,7 +69,22 @@ var _ = Describe("Notifications", func() { gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Info: &provider.ResourceInfo{Name: "secrets of the board", Space: &provider.StorageSpace{Name: "secret space"}}}, nil) vs = &settingssvc.MockValueService{} vs.GetValueByUniqueIdentifiersFunc = func(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settingssvc.GetValueResponse, error) { - return nil, nil + return &settingssvc.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_CollectionValue{ + CollectionValue: &settingsmsg.CollectionValue{ + Values: []*settingsmsg.CollectionOption{ + { + Key: "mail", + Option: &settingsmsg.CollectionOption_BoolValue{BoolValue: true}, + }, + }, + }, + }, + }, + }, + }, nil } }) @@ -266,7 +282,22 @@ var _ = Describe("Notifications X-Site Scripting", func() { }, nil) vs = &settingssvc.MockValueService{} vs.GetValueByUniqueIdentifiersFunc = func(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settingssvc.GetValueResponse, error) { - return nil, nil + return &settingssvc.GetValueResponse{ + Value: &settingsmsg.ValueWithIdentifier{ + Value: &settingsmsg.Value{ + Value: &settingsmsg.Value_CollectionValue{ + CollectionValue: &settingsmsg.CollectionValue{ + Values: []*settingsmsg.CollectionOption{ + { + Key: "mail", + Option: &settingsmsg.CollectionOption_BoolValue{BoolValue: true}, + }, + }, + }, + }, + }, + }, + }, nil } }) diff --git a/services/notifications/pkg/service/shares.go b/services/notifications/pkg/service/shares.go index 58ec8bec5..1413b3e04 100644 --- a/services/notifications/pkg/service/shares.go +++ b/services/notifications/pkg/service/shares.go @@ -4,6 +4,7 @@ import ( "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/services/notifications/pkg/email" + "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" "google.golang.org/protobuf/types/known/fieldmaskpb" ) @@ -48,7 +49,8 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) { } granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID) - if granteeList == nil { + filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventShareCreated) + if filteredGrantees == nil { return } @@ -59,7 +61,7 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) { "ShareSharer": sharerDisplayName, "ShareFolder": resourceInfo.Name, "ShareLink": shareLink, - }, granteeList, sharerDisplayName) + }, filteredGrantees, sharerDisplayName) if err != nil { s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("could not get render the email") return @@ -100,7 +102,8 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) { } granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID) - if granteeList == nil { + filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventShareExpired) + if filteredGrantees == nil { return } @@ -109,7 +112,7 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) { map[string]string{ "ShareFolder": resourceInfo.GetName(), "ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"), - }, granteeList, owner.GetDisplayName()) + }, filteredGrantees, owner.GetDisplayName()) if err != nil { s.logger.Error().Err(err).Str("event", "ShareExpired").Msg("could not get render the email") return diff --git a/services/notifications/pkg/service/spaces.go b/services/notifications/pkg/service/spaces.go index 32ed5bacc..5f1b4c827 100644 --- a/services/notifications/pkg/service/spaces.go +++ b/services/notifications/pkg/service/spaces.go @@ -5,6 +5,7 @@ import ( "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/services/notifications/pkg/email" + "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" ) func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) { @@ -63,7 +64,8 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) { // the Grantees of the shares. Ideally the notfication service would use some kind of service // user for this. granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID) - if granteeList == nil { + filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceShared) + if filteredGrantees == nil { return } @@ -74,7 +76,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) { "SpaceSharer": sharerDisplayName, "SpaceName": resourceInfo.GetSpace().GetName(), "ShareLink": shareLink, - }, granteeList, sharerDisplayName) + }, filteredGrantees, sharerDisplayName) if err != nil { s.logger.Error().Err(err).Str("event", "SharedSpace").Msg("could not get render the email") return @@ -136,7 +138,8 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) { // the Grantees of the shares. Ideally the notfication service would use some kind of service // user for this. granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID) - if granteeList == nil { + filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceUnshared) + if filteredGrantees == nil { return } @@ -147,7 +150,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) { "SpaceSharer": sharerDisplayName, "SpaceName": resourceInfo.GetSpace().Name, "ShareLink": shareLink, - }, granteeList, sharerDisplayName) + }, filteredGrantees, sharerDisplayName) if err != nil { s.logger.Error().Err(err).Str("event", "UnsharedSpace").Msg("Could not get render the email") return @@ -185,13 +188,17 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp if granteeList == nil { return } + filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceMembershipExpired) + if filteredGrantees == nil { + return + } recipientList, err := s.render(ctx, email.MembershipExpired, "SpaceGrantee", map[string]string{ "SpaceName": e.SpaceName, "ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"), - }, granteeList, owner.GetDisplayName()) + }, filteredGrantees, owner.GetDisplayName()) if err != nil { s.logger.Error().Err(err).Str("event", "SpaceUnshared").Msg("could not get render the email") return