Initial QSfera import
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user