mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-05 03:40:01 -06:00
README.md updated.
returned value swapped
This commit is contained in:
@@ -3,6 +3,7 @@ Enhancement: Notifications
|
||||
Make Emails translatable via transifex
|
||||
The transifex translation add in to the email templates.
|
||||
The optional environment variable NOTIFICATIONS_TRANSLATION_PATH added to config.
|
||||
The optional global environment variable OCIS_TRANSLATION_PATH added to notifications and userlog config.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/6038
|
||||
https://github.com/owncloud/ocis/issues/6025
|
||||
|
||||
@@ -2,14 +2,48 @@
|
||||
|
||||
The notification service is responsible for sending emails to users informing them about events that happened. To do this it hooks into the event system and listens for certain events that the users need to be informed about.
|
||||
|
||||
## Email notification
|
||||
|
||||
The `notifications` service has embedded email body templates.
|
||||
The email templates contain placeholders `{{ .Greeting }}`, `{{ .MessageBody }}`, `{{ .CallToAction }}` that are
|
||||
replaced with translations (See [Translations](#translations) in this readme).
|
||||
These embedded templates are available for all deployment scenarios. In addition, the service supports custom
|
||||
templates.
|
||||
The custom email template takes precedence over the embedded one. If a custom email template exists, the embedded ones
|
||||
are not used. To configure custom email templates,
|
||||
the `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` environment variable needs to point to a base folder that will contain the email
|
||||
templates. The source template files provided by ocis are located
|
||||
in [https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates](https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates) in the `shares`
|
||||
and `spaces` subfolders:
|
||||
[shares/shareCreated.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl)
|
||||
[shares/shareExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl)
|
||||
[spaces/membershipExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl)
|
||||
[spaces/sharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl)
|
||||
[spaces/unsharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl)
|
||||
|
||||
Custom Email templates referenced via `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` must be located in subfolders `shares`
|
||||
and `spaces` and have the same names as the embedded templates. This naming must match the embedded ones.
|
||||
```text
|
||||
templates
|
||||
│
|
||||
└───shares
|
||||
│ │ shareCreated.email.body.tmpl
|
||||
│ │ shareExpired.email.body.tmpl
|
||||
│
|
||||
└───spaces
|
||||
│ membershipExpired.email.body.tmpl
|
||||
│ sharedSpace.email.body.tmpl
|
||||
│ unsharedSpace.email.body.tmpl
|
||||
```
|
||||
|
||||
## Translations
|
||||
|
||||
The `translations` service has embedded translations sourced via transifex to provide a basic set of translated languages.
|
||||
The `notifications` service has embedded translations sourced via transifex to provide a basic set of translated languages.
|
||||
These embedded translations are available for all deployment scenarios. In addition, the service supports custom
|
||||
translations, though it is currently not possible to just add custom translations to embedded ones. If custom
|
||||
translations are configured, the embedded ones are not used. To configure custom translations,
|
||||
the `NOTIFICATIONS_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation
|
||||
files. This path must be available from all instances of the translations service, a shared storage is recommended.
|
||||
files. This path must be available from all instances of the notifications service, a shared storage is recommended.
|
||||
Translation files must be of type [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files)
|
||||
or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to
|
||||
be `translations.po` (or `translations.mo`) and stored in a folder structure defining the language code. In general the path/name
|
||||
|
||||
@@ -28,7 +28,7 @@ type Notifications struct {
|
||||
Events Events `yaml:"events"`
|
||||
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;NOTIFICATIONS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
|
||||
EmailTemplatePath string `yaml:"email_template_path" env:"OCIS_EMAIL_TEMPLATE_PATH;NOTIFICATIONS_EMAIL_TEMPLATE_PATH" desc:"Path to Email notification templates overriding embedded ones."`
|
||||
TranslationPath string `yaml:"translation_path" env:"NOTIFICATIONS_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. See the documentation for more details."`
|
||||
TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH,NOTIFICATIONS_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details."`
|
||||
RevaGateway string `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY;REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" deprecationVersion:"3.0" removalVersion:"3.1" deprecationInfo:"REVA_GATEWAY changing name for consistency" deprecationReplacement:"OCIS_REVA_GATEWAY"`
|
||||
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
|
||||
}
|
||||
|
||||
@@ -17,21 +17,21 @@ var (
|
||||
)
|
||||
|
||||
// RenderEmailTemplate renders the email template for a new share
|
||||
func RenderEmailTemplate(et MessageTemplate, locale string, emailTemplatePath string, translationPath string, vars map[string]interface{}) (string, string, error) {
|
||||
rawsub := ComposeMessage(et.Subject, locale, translationPath)
|
||||
func RenderEmailTemplate(mt MessageTemplate, locale string, emailTemplatePath string, translationPath string, vars map[string]interface{}) (string, string, error) {
|
||||
// translate a message
|
||||
mt.Subject = ComposeMessage(mt.Subject, locale, translationPath)
|
||||
mt.Greeting = ComposeMessage(mt.Greeting, locale, translationPath)
|
||||
mt.MessageBody = ComposeMessage(mt.MessageBody, locale, translationPath)
|
||||
mt.CallToAction = ComposeMessage(mt.CallToAction, locale, translationPath)
|
||||
|
||||
// replace the body email placeholders with the values
|
||||
subject, err := executeRaw(rawsub, vars)
|
||||
subject, err := executeRaw(mt.Subject, vars)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
bodyPlaceholders := map[string]interface{}{}
|
||||
bodyPlaceholders["Greeting"] = ComposeMessage(et.Greeting, locale, translationPath)
|
||||
bodyPlaceholders["MessageBody"] = ComposeMessage(et.MessageBody, locale, translationPath)
|
||||
bodyPlaceholders["CallToAction"] = ComposeMessage(et.CallToAction, locale, translationPath)
|
||||
|
||||
// replace the body email template placeholders with the translated template
|
||||
rawBody, err := executeEmailTemplate(emailTemplatePath, et.bodyTemplate, bodyPlaceholders)
|
||||
rawBody, err := executeEmailTemplate(emailTemplatePath, mt)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -43,19 +43,19 @@ func RenderEmailTemplate(et MessageTemplate, locale string, emailTemplatePath st
|
||||
return subject, body, nil
|
||||
}
|
||||
|
||||
func executeEmailTemplate(emailTemplatePath, templateName string, vars map[string]interface{}) (string, error) {
|
||||
func executeEmailTemplate(emailTemplatePath string, mt MessageTemplate) (string, error) {
|
||||
var err error
|
||||
var tpl *template.Template
|
||||
// try to lookup the files in the filesystem
|
||||
tpl, err = template.ParseFiles(filepath.Join(emailTemplatePath, templateName))
|
||||
tpl, err = template.ParseFiles(filepath.Join(emailTemplatePath, mt.bodyTemplate))
|
||||
if err != nil {
|
||||
// template has not been found in the fs, or path has not been specified => use embed templates
|
||||
tpl, err = template.ParseFS(templatesFS, filepath.Join("templates/", templateName))
|
||||
tpl, err = template.ParseFS(templatesFS, filepath.Join("templates/", mt.bodyTemplate))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
str, err := executeTemplate(tpl, vars)
|
||||
str, err := executeTemplate(tpl, mt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func executeRaw(raw string, vars map[string]interface{}) (string, error) {
|
||||
return executeTemplate(tpl, vars)
|
||||
}
|
||||
|
||||
func executeTemplate(tpl *template.Template, vars map[string]interface{}) (string, error) {
|
||||
func executeTemplate(tpl *template.Template, vars any) (string, error) {
|
||||
var writer bytes.Buffer
|
||||
if err := tpl.Execute(&writer, vars); err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -89,11 +89,7 @@ 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
|
||||
sub, msg, err := email.RenderEmailTemplate(template, "en", s.emailTemplatePath, s.translationPath, values)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return msg, sub, nil
|
||||
return email.RenderEmailTemplate(template, "en", s.emailTemplatePath, s.translationPath, values)
|
||||
}
|
||||
|
||||
func (s eventsNotifier) send(ctx context.Context, u *user.UserId, g *group.GroupId, msg, subj, sender string) error {
|
||||
|
||||
@@ -66,8 +66,19 @@ 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",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
expectedMessage: `Hello Dr. S. Harer
|
||||
|
||||
Dr. S. Harer has shared "secrets of the board" with you.
|
||||
|
||||
Click here to view it: files/shares/with-me
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.ShareCreated{
|
||||
Sharer: sharer.GetId(),
|
||||
@@ -80,8 +91,19 @@ var _ = Describe("Notifications", func() {
|
||||
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",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
expectedMessage: `Hello Dr. S. Harer,
|
||||
|
||||
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.
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.ShareExpired{
|
||||
ShareOwner: sharer.GetId(),
|
||||
@@ -94,8 +116,19 @@ var _ = Describe("Notifications", func() {
|
||||
Entry("Added to Space", testChannel{
|
||||
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
|
||||
expectedSubject: "Dr. S. Harer invited you to join secret space",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
expectedMessage: `Hello Dr. S. Harer,
|
||||
|
||||
Dr. S. Harer has invited you to join "secret space".
|
||||
|
||||
Click here to view it: f/spaceid
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceShared{
|
||||
Executant: sharer.GetId(),
|
||||
@@ -108,8 +141,21 @@ var _ = Describe("Notifications", func() {
|
||||
Entry("Removed from Space", testChannel{
|
||||
expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
|
||||
expectedSubject: "Dr. S. Harer removed you from secret space",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
expectedMessage: `Hello Dr. S. Harer,
|
||||
|
||||
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
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceUnshared{
|
||||
Executant: sharer.GetId(),
|
||||
@@ -121,8 +167,19 @@ var _ = Describe("Notifications", func() {
|
||||
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",
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
expectedMessage: `Hello Dr. S. Harer,
|
||||
|
||||
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
|
||||
|
||||
|
||||
---
|
||||
ownCloud - Store. Share. Work.
|
||||
https://owncloud.com
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceMembershipExpired{
|
||||
SpaceOwner: sharer.GetId(),
|
||||
@@ -139,6 +196,7 @@ var _ = Describe("Notifications", func() {
|
||||
type testChannel struct {
|
||||
expectedReceipients map[string]bool
|
||||
expectedSubject string
|
||||
expectedMessage string
|
||||
expectedSender string
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -150,8 +208,7 @@ func (tc testChannel) SendMessage(ctx context.Context, userIDs []string, msg, su
|
||||
Expect(tc.expectedReceipients[u]).To(Equal(true))
|
||||
}
|
||||
|
||||
// TODO: test the message?
|
||||
//Expect(msg).To(Equal(tc.expectedMessage))
|
||||
Expect(msg).To(Equal(tc.expectedMessage))
|
||||
Expect(subject).To(Equal(tc.expectedSubject))
|
||||
Expect(senderDisplayName).To(Equal(tc.expectedSender))
|
||||
tc.done <- struct{}{}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
|
||||
}
|
||||
|
||||
sharerDisplayName := owner.GetDisplayName()
|
||||
msg, subj, err := s.render(email.ShareCreated, map[string]interface{}{
|
||||
subj, msg, err := s.render(email.ShareCreated, map[string]interface{}{
|
||||
"ShareGrantee": shareGrantee,
|
||||
"ShareSharer": sharerDisplayName,
|
||||
"ShareFolder": resourceInfo.Name,
|
||||
@@ -85,7 +85,7 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
|
||||
return
|
||||
}
|
||||
|
||||
msg, subj, err := s.render(email.ShareExpired, map[string]interface{}{
|
||||
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"),
|
||||
|
||||
@@ -55,7 +55,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
|
||||
}
|
||||
|
||||
sharerDisplayName := owner.GetDisplayName()
|
||||
msg, subj, err := s.render(email.SharedSpace, map[string]interface{}{
|
||||
subj, msg, err := s.render(email.SharedSpace, map[string]interface{}{
|
||||
"SpaceGrantee": spaceGrantee,
|
||||
"SpaceSharer": sharerDisplayName,
|
||||
"SpaceName": resourceInfo.GetSpace().GetName(),
|
||||
@@ -117,7 +117,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
|
||||
}
|
||||
|
||||
sharerDisplayName := owner.GetDisplayName()
|
||||
msg, subj, err := s.render(email.UnsharedSpace, map[string]interface{}{
|
||||
subj, msg, err := s.render(email.UnsharedSpace, map[string]interface{}{
|
||||
"SpaceGrantee": spaceGrantee,
|
||||
"SpaceSharer": sharerDisplayName,
|
||||
"SpaceName": resourceInfo.GetSpace().Name,
|
||||
@@ -152,7 +152,7 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
|
||||
return
|
||||
}
|
||||
|
||||
msg, subj, err := s.render(email.MembershipExpired, map[string]interface{}{
|
||||
subj, msg, err := s.render(email.MembershipExpired, map[string]interface{}{
|
||||
"SpaceGrantee": shareGrantee,
|
||||
"SpaceName": e.SpaceName,
|
||||
"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
|
||||
|
||||
@@ -23,7 +23,7 @@ type Config struct {
|
||||
|
||||
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;USERLOG_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
|
||||
RevaGateway string `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY;REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" deprecationVersion:"3.0" removalVersion:"3.1" deprecationInfo:"REVA_GATEWAY changing name for consistency" deprecationReplacement:"OCIS_REVA_GATEWAY"`
|
||||
TranslationPath string `yaml:"translation_path" env:"USERLOG_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. See the documentation for more details."`
|
||||
TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH,USERLOG_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details."`
|
||||
Events Events `yaml:"events"`
|
||||
Persistence Persistence `yaml:"persistence"`
|
||||
|
||||
|
||||
@@ -355,9 +355,6 @@ func composeMessage(nt NotificationTemplate, locale string, path string, vars ma
|
||||
}
|
||||
|
||||
message, err := executeTemplate(messageraw, vars)
|
||||
if err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
return subject, subjectraw, message, messageraw, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user