From b7c6d5a88bec65d4087dbc6038a673643f4a01af Mon Sep 17 00:00:00 2001 From: Bastian Beier Date: Tue, 17 Dec 2024 16:56:27 +0100 Subject: [PATCH] Add filtering of in-app notifications based on user settings --- ...ancement-in-app-notifications-filtering.md | 6 ++ services/userlog/pkg/service/filter.go | 102 ++++++++++++++++++ services/userlog/pkg/service/filter_test.go | 97 +++++++++++++++++ services/userlog/pkg/service/service.go | 17 +-- services/userlog/pkg/service/service_test.go | 18 +++- 5 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 changelog/unreleased/enhancement-in-app-notifications-filtering.md create mode 100644 services/userlog/pkg/service/filter.go create mode 100644 services/userlog/pkg/service/filter_test.go diff --git a/changelog/unreleased/enhancement-in-app-notifications-filtering.md b/changelog/unreleased/enhancement-in-app-notifications-filtering.md new file mode 100644 index 0000000000..54660daa41 --- /dev/null +++ b/changelog/unreleased/enhancement-in-app-notifications-filtering.md @@ -0,0 +1,6 @@ +Enhancement: Part II: Filtering of in-app notifications + +Part II: In-app notifications are now filtered based on the notification preferences in the user settings + +https://github.com/owncloud/ocis/pull/10779 +https://github.com/owncloud/ocis/issues/10769 diff --git a/services/userlog/pkg/service/filter.go b/services/userlog/pkg/service/filter.go new file mode 100644 index 0000000000..660e361a10 --- /dev/null +++ b/services/userlog/pkg/service/filter.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + "errors" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/v2/pkg/events" + "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" + "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" + micrometadata "go-micro.dev/v4/metadata" +) + +type userlogFilter struct { + log log.Logger + valueClient settingssvc.ValueService +} + +func newUserlogFilter(l log.Logger, vc settingssvc.ValueService) *userlogFilter { + return &userlogFilter{log: l, valueClient: vc} +} + +// execute removes users who should not receive an in-app notifications for the event +func (ulf userlogFilter) execute(ctx context.Context, event events.Event, executant *user.UserId, users []string) []string { + filteredUsers := ulf.filterExecutant(users, executant) + return ulf.filterUsersBySettings(ctx, filteredUsers, event) +} + +// filterExecutant removes the executant +func (ulf userlogFilter) filterExecutant(users []string, executant *user.UserId) []string { + var filteredUsers []string + for _, u := range users { + if u != executant.GetOpaqueId() { + filteredUsers = append(filteredUsers, u) + } + } + return filteredUsers +} + +// filterUsersBySettings removes users who have disabled in-app notifications for the event +func (ulf userlogFilter) filterUsersBySettings(ctx context.Context, users []string, event events.Event) []string { + var filteredUsers []string + var settingId string + // map type to settings key + switch event.Event.(type) { + case events.ShareCreated: + settingId = defaults.SettingUUIDProfileEventShareCreated + case events.ShareRemoved: + settingId = defaults.SettingUUIDProfileEventShareRemoved + case events.ShareExpired: + settingId = defaults.SettingUUIDProfileEventShareExpired + case events.SpaceShared: + settingId = defaults.SettingUUIDProfileEventSpaceShared + case events.SpaceUnshared: + settingId = defaults.SettingUUIDProfileEventSpaceUnshared + case events.SpaceMembershipExpired: + settingId = defaults.SettingUUIDProfileEventSpaceMembershipExpired + case events.SpaceDisabled: + settingId = defaults.SettingUUIDProfileEventSpaceDisabled + case events.SpaceDeleted: + settingId = defaults.SettingUUIDProfileEventSpaceDeleted + default: + // event that cannot be disabled + return users + } + + for _, u := range users { + enabled, err := getSetting(ctx, ulf.valueClient, u, settingId) + if err != nil { + ulf.log.Error().Err(err).Str("userId", u).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() == "in-app" { + return option.GetBoolValue(), nil + } + } + return false, errors.New("no setting found") +} diff --git a/services/userlog/pkg/service/filter_test.go b/services/userlog/pkg/service/filter_test.go new file mode 100644 index 0000000000..4e9293a69c --- /dev/null +++ b/services/userlog/pkg/service/filter_test.go @@ -0,0 +1,97 @@ +package service + +import ( + "context" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/v2/pkg/events" + "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 TestUserlogFilter_execute(t *testing.T) { + type args struct { + ctx context.Context + event events.Event + executant *user.UserId + users []string + } + tests := []struct { + name string + vc settings.ValueService + args args + want []string + }{ + {"executant", settings.MockValueService{}, args{executant: &user.UserId{OpaqueId: "executant"}, users: []string{"foo", "executant"}}, []string{"foo"}}, + {"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: []string{"foo"}, event: events.Event{Event: events.ShareCreated{}}, ctx: context.TODO()}, []string(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: []string{"foo"}, event: events.Event{Event: events.ShareCreated{}}, ctx: context.TODO()}, []string(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: []string{"foo"}, event: events.Event{Event: events.ShareCreated{}}, ctx: context.TODO()}, []string(nil)}, + {"event that cannot be disabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.BytesReceived{}}, ctx: context.TODO()}, []string{"foo"}}, + {"ShareCreated enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.ShareCreated{}}, ctx: context.TODO()}, []string{"foo"}}, + {"ShareRemoved enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.ShareRemoved{}}, ctx: context.TODO()}, []string{"foo"}}, + {"ShareExpired enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.ShareExpired{}}, ctx: context.TODO()}, []string{"foo"}}, + {"SpaceShared enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceShared{}}, ctx: context.TODO()}, []string{"foo"}}, + {"SpaceUnshared enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceUnshared{}}, ctx: context.TODO()}, []string{"foo"}}, + {"SpaceMembershipExpired enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceMembershipExpired{}}, ctx: context.TODO()}, []string{"foo"}}, + {"SpaceDisabled enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceDisabled{}}, ctx: context.TODO()}, []string{"foo"}}, + {"SpaceDeleted enabled", setupMockValueService(true), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceDeleted{}}, ctx: context.TODO()}, []string{"foo"}}, + {"ShareCreated disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.ShareCreated{}}, ctx: context.TODO()}, []string(nil)}, + {"ShareRemoved disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.ShareRemoved{}}, ctx: context.TODO()}, []string(nil)}, + {"ShareExpired disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.ShareExpired{}}, ctx: context.TODO()}, []string(nil)}, + {"SpaceShared disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceShared{}}, ctx: context.TODO()}, []string(nil)}, + {"SpaceUnshared disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceUnshared{}}, ctx: context.TODO()}, []string(nil)}, + {"SpaceMembershipExpired disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceMembershipExpired{}}, ctx: context.TODO()}, []string(nil)}, + {"SpaceDisabled disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceDisabled{}}, ctx: context.TODO()}, []string(nil)}, + {"SpaceDeleted disabled", setupMockValueService(false), args{users: []string{"foo"}, event: events.Event{Event: events.SpaceDeleted{}}, ctx: context.TODO()}, []string(nil)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ulf := userlogFilter{ + log: testLogger, + valueClient: tt.vc, + } + assert.Equal(t, tt.want, ulf.execute(tt.args.ctx, tt.args.event, tt.args.executant, tt.args.users)) + }) + } +} + +func setupMockValueService(inApp 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: "in-app", + Option: &settingsmsg.CollectionOption_BoolValue{BoolValue: inApp}, + }, + }, + }, + }, + }, + }, + }, nil + }, + } +} diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index 89efc523f7..8de8432b50 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -39,6 +39,7 @@ type UserlogService struct { tp trace.TracerProvider tracer trace.Tracer publisher events.Publisher + filter *userlogFilter } // NewUserlogService returns an EventHistory service @@ -69,6 +70,7 @@ func NewUserlogService(opts ...Option) (*UserlogService, error) { tp: o.TraceProvider, tracer: o.TraceProvider.Tracer("github.com/owncloud/ocis/services/userlog/pkg/service"), publisher: o.Stream, + filter: newUserlogFilter(o.Logger, o.ValueClient), } for _, e := range o.RegisteredEvents { @@ -190,10 +192,7 @@ func (ul *UserlogService) processEvent(event events.Event) { } // II) filter users who want to receive the event - // This step is postponed for later. - // For now each user should get all events she is eligible to receive - // ...except notifications for their own actions - users = removeExecutant(users, executant) + users = ul.filter.execute(ctx, event, executant, users) // III) store the eventID for each user for _, id := range users { @@ -461,13 +460,3 @@ func (ul *UserlogService) alterGlobalEvents(ctx context.Context, alter func(map[ Value: val, }) } - -func removeExecutant(users []string, executant *user.UserId) []string { - var usrs []string - for _, u := range users { - if u != executant.GetOpaqueId() { - usrs = append(usrs, u) - } - } - return usrs -} diff --git a/services/userlog/pkg/service/service_test.go b/services/userlog/pkg/service/service_test.go index 1203aa654b..be62e3f5b6 100644 --- a/services/userlog/pkg/service/service_test.go +++ b/services/userlog/pkg/service/service_test.go @@ -3,6 +3,7 @@ package service_test import ( "context" "encoding/json" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" "reflect" "time" @@ -77,7 +78,22 @@ var _ = Describe("UserlogService", func() { gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{User: &user.User{Id: &user.UserId{OpaqueId: "userid"}}, Status: &rpc.Status{Code: rpc.Code_CODE_OK}}, nil) gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}}, nil) vc.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: "in-app", + Option: &settingsmsg.CollectionOption_BoolValue{BoolValue: true}, + }, + }, + }, + }, + }, + }, + }, nil } ul, err = service.NewUserlogService(