Files
opencloud/services/notifications/pkg/service/service.go
Roman Perekhod 04a5ee283e fix default language fallback (#7479)
* fix default language fallback

* Update services/userlog/pkg/config/config.go

Co-authored-by: Martin <github@diemattels.at>

* Update services/notifications/pkg/config/config.go

Co-authored-by: Martin <github@diemattels.at>

* readme updated. local env vars removed

* Update changelog/unreleased/fix-default-mail-language-fallback.md

Co-authored-by: Martin <github@diemattels.at>

* update readme's and envvar texts

* fix changelog text

---------

Co-authored-by: Roman Perekhod <rperekhod@owncloud.com>
Co-authored-by: Martin <github@diemattels.at>
2023-10-17 09:56:48 +02:00

280 lines
8.3 KiB
Go

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
}