Skip to content

Commit

Permalink
Merge pull request #6089 from 2403905/issue-6087
Browse files Browse the repository at this point in the history
Determine the users preferred language to translate emails via Transi…
  • Loading branch information
2403905 authored Apr 24, 2023
2 parents 6ad31f1 + 6cf0932 commit 5f4c068
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 134 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/users-preferred-language-email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Determine the users language to translate via Transifex

Enhance userlog service with proper api and messages
https://github.com/owncloud/ocis/pull/6089
https://github.com/owncloud/ocis/issues/6087
6 changes: 4 additions & 2 deletions services/notifications/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/go-micro/plugins/v4/events/natsjs"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/ocis-pkg/crypto"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
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/config"
"github.com/owncloud/ocis/v2/services/notifications/pkg/config/parser"
Expand Down Expand Up @@ -93,8 +95,8 @@ func Server(cfg *config.Config) *cli.Command {
if err != nil {
logger.Fatal().Err(err).Str("addr", cfg.Notifications.RevaGateway).Msg("could not get reva client")
}

svc := service.NewEventsNotifier(evts, channel, logger, gwclient, cfg.Notifications.MachineAuthAPIKey, cfg.Notifications.EmailTemplatePath, cfg.WebUIURL)
valueService := settingssvc.NewValueService("com.owncloud.api.settings", grpc.DefaultClient())
svc := service.NewEventsNotifier(evts, channel, logger, gwclient, valueService, cfg.Notifications.MachineAuthAPIKey, cfg.Notifications.EmailTemplatePath, cfg.WebUIURL)
return svc.Run()
},
}
Expand Down
117 changes: 88 additions & 29 deletions services/notifications/pkg/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/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
Expand All @@ -33,13 +39,16 @@ func NewEventsNotifier(
channel channels.Channel,
logger log.Logger,
gwClient gateway.GatewayAPIClient,
valueService settingssvc.ValueService,
machineAuthAPIKey, emailTemplatePath, ocisURL string) Service {

return eventsNotifier{
logger: logger,
channel: channel,
events: events,
signals: make(chan os.Signal, 1),
gwClient: gwClient,
valueService: valueService,
machineAuthAPIKey: machineAuthAPIKey,
emailTemplatePath: emailTemplatePath,
ocisURL: ocisURL,
Expand All @@ -52,6 +61,7 @@ type eventsNotifier struct {
events <-chan events.Event
signals chan os.Signal
gwClient gateway.GatewayAPIClient
valueService settingssvc.ValueService
machineAuthAPIKey string
emailTemplatePath string
translationPath string
Expand Down Expand Up @@ -87,52 +97,101 @@ func (s eventsNotifier) Run() error {
}
}

func (s eventsNotifier) render(template email.MessageTemplate, values map[string]interface{}) (string, string, error) {
// The locate have to come from the user setting
return email.RenderEmailTemplate(template, "en", s.emailTemplatePath, s.translationPath, values)
// recipient represent the already rendered message including the user id opaqueID
type recipient struct {
opaqueID string
subject string
msg string
}

func (s eventsNotifier) send(ctx context.Context, u *user.UserId, g *group.GroupId, msg, subj, sender string) error {
if u != nil {
return s.channel.SendMessage(ctx, []string{u.GetOpaqueId()}, msg, subj, sender)
func (s eventsNotifier) render(ctx context.Context, template email.MessageTemplate,
granteeFieldName string, fields map[string]interface{}, granteeList []*user.UserId) ([]recipient, error) {
// Render the Email Template for each user
recipientList := make([]recipient, len(granteeList))
for i, userID := range granteeList {
locale, err := s.getUserLang(ctx, userID)
if err != nil {
return nil, err
}
grantee, err := s.getUserName(ctx, userID)
if err != nil {
return nil, err
}
fields[granteeFieldName] = grantee

subj, msg, err := email.RenderEmailTemplate(template, locale, s.emailTemplatePath, s.translationPath, fields)
if err != nil {
return nil, err
}
recipientList[i] = recipient{opaqueID: userID.GetOpaqueId(), subject: subj, msg: msg}
}
return recipientList, nil
}

if g != nil {
return s.channel.SendMessageToGroup(ctx, g, msg, subj, sender)
func (s eventsNotifier) send(ctx context.Context, recipientList []recipient, sender string) {
for _, r := range recipientList {
err := s.channel.SendMessage(ctx, []string{r.opaqueID}, r.msg, r.subject, sender)
if err != nil {
s.logger.Error().Err(err).Str("event", "SendEmail").Msg("failed to send a message")
}
}

return nil
}

func (s eventsNotifier) getGranteeName(ctx context.Context, u *user.UserId, g *group.GroupId) (string, error) {
func (s eventsNotifier) getGranteeList(ctx context.Context, owner, u *user.UserId, g *group.GroupId) ([]*user.UserId, error) {
switch {
case u != nil:
r, err := s.gwClient.GetUser(ctx, &user.GetUserRequest{UserId: u})
if err != nil {
return "", err
}

if r.Status.Code != rpc.Code_CODE_OK {
return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
}

return r.GetUser().GetDisplayName(), nil
return []*user.UserId{u}, nil
case g != nil:
r, err := s.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: g})
res, err := s.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: g})
if err != nil {
return "", err
return nil, err
}

if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
if res.Status.Code != rpc.Code_CODE_OK {
return nil, errors.New("could not get group")
}

return r.GetGroup().GetDisplayName(), nil
for i, userID := range res.GetGroup().GetMembers() {
// remove an executant from a list
if userID.GetOpaqueId() == owner.GetOpaqueId() {
res.Group.Members[i] = res.Group.Members[len(res.Group.Members)-1]
return res.Group.Members[:len(res.Group.Members)-1], nil
}
}
return res.Group.Members, nil
default:
return "", errors.New("Need at least one non-nil grantee")
return nil, errors.New("need at least one non-nil grantee")
}
}

func (s eventsNotifier) getUserName(ctx context.Context, u *user.UserId) (string, error) {
if u == nil {
return "", errors.New("need at least one non-nil grantee")
}
r, err := s.gwClient.GetUser(ctx, &user.GetUserRequest{UserId: u})
if err != nil {
return "", err
}
if r.Status.Code != rpc.Code_CODE_OK {
return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
}
return r.GetUser().GetDisplayName(), nil
}

func (s eventsNotifier) getUserLang(ctx context.Context, u *user.UserId) (string, error) {
granteeCtx := metadata.Set(ctx, middleware.AccountID, u.OpaqueId)
if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx,
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: u.OpaqueId,
SettingId: defaults.SettingUUIDProfileLanguage,
}); err == nil {
if resp == nil {
return _defaultLocale, nil
}
val := resp.Value.GetValue().GetListValue().GetValues()
if len(val) > 0 && val[0] != nil {
return val[0].GetStringValue(), nil
}
}
return _defaultLocale, nil
}

func (s eventsNotifier) getResourceInfo(ctx context.Context, resourceID *provider.ResourceId, fieldmask *fieldmaskpb.FieldMask) (*provider.ResourceInfo, error) {
Expand Down
28 changes: 21 additions & 7 deletions services/notifications/pkg/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
"github.com/owncloud/ocis/v2/services/notifications/pkg/service"
"github.com/test-go/testify/mock"
"go-micro.dev/v4/client"
)

var _ = Describe("Notifications", func() {
var (
gwc *cs3mocks.GatewayAPIClient
vs *settingssvc.MockValueService
sharer = &user.User{
Id: &user.UserId{
OpaqueId: "sharer",
Expand All @@ -43,15 +49,23 @@ var _ = Describe("Notifications", func() {

BeforeEach(func() {
gwc = &cs3mocks.GatewayAPIClient{}
gwc.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
gwc.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once()
gwc.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once()
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
gwc.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 = &settingssvc.MockValueService{}
vs.GetValueByUniqueIdentifiersFunc = func(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settingssvc.GetValueResponse, error) {
return nil, nil
}
})

DescribeTable("Sending notifications",
func(tc testChannel, ev events.Event) {
cfg := defaults.FullDefaultConfig()
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
ch := make(chan events.Event)
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gwc, "", "", "")
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gwc, vs, "", "", "")
go evts.Run()

ch <- ev
Expand All @@ -66,7 +80,7 @@ var _ = Describe("Notifications", func() {
Entry("Share Created", testChannel{
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
expectedSubject: "Dr. S. Harer shared 'secrets of the board' with you",
expectedMessage: `Hello Dr. S. Harer
expectedMessage: `Hello Eric Expireling
Dr. S. Harer has shared "secrets of the board" with you.
Expand All @@ -91,7 +105,7 @@ https://owncloud.com
Entry("Share Expired", testChannel{
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
expectedSubject: "Share to 'secrets of the board' expired at 2023-04-17 16:42:00",
expectedMessage: `Hello Dr. S. Harer,
expectedMessage: `Hello Eric Expireling,
Your share to secrets of the board has expired at 2023-04-17 16:42:00
Expand All @@ -116,7 +130,7 @@ https://owncloud.com
Entry("Added to Space", testChannel{
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
expectedSubject: "Dr. S. Harer invited you to join secret space",
expectedMessage: `Hello Dr. S. Harer,
expectedMessage: `Hello Eric Expireling,
Dr. S. Harer has invited you to join "secret space".
Expand All @@ -141,7 +155,7 @@ https://owncloud.com
Entry("Removed from Space", testChannel{
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
expectedSubject: "Dr. S. Harer removed you from secret space",
expectedMessage: `Hello Dr. S. Harer,
expectedMessage: `Hello Eric Expireling,
Dr. S. Harer has removed you from "secret space".
Expand All @@ -167,7 +181,7 @@ https://owncloud.com
Entry("Space Expired", testChannel{
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
expectedSubject: "Membership of 'secret space' expired at 2023-04-17 16:42:00",
expectedMessage: `Hello Dr. S. Harer,
expectedMessage: `Hello Eric Expireling,
Your membership of space secret space has expired at 2023-04-17 16:42:00
Expand Down
56 changes: 25 additions & 31 deletions services/notifications/pkg/service/shares.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,25 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
return
}

shareGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
granteeList, err := s.getGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee name")
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee list")
return
}

sharerDisplayName := owner.GetDisplayName()
subj, msg, err := s.render(email.ShareCreated, map[string]interface{}{
"ShareGrantee": shareGrantee,
"ShareSharer": sharerDisplayName,
"ShareFolder": resourceInfo.Name,
"ShareLink": shareLink,
})

recipientList, err := s.render(ownerCtx, email.ShareCreated,
"ShareGrantee",
map[string]interface{}{
"ShareSharer": sharerDisplayName,
"ShareFolder": resourceInfo.Name,
"ShareLink": shareLink,
}, granteeList)
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not render E-Mail body template for shares")
}

if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("could not get render the email")
return
}

s.send(ownerCtx, recipientList, sharerDisplayName)
}

func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
Expand All @@ -65,38 +62,35 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
Str("itemid", e.ItemID.GetOpaqueId()).
Logger()

ctx, owner, err := utils.Impersonate(e.ShareOwner, s.gwClient, s.machineAuthAPIKey)
ownerCtx, owner, err := utils.Impersonate(e.ShareOwner, s.gwClient, s.machineAuthAPIKey)
if err != nil {
logger.Error().Err(err).Msg("Could not impersonate sharer")
return
}

resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
resourceInfo, err := s.getResourceInfo(ownerCtx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
if err != nil {
logger.Error().
Err(err).
Msg("could not stat resource")
return
}

shareGrantee, err := s.getGranteeName(ctx, e.GranteeUserID, e.GranteeGroupID)
granteeList, err := s.getGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee name")
s.logger.Error().Err(err).Str("event", "ShareExpired").Msg("Could not get grantee name")
return
}

subj, msg, err := s.render(email.ShareExpired, map[string]interface{}{
"ShareGrantee": shareGrantee,
"ShareFolder": resourceInfo.GetName(),
"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
})

recipientList, err := s.render(ownerCtx, email.ShareExpired,
"ShareGrantee",
map[string]interface{}{
"ShareFolder": resourceInfo.GetName(),
"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
}, granteeList)
if err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not render E-Mail body template for shares")
}

if err := s.send(ctx, e.GranteeUserID, e.GranteeGroupID, msg, subj, owner.GetDisplayName()); err != nil {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
s.logger.Error().Err(err).Str("event", "ShareExpired").Msg("could not get render the email")
return
}

s.send(ownerCtx, recipientList, owner.GetDisplayName())
}
Loading

0 comments on commit 5f4c068

Please sign in to comment.