package service import ( "context" "errors" "fmt" "net/url" "os" "os/signal" "path" "strings" "syscall" 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/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "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/notifications/pkg/channels" "github.com/owncloud/ocis/v2/services/notifications/pkg/email" "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" "go-micro.dev/v4/metadata" "google.golang.org/protobuf/types/known/fieldmaskpb" ) var _defaultLocale = "en" // Service should be named `Runner` type Service interface { Run() error } // 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, ocisURL string) Service { return eventsNotifier{ logger: logger, channel: channel, events: events, signals: make(chan os.Signal, 1), gatewaySelector: gatewaySelector, valueService: valueService, serviceAccountID: serviceAccountID, serviceAccountSecret: serviceAccountSecret, emailTemplatePath: emailTemplatePath, defaultLanguage: defaultLanguage, ocisURL: ocisURL, } } type eventsNotifier struct { logger log.Logger channel channels.Channel events <-chan events.Event signals chan os.Signal gatewaySelector pool.Selectable[gateway.GatewayAPIClient] valueService settingssvc.ValueService emailTemplatePath string translationPath string defaultLanguage string ocisURL string serviceAccountID string serviceAccountSecret string } func (s eventsNotifier) Run() error { signal.Notify(s.signals, syscall.SIGINT, syscall.SIGTERM) s.logger.Debug(). Msg("eventsNotifier started") for { select { case evt := <-s.events: go func() { switch e := evt.Event.(type) { case events.SpaceShared: s.handleSpaceShared(e) case events.SpaceUnshared: s.handleSpaceUnshared(e) case events.SpaceMembershipExpired: s.handleSpaceMembershipExpired(e) case events.ShareCreated: s.handleShareCreated(e) case events.ShareExpired: s.handleShareExpired(e) } }() case <-s.signals: s.logger.Debug(). Msg("eventsNotifier stopped") return nil } } } 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 := s.getUserLang(ctx, usr.GetId()) 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, recipientList []*channels.Message) { for _, r := range recipientList { 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) getUserLang(ctx context.Context, u *user.UserId) string { granteeCtx := metadata.Set(ctx, middleware.AccountID, u.GetOpaqueId()) if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx, &settingssvc.GetValueByUniqueIdentifiersRequest{ AccountUuid: u.GetOpaqueId(), SettingId: defaults.SettingUUIDProfileLanguage, }, ); err == nil { val := resp.GetValue().GetValue().GetListValue().GetValues() if len(val) > 0 && val[0] != nil { return val[0].GetStringValue() } } return _defaultLocale } 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 }