Add filtering of in-app notifications based on user settings

This commit is contained in:
Bastian Beier
2024-12-17 16:56:27 +01:00
parent e22c355641
commit b7c6d5a88b
5 changed files with 225 additions and 15 deletions

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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
},
}
}

View File

@@ -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
}

View File

@@ -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(