Initial QSfera import
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/middleware"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/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")
|
||||
filteredUsers = append(filteredUsers, u)
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
settings "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
settingsmocks "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0/mocks"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("NotificationFilter", func() {
|
||||
var (
|
||||
testLogger = log.NewLogger()
|
||||
vs = &settingsmocks.ValueService{}
|
||||
)
|
||||
|
||||
setupMockValueService := func(mail bool) *settingsmocks.ValueService {
|
||||
m := &settingsmocks.ValueService{}
|
||||
m.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).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)
|
||||
return m
|
||||
}
|
||||
|
||||
Describe("execute", func() {
|
||||
It("handles connection errors", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(nil, errors.New("no connection to ValueService"))
|
||||
ulf := notificationFilter{
|
||||
log: testLogger,
|
||||
valueClient: vs,
|
||||
}
|
||||
Expect(ulf.execute(context.TODO(), []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, "bar")).To(ConsistOf(&user.User{Id: &user.UserId{OpaqueId: "foo"}}))
|
||||
})
|
||||
|
||||
It("handles no setting in ValueService response", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(&settings.GetValueResponse{}, nil)
|
||||
ulf := notificationFilter{
|
||||
log: testLogger,
|
||||
valueClient: vs,
|
||||
}
|
||||
Expect(ulf.execute(context.TODO(), []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, "bar")).To(ConsistOf(&user.User{Id: &user.UserId{OpaqueId: "foo"}}))
|
||||
})
|
||||
|
||||
It("handles nil responses", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
ulf := notificationFilter{
|
||||
log: testLogger,
|
||||
valueClient: vs,
|
||||
}
|
||||
Expect(ulf.execute(context.TODO(), []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, "bar")).To(ConsistOf(&user.User{Id: &user.UserId{OpaqueId: "foo"}}))
|
||||
})
|
||||
|
||||
It("return users when events are enabled", func() {
|
||||
vs = setupMockValueService(true)
|
||||
ulf := notificationFilter{
|
||||
log: testLogger,
|
||||
valueClient: vs,
|
||||
}
|
||||
Expect(ulf.execute(context.TODO(), []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, "bar")).To(ConsistOf(&user.User{Id: &user.UserId{OpaqueId: "foo"}}))
|
||||
})
|
||||
|
||||
It("return no users when events are disabled", func() {
|
||||
vs = setupMockValueService(false)
|
||||
ulf := notificationFilter{
|
||||
log: testLogger,
|
||||
valueClient: vs,
|
||||
}
|
||||
Expect(ulf.execute(context.TODO(), []*user.User{{Id: &user.UserId{OpaqueId: "foo"}}}, "bar")).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,162 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qsfera/server/pkg/l10n"
|
||||
ehmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/eventhistory/v0"
|
||||
"github.com/qsfera/server/services/notifications/pkg/channels"
|
||||
"github.com/qsfera/server/services/notifications/pkg/email"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s eventsNotifier) sendGroupedEmailsJob(sendEmailsEvent events.SendEmailsEvent, eventId string) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "SendEmailsEvent").
|
||||
Str("eventId", eventId).
|
||||
Logger()
|
||||
|
||||
if sendEmailsEvent.Interval != _intervalDaily && sendEmailsEvent.Interval != _intervalWeekly {
|
||||
logger.Error().Str("interval", sendEmailsEvent.Interval).Msg("unsupported email sending interval")
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := s.userEventStore.listKeys(sendEmailsEvent.Interval)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get list of keys")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
jobs := make(chan string, 10)
|
||||
go func() {
|
||||
for _, key := range keys {
|
||||
jobs <- key
|
||||
}
|
||||
close(jobs)
|
||||
}()
|
||||
|
||||
for job := range jobs {
|
||||
go s.createGroupedMail(ctx, logger, job)
|
||||
}
|
||||
}
|
||||
|
||||
func (s eventsNotifier) createGroupedMail(ctx context.Context, logger zerolog.Logger, key string) {
|
||||
userEvents, err := s.userEventStore.pop(ctx, key)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("key", key).Msg("could not pop user events")
|
||||
return
|
||||
}
|
||||
|
||||
var mts []email.MessageTemplate
|
||||
var mtsVars []map[string]string
|
||||
locale := l10n.MustGetUserLocale(ctx, userEvents.User.GetId().GetOpaqueId(), "", s.valueService)
|
||||
|
||||
for _, e := range userEvents.Events {
|
||||
switch te := s.unwrapEvent(logger, e).(type) {
|
||||
case events.SpaceShared:
|
||||
logger := logger.With().
|
||||
Str("event", "SpaceShared").
|
||||
Str("eventId", te.ID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
executant, spaceName, shareLink, _, err := s.prepareSpaceShared(logger, te)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for grouped email")
|
||||
continue
|
||||
}
|
||||
|
||||
mts = append(mts, email.SharedSpace)
|
||||
mtsVars = append(mtsVars, map[string]string{
|
||||
"SpaceSharer": executant.GetDisplayName(),
|
||||
"SpaceName": spaceName,
|
||||
"ShareLink": shareLink,
|
||||
})
|
||||
case events.SpaceUnshared:
|
||||
logger := logger.With().
|
||||
Str("event", "SpaceUnshared").
|
||||
Str("eventId", te.ID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
executant, spaceName, shareLink, _, err := s.prepareSpaceUnshared(logger, te)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for grouped email")
|
||||
continue
|
||||
}
|
||||
mts = append(mts, email.UnsharedSpace)
|
||||
mtsVars = append(mtsVars, map[string]string{
|
||||
"SpaceSharer": executant.GetDisplayName(),
|
||||
"SpaceName": spaceName,
|
||||
"ShareLink": shareLink,
|
||||
})
|
||||
case events.SpaceMembershipExpired:
|
||||
mts = append(mts, email.MembershipExpired)
|
||||
mtsVars = append(mtsVars, map[string]string{
|
||||
"SpaceName": te.SpaceName,
|
||||
"ExpiredAt": te.ExpiredAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
case events.ShareCreated:
|
||||
logger := logger.With().
|
||||
Str("event", "ShareCreated").
|
||||
Str("eventId", te.ItemID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
owner, shareFolder, shareLink, _, err := s.prepareShareCreated(logger, te)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for grouped email")
|
||||
continue
|
||||
}
|
||||
mts = append(mts, email.ShareCreated)
|
||||
mtsVars = append(mtsVars, map[string]string{
|
||||
"ShareSharer": owner.GetDisplayName(),
|
||||
"ShareFolder": shareFolder,
|
||||
"ShareLink": shareLink,
|
||||
})
|
||||
case events.ShareExpired:
|
||||
logger := logger.With().
|
||||
Str("event", "ShareCreated").
|
||||
Str("eventId", te.ItemID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
shareFolder, _, err := s.prepareShareExpired(logger, te)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for grouped email")
|
||||
continue
|
||||
}
|
||||
mts = append(mts, email.ShareExpired)
|
||||
mtsVars = append(mtsVars, map[string]string{
|
||||
"ShareFolder": shareFolder,
|
||||
"ExpiredAt": te.ExpiredAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rendered, err := email.RenderGroupedEmailTemplate(email.Grouped, map[string]string{
|
||||
"DisplayName": userEvents.User.GetDisplayName(),
|
||||
}, locale, s.defaultLanguage, s.emailTemplatePath, s.translationPath, mts, mtsVars)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not render template")
|
||||
return
|
||||
}
|
||||
rendered.Sender = s.defaultEmailSender
|
||||
rendered.Recipient = []string{userEvents.User.GetMail()}
|
||||
s.send(ctx, []*channels.Message{rendered})
|
||||
}
|
||||
|
||||
func (s eventsNotifier) unwrapEvent(logger zerolog.Logger, e *ehmsg.Event) any {
|
||||
etype, ok := s.registeredEvents[e.GetType()]
|
||||
if !ok {
|
||||
logger.Error().Str("eventId", e.GetId()).Str("eventType", e.GetType()).Msg("event not registered")
|
||||
return nil
|
||||
}
|
||||
|
||||
ue, err := etype.Unmarshal(e.GetEvent())
|
||||
if err != nil {
|
||||
logger.Error().Str("eventId", e.GetId()).Str("eventType", e.GetType()).Msg("failed to umarshal event")
|
||||
return nil
|
||||
}
|
||||
|
||||
return ue
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qsfera/server/pkg/registry"
|
||||
mRegistry "go-micro.dev/v4/registry"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func init() {
|
||||
r := registry.GetRegistry(registry.Inmemory())
|
||||
service := registry.BuildGRPCService("qsfera.api.gateway", "", "", "")
|
||||
service.Nodes = []*mRegistry.Node{{
|
||||
Address: "any",
|
||||
}}
|
||||
|
||||
_ = r.Register(service)
|
||||
}
|
||||
|
||||
func TestNotifications(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Notification Suite")
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
v0 "github.com/qsfera/server/protogen/gen/qsfera/messages/eventhistory/v0"
|
||||
ehsvc "github.com/qsfera/server/protogen/gen/qsfera/services/eventhistory/v0"
|
||||
"github.com/pkg/errors"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
type userEventStore struct {
|
||||
log log.Logger
|
||||
store store.Store
|
||||
historyClient ehsvc.EventHistoryService
|
||||
}
|
||||
|
||||
type userEventIds struct {
|
||||
User *user.User `json:"user"`
|
||||
EventIds []string `json:"event_ids"`
|
||||
}
|
||||
|
||||
type userEvents struct {
|
||||
User *user.User
|
||||
Events []*v0.Event
|
||||
}
|
||||
|
||||
const (
|
||||
_intervalDaily = "daily"
|
||||
_intervalWeekly = "weekly"
|
||||
)
|
||||
|
||||
func newUserEventStore(l log.Logger, s store.Store, hc ehsvc.EventHistoryService) *userEventStore {
|
||||
return &userEventStore{log: l, store: s, historyClient: hc}
|
||||
}
|
||||
|
||||
func (s *userEventStore) persist(interval string, eventId string, users []*user.User) []*user.User {
|
||||
var errorUsers []*user.User
|
||||
for _, u := range users {
|
||||
key := interval + u.Id.OpaqueId
|
||||
|
||||
// Note: This is not thread safe and can result in missing events
|
||||
records, err := s.store.Read(key)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
s.log.Error().Err(err).Str("eventId", eventId).Str("userId", u.Id.OpaqueId).Msg("cannot read record")
|
||||
errorUsers = append(errorUsers, u)
|
||||
continue
|
||||
}
|
||||
var record userEventIds
|
||||
if len(records) == 0 {
|
||||
record = userEventIds{}
|
||||
} else {
|
||||
if err = json.Unmarshal(records[0].Value, &record); err != nil {
|
||||
s.log.Warn().Err(err).Str("eventId", eventId).Str("userId", u.Id.OpaqueId).Msg("cannot unmarshal json")
|
||||
errorUsers = append(errorUsers, u)
|
||||
continue
|
||||
}
|
||||
}
|
||||
record.User = u
|
||||
record.EventIds = append(record.EventIds, eventId)
|
||||
b, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
s.log.Warn().Err(err).Str("eventId", eventId).Str("userId", u.Id.OpaqueId).Msg("cannot marshal record")
|
||||
errorUsers = append(errorUsers, u)
|
||||
continue
|
||||
}
|
||||
err = s.store.Write(&store.Record{
|
||||
Key: key,
|
||||
Value: b,
|
||||
})
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Str("eventId", eventId).Str("userId", u.Id.OpaqueId).Msg("cannot write record")
|
||||
errorUsers = append(errorUsers, u)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
return errorUsers
|
||||
}
|
||||
|
||||
func (s *userEventStore) listKeys(interval string) ([]string, error) {
|
||||
return s.store.List(store.ListPrefix(interval))
|
||||
}
|
||||
|
||||
func (s *userEventStore) pop(ctx context.Context, key string) (*userEvents, error) {
|
||||
records, err := s.store.Read(key)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, errors.New("cannot get records")
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, errors.New("no records found")
|
||||
}
|
||||
var record userEventIds
|
||||
err = json.Unmarshal(records[0].Value, &record)
|
||||
if err != nil {
|
||||
s.log.Warn().Err(err).Str("key", key).Msg("cannot unmarshal json")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.historyClient.GetEvents(ctx, &ehsvc.GetEventsRequest{Ids: record.EventIds})
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Strs("eventIds", record.EventIds).Msg("cannot get events")
|
||||
return nil, err
|
||||
}
|
||||
err = s.store.Delete(key)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Strs("eventIds", record.EventIds).Msg("cannot delete records")
|
||||
return nil, err
|
||||
}
|
||||
return &userEvents{
|
||||
User: record.User,
|
||||
Events: res.GetEvents(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qsfera/server/services/notifications/pkg/channels"
|
||||
"github.com/qsfera/server/services/notifications/pkg/email"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s eventsNotifier) handleScienceMeshInviteTokenGenerated(e events.ScienceMeshInviteTokenGenerated) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "ScienceMeshInviteTokenGenerated").
|
||||
Logger()
|
||||
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err := utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Could not impersonate service user")
|
||||
return
|
||||
}
|
||||
|
||||
owner, err := utils.GetUserNoGroups(ctx, e.Sharer, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("unable to get user")
|
||||
return
|
||||
}
|
||||
|
||||
msgENV := map[string]string{
|
||||
"ShareSharer": owner.GetDisplayName(),
|
||||
"ShareSharerMail": owner.GetMail(),
|
||||
"ShareLink": e.InviteLink,
|
||||
"Token": e.Token,
|
||||
"ProviderDomain": owner.GetId().GetIdp(),
|
||||
"RecipientMail": e.RecipientMail,
|
||||
}
|
||||
|
||||
// validate the message, we only need recipient mail at the moment,
|
||||
// event that is optional when the event got triggered...
|
||||
// this means if we get a validation error, we can't send the message and skip it
|
||||
{
|
||||
validationEnv := make(map[string]any, len(msgENV))
|
||||
for k, v := range msgENV {
|
||||
validationEnv[k] = v
|
||||
}
|
||||
if errs := validate.ValidateMap(validationEnv,
|
||||
map[string]any{
|
||||
"RecipientMail": "required,email", // only recipient mail is required to send the message
|
||||
}); len(errs) > 0 {
|
||||
return // no mail, no message
|
||||
}
|
||||
}
|
||||
|
||||
emailTpl := email.ScienceMeshInviteTokenGenerated
|
||||
if e.InviteLink == "" {
|
||||
emailTpl = email.ScienceMeshInviteTokenGeneratedWithoutShareLink
|
||||
}
|
||||
|
||||
msg, err := email.RenderEmailTemplate(
|
||||
emailTpl,
|
||||
s.defaultLanguage, // fixMe: the recipient is unknown, should it be the defaultLocale?,
|
||||
s.defaultLanguage, // fixMe: the defaultLocale is not set by default, shouldn't it be?,
|
||||
s.emailTemplatePath,
|
||||
s.translationPath,
|
||||
msgENV,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("building the message has failed")
|
||||
return
|
||||
}
|
||||
|
||||
msg.Sender = owner.GetDisplayName()
|
||||
msg.Recipient = []string{e.RecipientMail}
|
||||
|
||||
s.send(ctx, []*channels.Message{msg})
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
ehsvc "github.com/qsfera/server/protogen/gen/qsfera/services/eventhistory/v0"
|
||||
"go-micro.dev/v4/store"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
"github.com/qsfera/server/pkg/l10n"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/middleware"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
"github.com/qsfera/server/services/notifications/pkg/channels"
|
||||
"github.com/qsfera/server/services/notifications/pkg/email"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
)
|
||||
|
||||
// validate is the package level validator instance
|
||||
var validate *validator.Validate
|
||||
|
||||
func init() {
|
||||
validate = validator.New()
|
||||
}
|
||||
|
||||
// Service should be named `Runner`
|
||||
type Service interface {
|
||||
Run() error
|
||||
Close()
|
||||
}
|
||||
|
||||
// NewEventsNotifier provides a new eventsNotifier
|
||||
func NewEventsNotifier(
|
||||
events <-chan events.Event,
|
||||
channel channels.Channel,
|
||||
logger log.Logger,
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient],
|
||||
valueService settingssvc.ValueService,
|
||||
serviceAccountID, serviceAccountSecret, emailTemplatePath, defaultLanguage, qsferaURL, translationPath, emailSender string,
|
||||
store store.Store,
|
||||
historyClient ehsvc.EventHistoryService,
|
||||
registeredEvents map[string]events.Unmarshaller) Service {
|
||||
|
||||
return eventsNotifier{
|
||||
logger: logger,
|
||||
channel: channel,
|
||||
events: events,
|
||||
gatewaySelector: gatewaySelector,
|
||||
valueService: valueService,
|
||||
serviceAccountID: serviceAccountID,
|
||||
serviceAccountSecret: serviceAccountSecret,
|
||||
emailTemplatePath: emailTemplatePath,
|
||||
defaultLanguage: defaultLanguage,
|
||||
defaultEmailSender: emailSender,
|
||||
qsferaURL: qsferaURL,
|
||||
translationPath: translationPath,
|
||||
filter: newNotificationFilter(logger, valueService),
|
||||
splitter: newIntervalSplitter(logger, valueService),
|
||||
userEventStore: newUserEventStore(logger, store, historyClient),
|
||||
registeredEvents: registeredEvents,
|
||||
stopCh: make(chan struct{}, 1),
|
||||
stopped: new(atomic.Bool),
|
||||
}
|
||||
}
|
||||
|
||||
type eventsNotifier struct {
|
||||
logger log.Logger
|
||||
channel channels.Channel
|
||||
events <-chan events.Event
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
valueService settingssvc.ValueService
|
||||
emailTemplatePath string
|
||||
translationPath string
|
||||
defaultLanguage string
|
||||
defaultEmailSender string
|
||||
qsferaURL string
|
||||
serviceAccountID string
|
||||
serviceAccountSecret string
|
||||
filter *notificationFilter
|
||||
splitter *intervalSplitter
|
||||
userEventStore *userEventStore
|
||||
registeredEvents map[string]events.Unmarshaller
|
||||
stopCh chan struct{}
|
||||
stopped *atomic.Bool
|
||||
}
|
||||
|
||||
func (s eventsNotifier) Run() error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
s.logger.Debug().
|
||||
Msg("eventsNotifier started")
|
||||
EventLoop:
|
||||
for {
|
||||
select {
|
||||
case evt, ok := <-s.events:
|
||||
if !ok {
|
||||
break EventLoop
|
||||
}
|
||||
// TODO: needs to be replaced with a worker pool
|
||||
wg.Go(func() {
|
||||
|
||||
switch e := evt.Event.(type) {
|
||||
case events.SpaceShared:
|
||||
s.handleSpaceShared(e, evt.ID)
|
||||
case events.SpaceUnshared:
|
||||
s.handleSpaceUnshared(e, evt.ID)
|
||||
case events.SpaceMembershipExpired:
|
||||
s.handleSpaceMembershipExpired(e, evt.ID)
|
||||
case events.ShareCreated:
|
||||
s.handleShareCreated(e, evt.ID)
|
||||
case events.ShareExpired:
|
||||
s.handleShareExpired(e, evt.ID)
|
||||
case events.ScienceMeshInviteTokenGenerated:
|
||||
s.handleScienceMeshInviteTokenGenerated(e)
|
||||
case events.SendEmailsEvent:
|
||||
s.sendGroupedEmailsJob(e, evt.ID)
|
||||
}
|
||||
})
|
||||
|
||||
if s.stopped.Load() {
|
||||
break EventLoop
|
||||
}
|
||||
case <-s.stopCh:
|
||||
s.logger.Debug().
|
||||
Msg("eventsNotifier stopped")
|
||||
break EventLoop
|
||||
}
|
||||
}
|
||||
// wait until all the goroutines processing events have finished
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s eventsNotifier) Close() {
|
||||
if s.stopped.CompareAndSwap(false, true) {
|
||||
close(s.stopCh)
|
||||
}
|
||||
}
|
||||
|
||||
func (s eventsNotifier) render(ctx context.Context, template email.MessageTemplate,
|
||||
granteeFieldName string, fields map[string]string, granteeList []*user.User, sender string) ([]*channels.Message, error) {
|
||||
// Render the Email Template for each user
|
||||
messageList := make([]*channels.Message, len(granteeList))
|
||||
for i, usr := range granteeList {
|
||||
locale := l10n.MustGetUserLocale(ctx, usr.GetId().GetOpaqueId(), "", s.valueService)
|
||||
fields[granteeFieldName] = usr.GetDisplayName()
|
||||
|
||||
rendered, err := email.RenderEmailTemplate(template, locale, s.defaultLanguage, s.emailTemplatePath, s.translationPath, fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rendered.Sender = sender
|
||||
rendered.Recipient = []string{usr.GetMail()}
|
||||
messageList[i] = rendered
|
||||
}
|
||||
return messageList, nil
|
||||
}
|
||||
|
||||
func (s eventsNotifier) send(ctx context.Context, emails []*channels.Message) {
|
||||
for _, r := range emails {
|
||||
err := s.channel.SendMessage(ctx, r)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Str("event", "SendEmail").Msg("failed to send a message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s eventsNotifier) ensureGranteeList(ctx context.Context, executant, u *user.UserId, g *group.GroupId) []*user.User {
|
||||
granteeList, err := s.getGranteeList(ctx, executant, u, g)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Str("event", "ensureGranteeList").Msg("Could not get grantee list")
|
||||
return nil
|
||||
} else if len(granteeList) == 0 {
|
||||
return nil
|
||||
}
|
||||
return granteeList
|
||||
}
|
||||
|
||||
func (s eventsNotifier) getGranteeList(ctx context.Context, executant, u *user.UserId, g *group.GroupId) ([]*user.User, error) {
|
||||
switch {
|
||||
case u != nil:
|
||||
if s.disableEmails(ctx, u) {
|
||||
return nil, nil
|
||||
}
|
||||
usr, err := s.getUser(ctx, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(usr.GetMail()) == "" {
|
||||
s.logger.Debug().Str("event", "getGranteeList").Msgf("User %s has no email, skipped", usr.GetUsername())
|
||||
return nil, nil
|
||||
}
|
||||
return []*user.User{usr}, nil
|
||||
case g != nil:
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := gatewayClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: g})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, errors.New("could not get group")
|
||||
}
|
||||
|
||||
userList := make([]*user.User, 0, len(res.GetGroup().GetMembers()))
|
||||
for _, userID := range res.GetGroup().GetMembers() {
|
||||
// don't add the executant
|
||||
if userID.GetOpaqueId() == executant.GetOpaqueId() {
|
||||
continue
|
||||
}
|
||||
// don't add users who opted out
|
||||
if s.disableEmails(ctx, userID) {
|
||||
continue
|
||||
}
|
||||
usr, err := s.getUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(usr.GetMail()) == "" {
|
||||
s.logger.Debug().Str("event", "getGranteeList").Msgf("User %s has no email, skipped", usr.GetUsername())
|
||||
continue
|
||||
}
|
||||
userList = append(userList, usr)
|
||||
}
|
||||
return userList, nil
|
||||
default:
|
||||
return nil, errors.New("need at least one non-nil grantee")
|
||||
}
|
||||
}
|
||||
|
||||
func (s eventsNotifier) getUser(ctx context.Context, u *user.UserId) (*user.User, error) {
|
||||
if u == nil {
|
||||
return nil, errors.New("need at least one non-nil grantee")
|
||||
}
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := gatewayClient.GetUser(ctx, &user.GetUserRequest{UserId: u})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
|
||||
}
|
||||
return r.GetUser(), nil
|
||||
}
|
||||
|
||||
func (s eventsNotifier) disableEmails(ctx context.Context, u *user.UserId) bool {
|
||||
granteeCtx := metadata.Set(ctx, middleware.AccountID, u.OpaqueId)
|
||||
if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx,
|
||||
&settingssvc.GetValueByUniqueIdentifiersRequest{
|
||||
AccountUuid: u.OpaqueId,
|
||||
SettingId: defaults.SettingUUIDProfileDisableNotifications,
|
||||
},
|
||||
); err == nil {
|
||||
return resp.GetValue().GetValue().GetBoolValue()
|
||||
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s eventsNotifier) getResourceInfo(ctx context.Context, resourceID *provider.ResourceId, fieldmask *fieldmaskpb.FieldMask) (*provider.ResourceInfo, error) {
|
||||
// TODO: maybe cache this stat to reduce storage iops
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md, err := gatewayClient.Stat(ctx, &provider.StatRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: resourceID,
|
||||
},
|
||||
FieldMask: fieldmask,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if md.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, fmt.Errorf("could not resource info: %s", md.Status.Message)
|
||||
}
|
||||
return md.GetInfo(), nil
|
||||
}
|
||||
|
||||
// TODO: this function is a backport for go1.19 url.JoinPath, upon go bump, replace this
|
||||
func urlJoinPath(base string, elements ...string) (string, error) {
|
||||
u, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.Path = path.Join(append([]string{u.Path}, elements...)...)
|
||||
return u.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/store"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
settingsmocks "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0/mocks"
|
||||
"github.com/qsfera/server/services/graph/pkg/config/defaults"
|
||||
"github.com/qsfera/server/services/notifications/pkg/channels"
|
||||
"github.com/qsfera/server/services/notifications/pkg/service"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ = Describe("Notifications", func() {
|
||||
var (
|
||||
gatewayClient *cs3mocks.GatewayAPIClient
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
vs *settingsmocks.ValueService
|
||||
sharer = &user.User{
|
||||
Id: &user.UserId{
|
||||
OpaqueId: "sharer",
|
||||
},
|
||||
Mail: "sharer@qsfera.eu",
|
||||
DisplayName: "Dr. S. Harer",
|
||||
}
|
||||
sharee = &user.User{
|
||||
Id: &user.UserId{
|
||||
OpaqueId: "sharee",
|
||||
},
|
||||
Mail: "sharee@qsfera.eu",
|
||||
DisplayName: "Eric Expireling",
|
||||
}
|
||||
resourceid = &provider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
SpaceId: "spaceid",
|
||||
OpaqueId: "itemid",
|
||||
}
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
pool.RemoveSelector("GatewaySelector" + "qsfera.api.gateway")
|
||||
gatewayClient = &cs3mocks.GatewayAPIClient{}
|
||||
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
|
||||
"GatewaySelector",
|
||||
"qsfera.api.gateway",
|
||||
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
|
||||
return gatewayClient
|
||||
},
|
||||
)
|
||||
|
||||
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once()
|
||||
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once()
|
||||
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
|
||||
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 = &settingsmocks.ValueService{}
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).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)
|
||||
})
|
||||
|
||||
DescribeTable("Sending userEventIds",
|
||||
func(tc testChannel, ev events.Event) {
|
||||
cfg := defaults.FullDefaultConfig()
|
||||
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
|
||||
ch := make(chan events.Event)
|
||||
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "",
|
||||
"", "", "", "", "", "",
|
||||
store.Create(), nil, nil)
|
||||
go evts.Run()
|
||||
|
||||
ch <- ev
|
||||
select {
|
||||
case <-tc.done:
|
||||
// finished
|
||||
case <-time.Tick(3 * time.Second):
|
||||
Fail("timeout waiting for notification")
|
||||
}
|
||||
},
|
||||
|
||||
Entry("Share Created", testChannel{
|
||||
expectedReceipients: []string{sharee.GetMail()},
|
||||
expectedSubject: "Dr. S. Harer shared 'secrets of the board' with you",
|
||||
expectedTextBody: `Hello Eric Expireling
|
||||
|
||||
Dr. S. Harer has shared "secrets of the board" with you.
|
||||
|
||||
Click here to view it: files/shares/with-me
|
||||
|
||||
|
||||
---
|
||||
КуСфера - a safe home for all your data
|
||||
https://qsfera.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.ShareCreated{
|
||||
Sharer: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
CTime: utils.TimeToTS(time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC)),
|
||||
ItemID: resourceid,
|
||||
},
|
||||
}),
|
||||
|
||||
Entry("Share Expired", testChannel{
|
||||
expectedReceipients: []string{sharee.GetMail()},
|
||||
expectedSubject: "Share to 'secrets of the board' expired at 2023-04-17 16:42:00",
|
||||
expectedTextBody: `Hello Eric Expireling,
|
||||
|
||||
Your share to secrets of the board has expired at 2023-04-17 16:42:00
|
||||
|
||||
Even though this share has been revoked you still might have access through other shares and/or space memberships.
|
||||
|
||||
|
||||
---
|
||||
КуСфера - a safe home for all your data
|
||||
https://qsfera.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.ShareExpired{
|
||||
ShareOwner: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
ExpiredAt: time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC),
|
||||
ItemID: resourceid,
|
||||
},
|
||||
}),
|
||||
|
||||
Entry("Added to Space", testChannel{
|
||||
expectedReceipients: []string{sharee.GetMail()},
|
||||
expectedSubject: "Dr. S. Harer invited you to join secret space",
|
||||
expectedTextBody: `Hello Eric Expireling,
|
||||
|
||||
Dr. S. Harer has invited you to join "secret space".
|
||||
|
||||
Click here to view it: f/spaceid
|
||||
|
||||
|
||||
---
|
||||
КуСфера - a safe home for all your data
|
||||
https://qsfera.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceShared{
|
||||
Executant: sharer.GetId(),
|
||||
Creator: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
ID: &provider.StorageSpaceId{OpaqueId: "spaceid"},
|
||||
},
|
||||
}),
|
||||
|
||||
Entry("Removed from Space", testChannel{
|
||||
expectedReceipients: []string{sharee.GetMail()},
|
||||
expectedSubject: "Dr. S. Harer removed you from secret space",
|
||||
expectedTextBody: `Hello Eric Expireling,
|
||||
|
||||
Dr. S. Harer has removed you from "secret space".
|
||||
|
||||
You might still have access through your other groups or direct membership.
|
||||
|
||||
Click here to check it: f/spaceid
|
||||
|
||||
|
||||
---
|
||||
КуСфера - a safe home for all your data
|
||||
https://qsfera.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceUnshared{
|
||||
Executant: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
ID: &provider.StorageSpaceId{OpaqueId: "spaceid"},
|
||||
},
|
||||
}),
|
||||
|
||||
Entry("Space Expired", testChannel{
|
||||
expectedReceipients: []string{sharee.GetMail()},
|
||||
expectedSubject: "Membership of 'secret space' expired at 2023-04-17 16:42:00",
|
||||
expectedTextBody: `Hello Eric Expireling,
|
||||
|
||||
Your membership of space secret space has expired at 2023-04-17 16:42:00
|
||||
|
||||
Even though this membership has expired you still might have access through other shares and/or space memberships
|
||||
|
||||
|
||||
---
|
||||
КуСфера - a safe home for all your data
|
||||
https://qsfera.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceMembershipExpired{
|
||||
SpaceOwner: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: "spaceid"},
|
||||
SpaceName: "secret space",
|
||||
ExpiredAt: time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC),
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
var _ = Describe("Notifications X-Site Scripting", func() {
|
||||
var (
|
||||
gatewayClient *cs3mocks.GatewayAPIClient
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
vs *settingsmocks.ValueService
|
||||
sharer = &user.User{
|
||||
Id: &user.UserId{
|
||||
OpaqueId: "sharer",
|
||||
},
|
||||
Mail: "sharer@qsfera.eu",
|
||||
DisplayName: "Dr. O'reilly",
|
||||
}
|
||||
sharee = &user.User{
|
||||
Id: &user.UserId{
|
||||
OpaqueId: "sharee",
|
||||
},
|
||||
Mail: "sharee@qsfera.eu",
|
||||
DisplayName: "<script>alert('Eric Expireling');</script>",
|
||||
}
|
||||
resourceid = &provider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
SpaceId: "spaceid",
|
||||
OpaqueId: "itemid",
|
||||
}
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
pool.RemoveSelector("GatewaySelector" + "qsfera.api.gateway")
|
||||
gatewayClient = &cs3mocks.GatewayAPIClient{}
|
||||
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
|
||||
"GatewaySelector",
|
||||
"qsfera.api.gateway",
|
||||
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
|
||||
return gatewayClient
|
||||
},
|
||||
)
|
||||
|
||||
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once()
|
||||
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once()
|
||||
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
|
||||
Status: &rpc.Status{Code: rpc.Code_CODE_OK},
|
||||
Info: &provider.ResourceInfo{
|
||||
Name: "<script>alert('secrets of the board');</script>",
|
||||
Space: &provider.StorageSpace{Name: "<script>alert('secret space');</script>"}},
|
||||
}, nil)
|
||||
vs = &settingsmocks.ValueService{}
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).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)
|
||||
})
|
||||
|
||||
DescribeTable("Sending userEventIds",
|
||||
func(tc testChannel, ev events.Event) {
|
||||
cfg := defaults.FullDefaultConfig()
|
||||
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
|
||||
ch := make(chan events.Event)
|
||||
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "",
|
||||
"", "", "", "", "", "",
|
||||
store.Create(), nil, nil)
|
||||
go evts.Run()
|
||||
|
||||
ch <- ev
|
||||
select {
|
||||
case <-tc.done:
|
||||
// finished
|
||||
case <-time.Tick(3 * time.Second):
|
||||
Fail("timeout waiting for notification")
|
||||
}
|
||||
},
|
||||
|
||||
Entry("Share Created", testChannel{
|
||||
expectedReceipients: []string{sharee.GetMail()},
|
||||
expectedSubject: "Dr. O'reilly shared '<script>alert('secrets of the board');</script>' with you",
|
||||
expectedTextBody: `Hello <script>alert('Eric Expireling');</script>
|
||||
|
||||
Dr. O'reilly has shared "<script>alert('secrets of the board');</script>" with you.
|
||||
|
||||
Click here to view it: files/shares/with-me
|
||||
|
||||
|
||||
---
|
||||
КуСфера - a safe home for all your data
|
||||
https://qsfera.eu
|
||||
`,
|
||||
expectedHTMLBody: `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="600px">
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
Hello <script>alert('Eric Expireling');</script>
|
||||
<br><br>
|
||||
Dr. O'reilly has shared "<script>alert('secrets of the board');</script>" with you.
|
||||
<br><br>
|
||||
Click here to view it: <a href="files/shares/with-me">files/shares/with-me</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
<footer>
|
||||
<br>
|
||||
<br>
|
||||
--- <br>
|
||||
КуСфера - a safe home for all your data<br>
|
||||
<a href="https://qsfera.eu">https://qsfera.eu</a>
|
||||
</footer>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.ShareCreated{
|
||||
Sharer: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
CTime: utils.TimeToTS(time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC)),
|
||||
ItemID: resourceid,
|
||||
},
|
||||
}),
|
||||
|
||||
Entry("Added to Space", testChannel{
|
||||
expectedReceipients: []string{sharee.GetMail()},
|
||||
expectedSubject: "Dr. O'reilly invited you to join <script>alert('secret space');</script>",
|
||||
expectedTextBody: `Hello <script>alert('Eric Expireling');</script>,
|
||||
|
||||
Dr. O'reilly has invited you to join "<script>alert('secret space');</script>".
|
||||
|
||||
Click here to view it: f/spaceid
|
||||
|
||||
|
||||
---
|
||||
КуСфера - a safe home for all your data
|
||||
https://qsfera.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
expectedHTMLBody: `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="600px">
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
Hello <script>alert('Eric Expireling');</script>,
|
||||
<br><br>
|
||||
Dr. O'reilly has invited you to join "<script>alert('secret space');</script>".
|
||||
<br><br>
|
||||
Click here to view it: <a href="f/spaceid">f/spaceid</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
<footer>
|
||||
<br>
|
||||
<br>
|
||||
--- <br>
|
||||
КуСфера - a safe home for all your data<br>
|
||||
<a href="https://qsfera.eu">https://qsfera.eu</a>
|
||||
</footer>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceShared{
|
||||
Executant: sharer.GetId(),
|
||||
Creator: sharer.GetId(),
|
||||
GranteeUserID: sharee.GetId(),
|
||||
ID: &provider.StorageSpaceId{OpaqueId: "spaceid"},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// NOTE: This is explictitly not testing the message itself. Should we?
|
||||
type testChannel struct {
|
||||
expectedReceipients []string
|
||||
expectedSubject string
|
||||
expectedTextBody string
|
||||
expectedHTMLBody string
|
||||
expectedSender string
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (tc testChannel) SendMessage(ctx context.Context, m *channels.Message) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
Expect(tc.expectedReceipients).To(Equal(m.Recipient))
|
||||
Expect(tc.expectedSubject).To(Equal(m.Subject))
|
||||
Expect(tc.expectedTextBody).To(Equal(m.TextBody))
|
||||
Expect(tc.expectedSender).To(Equal(m.Sender))
|
||||
if tc.expectedHTMLBody != "" {
|
||||
Expect(tc.expectedHTMLBody).To(Equal(m.HTMLBody))
|
||||
}
|
||||
tc.done <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/services/notifications/pkg/email"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
func (s eventsNotifier) handleShareCreated(e events.ShareCreated, eventId string) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "ShareCreated").
|
||||
Str("itemid", e.ItemID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
owner, shareFolder, shareLink, ctx, err := s.prepareShareCreated(logger, e)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for email")
|
||||
return
|
||||
}
|
||||
|
||||
granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
|
||||
filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventShareCreated)
|
||||
|
||||
recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...)
|
||||
if recipientsInstant == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sharerDisplayName := owner.GetDisplayName()
|
||||
emails, err := s.render(ctx, email.ShareCreated,
|
||||
"ShareGrantee",
|
||||
map[string]string{
|
||||
"ShareSharer": sharerDisplayName,
|
||||
"ShareFolder": shareFolder,
|
||||
"ShareLink": shareLink,
|
||||
}, recipientsInstant, sharerDisplayName)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get render the email")
|
||||
return
|
||||
}
|
||||
s.send(ctx, emails)
|
||||
}
|
||||
|
||||
func (s eventsNotifier) prepareShareCreated(logger zerolog.Logger, e events.ShareCreated) (owner *user.User, shareFolder, shareLink string, ctx context.Context, err error) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return owner, shareFolder, shareLink, ctx, err
|
||||
}
|
||||
|
||||
ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get service user context")
|
||||
return owner, shareFolder, shareLink, ctx, err
|
||||
}
|
||||
|
||||
resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not stat resource")
|
||||
return owner, shareFolder, shareLink, ctx, err
|
||||
}
|
||||
shareFolder = resourceInfo.Name
|
||||
|
||||
shareLink, err = urlJoinPath(s.qsferaURL, "files/shares/with-me")
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not create link to the share")
|
||||
return owner, shareFolder, shareLink, ctx, err
|
||||
}
|
||||
|
||||
owner, err = utils.GetUserNoGroups(ctx, e.Sharer, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not get user")
|
||||
return owner, shareFolder, shareLink, ctx, err
|
||||
}
|
||||
|
||||
return owner, shareFolder, shareLink, ctx, err
|
||||
}
|
||||
|
||||
func (s eventsNotifier) handleShareExpired(e events.ShareExpired, eventId string) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "ShareExpired").
|
||||
Str("itemid", e.ItemID.GetOpaqueId()).
|
||||
Logger()
|
||||
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return
|
||||
}
|
||||
|
||||
shareFolder, ctx, err := s.prepareShareExpired(logger, e)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for email")
|
||||
return
|
||||
}
|
||||
|
||||
owner, err := utils.GetUserNoGroups(ctx, e.ShareOwner, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Could not get user")
|
||||
return
|
||||
}
|
||||
|
||||
granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
|
||||
filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventShareExpired)
|
||||
|
||||
recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...)
|
||||
if recipientsInstant == nil {
|
||||
return
|
||||
}
|
||||
|
||||
emails, err := s.render(ctx, email.ShareExpired,
|
||||
"ShareGrantee",
|
||||
map[string]string{
|
||||
"ShareFolder": shareFolder,
|
||||
"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
|
||||
}, recipientsInstant, owner.GetDisplayName())
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get render the email")
|
||||
return
|
||||
}
|
||||
s.send(ctx, emails)
|
||||
}
|
||||
|
||||
func (s eventsNotifier) prepareShareExpired(logger zerolog.Logger, e events.ShareExpired) (shareFolder string, ctx context.Context, err error) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return shareFolder, ctx, err
|
||||
}
|
||||
|
||||
ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get service user context")
|
||||
return shareFolder, ctx, err
|
||||
}
|
||||
|
||||
resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not stat resource")
|
||||
return shareFolder, ctx, err
|
||||
}
|
||||
shareFolder = resourceInfo.GetName()
|
||||
|
||||
return shareFolder, ctx, err
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/services/notifications/pkg/email"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s eventsNotifier) handleSpaceShared(e events.SpaceShared, eventId string) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "SpaceShared").
|
||||
Str("itemid", e.ID.OpaqueId).
|
||||
Logger()
|
||||
executant, spaceName, shareLink, ctx, err := s.prepareSpaceShared(logger, e)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for email")
|
||||
return
|
||||
}
|
||||
|
||||
granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
|
||||
filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceShared)
|
||||
|
||||
recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...)
|
||||
if recipientsInstant == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sharerDisplayName := executant.GetDisplayName()
|
||||
emails, err := s.render(ctx, email.SharedSpace,
|
||||
"SpaceGrantee",
|
||||
map[string]string{
|
||||
"SpaceSharer": sharerDisplayName,
|
||||
"SpaceName": spaceName,
|
||||
"ShareLink": shareLink,
|
||||
}, recipientsInstant, sharerDisplayName)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get render the email")
|
||||
return
|
||||
}
|
||||
s.send(ctx, emails)
|
||||
}
|
||||
|
||||
func (s eventsNotifier) prepareSpaceShared(logger zerolog.Logger, e events.SpaceShared) (executant *user.User, spaceName, shareLink string, ctx context.Context, err error) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get service user context")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
executant, err = utils.GetUserNoGroups(ctx, e.Executant, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not get user")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
resourceID, err := storagespace.ParseID(e.ID.OpaqueId)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not parse resourceid from ItemID ")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
resourceInfo, err := s.getResourceInfo(ctx, &resourceID, nil)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not get space info")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
spaceName = resourceInfo.GetSpace().GetName()
|
||||
|
||||
shareLink, err = urlJoinPath(s.qsferaURL, "f", e.ID.OpaqueId)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not create link to the share")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared, eventId string) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "SpaceUnshared").
|
||||
Str("itemid", e.ID.OpaqueId).
|
||||
Logger()
|
||||
|
||||
executant, spaceName, shareLink, ctx, err := s.prepareSpaceUnshared(logger, e)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not prepare vars for email")
|
||||
return
|
||||
}
|
||||
|
||||
granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
|
||||
filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceUnshared)
|
||||
|
||||
recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...)
|
||||
if recipientsInstant == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sharerDisplayName := executant.GetDisplayName()
|
||||
emails, err := s.render(ctx, email.UnsharedSpace,
|
||||
"SpaceGrantee",
|
||||
map[string]string{
|
||||
"SpaceSharer": sharerDisplayName,
|
||||
"SpaceName": spaceName,
|
||||
"ShareLink": shareLink,
|
||||
}, recipientsInstant, sharerDisplayName)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Could not get render the email")
|
||||
return
|
||||
}
|
||||
s.send(ctx, emails)
|
||||
}
|
||||
|
||||
func (s eventsNotifier) prepareSpaceUnshared(logger zerolog.Logger, e events.SpaceUnshared) (executant *user.User, spaceName, shareLink string, ctx context.Context, err error) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get service user context")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
executant, err = utils.GetUserNoGroups(ctx, e.Executant, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not get user")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
resourceID, err := storagespace.ParseID(e.ID.OpaqueId)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not parse resourceid from ItemID ")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
resourceInfo, err := s.getResourceInfo(ctx, &resourceID, nil)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not get space info")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
spaceName = resourceInfo.GetSpace().GetName()
|
||||
|
||||
shareLink, err = urlJoinPath(s.qsferaURL, "f", e.ID.OpaqueId)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not create link to the share")
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
return executant, spaceName, shareLink, ctx, err
|
||||
}
|
||||
|
||||
func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExpired, eventId string) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "SpaceMembershipExpired").
|
||||
Str("itemid", e.SpaceID.GetOpaqueId()).
|
||||
Logger()
|
||||
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Could not impersonate sharer")
|
||||
return
|
||||
}
|
||||
|
||||
owner, err := utils.GetUserNoGroups(ctx, e.SpaceOwner, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Msg("could not get user")
|
||||
return
|
||||
}
|
||||
|
||||
granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
|
||||
if granteeList == nil {
|
||||
return
|
||||
}
|
||||
filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceMembershipExpired)
|
||||
|
||||
recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...)
|
||||
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...)
|
||||
if recipientsInstant == nil {
|
||||
return
|
||||
}
|
||||
|
||||
emails, err := s.render(ctx, email.MembershipExpired,
|
||||
"SpaceGrantee",
|
||||
map[string]string{
|
||||
"SpaceName": e.SpaceName,
|
||||
"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
|
||||
}, recipientsInstant, owner.GetDisplayName())
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get render the email")
|
||||
return
|
||||
}
|
||||
s.send(ctx, emails)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/middleware"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/pkg/errors"
|
||||
micrometadata "go-micro.dev/v4/metadata"
|
||||
)
|
||||
|
||||
type intervalSplitter struct {
|
||||
log log.Logger
|
||||
valueClient settingssvc.ValueService
|
||||
}
|
||||
|
||||
func newIntervalSplitter(l log.Logger, vc settingssvc.ValueService) *intervalSplitter {
|
||||
return &intervalSplitter{log: l, valueClient: vc}
|
||||
}
|
||||
|
||||
// execute splits users into 3 lists depending on their email sending interval settings
|
||||
func (s intervalSplitter) execute(ctx context.Context, users []*user.User) (instant, daily, weekly []*user.User) {
|
||||
for _, u := range users {
|
||||
userId := u.GetId().GetOpaqueId()
|
||||
interval, err := getEmailSendingInterval(ctx, s.valueClient, userId)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Str("userId", userId).Msg("cannot get user email sending interval")
|
||||
instant = append(instant, u)
|
||||
} else if interval == "instant" {
|
||||
instant = append(instant, u)
|
||||
} else if interval == _intervalDaily {
|
||||
daily = append(daily, u)
|
||||
} else if interval == _intervalWeekly {
|
||||
weekly = append(weekly, u)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getEmailSendingInterval(ctx context.Context, vc settingssvc.ValueService, userId string) (string, error) {
|
||||
resp, err := vc.GetValueByUniqueIdentifiers(
|
||||
micrometadata.Set(ctx, middleware.AccountID, userId),
|
||||
&settingssvc.GetValueByUniqueIdentifiersRequest{
|
||||
AccountUuid: userId,
|
||||
SettingId: defaults.SettingUUIDProfileEmailSendingInterval,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
val := resp.GetValue().GetValue().GetStringValue()
|
||||
if val == "" {
|
||||
return "", errors.New("email sending interval is empty")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"go-micro.dev/v4/client"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
settings "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
settingsmocks "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0/mocks"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("NotificationFilter", func() {
|
||||
var (
|
||||
testLogger = log.NewLogger()
|
||||
|
||||
vs *settingsmocks.ValueService
|
||||
s intervalSplitter
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
vs = &settingsmocks.ValueService{}
|
||||
s = intervalSplitter{
|
||||
log: testLogger,
|
||||
valueClient: vs,
|
||||
}
|
||||
})
|
||||
|
||||
It("handles connection errors", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(nil, errors.New("no connection to ValueService"))
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("foo"))
|
||||
Expect(instant).To(Equal(newUsers("foo")))
|
||||
Expect(daily).To(BeEmpty())
|
||||
Expect(weekly).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles no setting in ValueService response", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(&settings.GetValueResponse{}, nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("foo"))
|
||||
Expect(instant).To(Equal(newUsers("foo")))
|
||||
Expect(daily).To(BeEmpty())
|
||||
Expect(weekly).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles nil response", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("foo"))
|
||||
Expect(instant).To(Equal(newUsers("foo")))
|
||||
Expect(daily).To(BeEmpty())
|
||||
Expect(weekly).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles nil input user", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), nil)
|
||||
Expect(instant).To(BeEmpty())
|
||||
Expect(daily).To(BeEmpty())
|
||||
Expect(weekly).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles never interval", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(newGetValueResponseStringValue("never"), nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("foo"))
|
||||
Expect(instant).To(BeEmpty())
|
||||
Expect(daily).To(BeEmpty())
|
||||
Expect(weekly).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles instant interval", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(newGetValueResponseStringValue("instant"), nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("foo"))
|
||||
Expect(instant).To(Equal(newUsers("foo")))
|
||||
Expect(daily).To(BeEmpty())
|
||||
Expect(weekly).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles daily interval", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(newGetValueResponseStringValue("daily"), nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("foo"))
|
||||
Expect(instant).To(BeEmpty())
|
||||
Expect(daily).To(Equal(newUsers("foo")))
|
||||
Expect(weekly).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles weekly interval", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(newGetValueResponseStringValue("weekly"), nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("foo"))
|
||||
Expect(instant).To(BeEmpty())
|
||||
Expect(daily).To(BeEmpty())
|
||||
Expect(weekly).To(Equal(newUsers("foo")))
|
||||
})
|
||||
|
||||
It("handles multiple users and intervals", func() {
|
||||
vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(func(ctx context.Context, req *settings.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) *settings.GetValueResponse {
|
||||
if strings.Contains(req.AccountUuid, "never") {
|
||||
return newGetValueResponseStringValue("never")
|
||||
} else if strings.Contains(req.AccountUuid, "instant") {
|
||||
return newGetValueResponseStringValue("instant")
|
||||
} else if strings.Contains(req.AccountUuid, "daily") {
|
||||
return newGetValueResponseStringValue("daily")
|
||||
} else if strings.Contains(req.AccountUuid, "weekly") {
|
||||
return newGetValueResponseStringValue("weekly")
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
instant, daily, weekly := s.execute(context.TODO(), newUsers("never1", "instant1", "daily1", "weekly1", "never2", "instant2", "daily2", "weekly2"))
|
||||
Expect(instant).To(Equal(newUsers("instant1", "instant2")))
|
||||
Expect(daily).To(Equal(newUsers("daily1", "daily2")))
|
||||
Expect(weekly).To(Equal(newUsers("weekly1", "weekly2")))
|
||||
})
|
||||
})
|
||||
|
||||
func newGetValueResponseStringValue(strVal string) *settings.GetValueResponse {
|
||||
return &settings.GetValueResponse{Value: &settingsmsg.ValueWithIdentifier{
|
||||
Value: &settingsmsg.Value{
|
||||
Value: &settingsmsg.Value_StringValue{
|
||||
StringValue: strVal,
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func newUsers(ids ...string) []*user.User {
|
||||
var users []*user.User
|
||||
for _, s := range ids {
|
||||
users = append(users, &user.User{Id: &user.UserId{OpaqueId: s}})
|
||||
}
|
||||
return users
|
||||
}
|
||||
Reference in New Issue
Block a user