Merge pull request #6038 from 2403905/issue-6025

Make Emails translatable via transifex #6025
This commit is contained in:
kobergj
2023-04-18 12:08:40 +02:00
committed by GitHub
25 changed files with 486 additions and 98 deletions

View File

@@ -0,0 +1,9 @@
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

View File

@@ -1,6 +1,10 @@
SHELL := bash
NAME := notifications
# Where to write the files generated by this makefile.
OUTPUT_DIR = ./pkg/email/l10n
TEMPLATE_FILE = ./pkg/email/l10n/notifications.pot
include ../../.make/recursion.mk
############ tooling ############
@@ -29,6 +33,26 @@ ci-go-generate: # CI runs ci-node-generate automatically before this target
.PHONY: ci-node-generate
ci-node-generate:
############ translations ########
.PHONY: l10n-pull
l10n-pull:
cd $(OUTPUT_DIR) && tx pull -a --skip --minimum-perc=75
.PHONY: l10n-push
l10n-push:
cd $(OUTPUT_DIR) && tx push -s --skip
.PHONY: l10n-read
l10n-read: $(GO_XGETTEXT)
go-xgettext -o $(OUTPUT_DIR)/notifications.pot --keyword=Template --add-comments -s pkg/email/l10n/templates.go
.PHONY: l10n-write
l10n-write:
.PHONY: l10n-clean
l10n-clean:
rm -f $(TEMPLATE_FILE);
############ licenses ############
.PHONY: ci-node-check-licenses
ci-node-check-licenses:

View File

@@ -2,3 +2,66 @@
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 Templates
The `notifications` service has embedded email body templates.
The email templates contain placeholders `{{ .Greeting }}`, `{{ .MessageBody }}`, `{{ .CallToAction }}` that are
replaced with translations, see the [Translations](#translations) section.
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 `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 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
pattern for a translation file needs to be:
```text
{NOTIFICATIONS_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/translations.po
```
The language code pattern is composed of `language[_territory]` where `language` is the base language and `_territory`
is optional and defines a country.
For example, for the language `de_DE`, one needs to place the corresponding translation files
to `{NOTIFICATIONS_TRANSLATION_PATH}/de_DE/LC_MESSAGES/translations.po`.
### Translation Rules
* If a requested language code is not available, the service tries to fall back to the base language if available.
For example, if `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
* If the base language `de` is also not available, the service falls back to the system's default English (`en`),
which is the source of the texts provided by the code.

View File

@@ -28,6 +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:"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"`
}

View File

@@ -0,0 +1,41 @@
package email
import (
"embed"
"io/fs"
"strings"
"github.com/leonelquinteros/gotext"
)
var (
//go:embed l10n/locale
_translationFS embed.FS
_domain = "notifications"
)
// ComposeMessage renders the message based on template
func ComposeMessage(template, locale string, path string) string {
raw := loadTemplate(template, locale, path)
return replacePlaceholders(raw)
}
func loadTemplate(template, locale string, path string) string {
// Create Locale with library path and language code and load default domain
var l *gotext.Locale
if path == "" {
filesystem, _ := fs.Sub(_translationFS, "l10n/locale")
l = gotext.NewLocaleFS(locale, filesystem)
} else { // use custom path instead
l = gotext.NewLocale(path, locale)
}
l.AddDomain(_domain) // make domain configurable only if needed
return l.Get(template)
}
func replacePlaceholders(raw string) string {
for o, n := range _placeholders {
raw = strings.ReplaceAll(raw, o, n)
}
return raw
}

View File

@@ -1,8 +1,12 @@
// Package email implements utility for rendering the Email.
//
// The email package supports transifex translation for email templates.
package email
import (
"bytes"
"embed"
"html"
"html/template"
"path/filepath"
)
@@ -13,22 +17,63 @@ var (
)
// RenderEmailTemplate renders the email template for a new share
func RenderEmailTemplate(templateName string, templateVariables map[string]string, emailTemplatePath string) (string, error) {
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(mt.Subject, vars)
if err != nil {
return "", "", err
}
// replace the body email template placeholders with the translated template
rawBody, err := executeEmailTemplate(emailTemplatePath, mt)
if err != nil {
return "", "", err
}
// replace the body email placeholders with the values
body, err := executeRaw(rawBody, vars)
if err != nil {
return "", "", err
}
return subject, body, nil
}
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
}
}
var writer bytes.Buffer
err = tpl.Execute(&writer, templateVariables)
str, err := executeTemplate(tpl, mt)
if err != nil {
return "", err
}
return html.UnescapeString(str), err
}
func executeRaw(raw string, vars map[string]interface{}) (string, error) {
tpl, err := template.New("").Parse(raw)
if err != nil {
return "", err
}
return executeTemplate(tpl, vars)
}
func executeTemplate(tpl *template.Template, vars any) (string, error) {
var writer bytes.Buffer
if err := tpl.Execute(&writer, vars); err != nil {
return "", err
}
return writer.String(), nil
}

View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:owncloud-org:p:owncloud:r:ocis-notifications]
file_filter = locale/<lang>/LC_MESSAGES/notifications.po
minimum_perc = 75
source_file = notifications.pot
source_lang = en
type = PO

View File

@@ -0,0 +1,102 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2023-04-14 16:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#. UnsharedSpace email template, CallToAction field
#: pkg/email/templates.go:57
msgid "Click here to check it: {ShareLink}"
msgstr ""
#. ShareCreated email template, CallToAction field
#. SharedSpace email template, CallToAction field
#: pkg/email/templates.go:18 pkg/email/templates.go:43
msgid "Click here to view it: {ShareLink}"
msgstr ""
#. ShareCreated email template, Greeting field
#: pkg/email/templates.go:14
msgid "Hello {ShareGrantee}"
msgstr ""
#. ShareExpired email template, Greeting field
#: pkg/email/templates.go:26
msgid "Hello {ShareGrantee},"
msgstr ""
#. SharedSpace email template, Greeting field
#. UnsharedSpace email template, Greeting field
#. MembershipExpired email template, Greeting field
#: pkg/email/templates.go:39 pkg/email/templates.go:51 pkg/email/templates.go:65
msgid "Hello {SpaceGrantee},"
msgstr ""
#. MembershipExpired email template, Subject field
#: pkg/email/templates.go:63
msgid "Membership of '{SpaceName}' expired at {ExpiredAt}"
msgstr ""
#. ShareExpired email template, Subject field
#: pkg/email/templates.go:24
msgid "Share to '{ShareFolder}' expired at {ExpiredAt}"
msgstr ""
#. MembershipExpired email template, Message field
#: pkg/email/templates.go:67
msgid "Your membership of space {SpaceName} has expired at {ExpiredAt}\n"
"\n"
"Even though this membership has expired you still might have access through other shares and/or space memberships"
msgstr ""
#. ShareExpired email template, Message field
#: pkg/email/templates.go:28
msgid "Your share to {ShareFolder} has expired at {ExpiredAt}\n"
"\n"
"Even though this share has been revoked you still might have access through other shares and/or space memberships."
msgstr ""
#. ShareCreated email template, Message field
#: pkg/email/templates.go:16
msgid "{ShareSharer} has shared \"{ShareFolder}\" with you."
msgstr ""
#. ShareCreated email template, Subject field
#: pkg/email/templates.go:12
msgid "{ShareSharer} shared '{ShareFolder}' with you"
msgstr ""
#. SharedSpace email template, Message field
#: pkg/email/templates.go:41
msgid "{SpaceSharer} has invited you to join \"{SpaceName}\"."
msgstr ""
#. UnsharedSpace email template, Message field
#: pkg/email/templates.go:53
msgid "{SpaceSharer} has removed you from \"{SpaceName}\".\n"
"\n"
"You might still have access through your other groups or direct membership."
msgstr ""
#. SharedSpace email template, Subject field
#: pkg/email/templates.go:37
msgid "{SpaceSharer} invited you to join {SpaceName}"
msgstr ""
#. UnsharedSpace email template, Subject field
#: pkg/email/templates.go:49
msgid "{SpaceSharer} removed you from {SpaceName}"
msgstr ""

View File

@@ -0,0 +1,93 @@
package email
// Template marks the string as a translatable template
func Template(s string) string { return s }
// the available templates
var (
// Shares
ShareCreated = MessageTemplate{
bodyTemplate: "shares/shareCreated.email.body.tmpl",
// ShareCreated email template, Subject field
Subject: Template(`{ShareSharer} shared '{ShareFolder}' with you`),
// ShareCreated email template, Greeting field
Greeting: Template(`Hello {ShareGrantee}`),
// ShareCreated email template, Message field
MessageBody: Template(`{ShareSharer} has shared "{ShareFolder}" with you.`),
// ShareCreated email template, CallToAction field
CallToAction: Template(`Click here to view it: {ShareLink}`),
}
ShareExpired = MessageTemplate{
bodyTemplate: "shares/shareExpired.email.body.tmpl",
// ShareExpired email template, Subject field
Subject: Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`),
// ShareExpired email template, Greeting field
Greeting: Template(`Hello {ShareGrantee},`),
// ShareExpired email template, Message field
MessageBody: Template(`Your share to {ShareFolder} has expired at {ExpiredAt}
Even though this share has been revoked you still might have access through other shares and/or space memberships.`),
}
// Spaces templates
SharedSpace = MessageTemplate{
bodyTemplate: "spaces/sharedSpace.email.body.tmpl",
// SharedSpace email template, Subject field
Subject: Template("{SpaceSharer} invited you to join {SpaceName}"),
// SharedSpace email template, Greeting field
Greeting: Template(`Hello {SpaceGrantee},`),
// SharedSpace email template, Message field
MessageBody: Template(`{SpaceSharer} has invited you to join "{SpaceName}".`),
// SharedSpace email template, CallToAction field
CallToAction: Template(`Click here to view it: {ShareLink}`),
}
UnsharedSpace = MessageTemplate{
bodyTemplate: "spaces/unsharedSpace.email.body.tmpl",
// UnsharedSpace email template, Subject field
Subject: Template(`{SpaceSharer} removed you from {SpaceName}`),
// UnsharedSpace email template, Greeting field
Greeting: Template(`Hello {SpaceGrantee},`),
// UnsharedSpace email template, Message field
MessageBody: Template(`{SpaceSharer} has removed you from "{SpaceName}".
You might still have access through your other groups or direct membership.`),
// UnsharedSpace email template, CallToAction field
CallToAction: Template(`Click here to check it: {ShareLink}`),
}
MembershipExpired = MessageTemplate{
bodyTemplate: "spaces/membershipExpired.email.body.tmpl",
// MembershipExpired email template, Subject field
Subject: Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`),
// MembershipExpired email template, Greeting field
Greeting: Template(`Hello {SpaceGrantee},`),
// MembershipExpired email template, Message field
MessageBody: Template(`Your membership of space {SpaceName} has expired at {ExpiredAt}
Even though this membership has expired you still might have access through other shares and/or space memberships`),
}
)
// holds the information to turn the raw template into a parseable go template
var _placeholders = map[string]string{
"{ShareSharer}": "{{ .ShareSharer }}",
"{ShareFolder}": "{{ .ShareFolder }}",
"{ShareGrantee}": "{{ .ShareGrantee }}",
"{ShareLink}": "{{ .ShareLink }}",
"{SpaceName}": "{{ .SpaceName }}",
"{SpaceGrantee}": "{{ .SpaceGrantee }}",
"{SpaceSharer}": "{{ .SpaceSharer }}",
"{ExpiredAt}": "{{ .ExpiredAt }}",
}
// MessageTemplate is the data structure for the email
type MessageTemplate struct {
// bodyTemplate represent the path to .tmpl file
bodyTemplate string
Subject string
Greeting string
MessageBody string
CallToAction string
}

View File

@@ -1,16 +1,8 @@
Hello {{ .ShareGrantee }},
{{ .Greeting }}
{{ .ShareSharer }} has shared "{{ .ShareFolder }}" with you.
{{ .MessageBody }}
Click here to view it: {{ .ShareLink }}
----------------------------------------------------------
Hallo {{ .ShareGrantee }},
{{ .ShareSharer }} hat dich zu "{{ .ShareFolder }}" eingeladen.
Klicke hier zum Anzeigen: {{ .ShareLink }}
{{ .CallToAction }}
---

View File

@@ -1 +0,0 @@
{{ .ShareSharer }} shared '{{ .ShareFolder }}' with you

View File

@@ -1,16 +1,6 @@
Hello {{ .ShareGrantee }},
{{ .Greeting }}
Your share to {{ .ShareFolder }} has expired at {{ .ExpiredAt }}
Even though this share has been revoked you still might have access through other shares and/or space memberships
----------------------------------------------------------
Hallo {{ .ShareGrantee }},
Deine Freigabe zu {{ .ShareFolder }} ist am {{ .ExpiredAt }} abgelaufen.
Obwohl diese Freigabe nicht mehr zur Verfügung steht, könntest du immer noch Zugriff über andere Freigaben und/oder Space Mitgliedschaften haben.
{{ .MessageBody }}
---

View File

@@ -1 +0,0 @@
Share to '{{ .ShareFolder }}' expired at {{ .ExpiredAt }}

View File

@@ -1,16 +1,6 @@
Hello {{ .SpaceGrantee }},
{{ .Greeting }}
Your membership of space {{ .SpaceName }} has expired at {{ .ExpiredAt }}
Even though this membership has expired you still might have access through other shares and/or space memberships
----------------------------------------------------------
Hallo {{ .SpaceGrantee }},
Deine Mitgliedschaft zu dem Space {{ .SpaceName }} ist am {{ .ExpiredAt }} abgelaufen.
Obwohl diese Mitgliedschaft nicht mehr zur Verfügung steht, könntest du immer noch Zugriff über andere Freigaben und/oder Space Mitgliedschaften haben.
{{ .MessageBody }}
---

View File

@@ -1 +0,0 @@
Membership of '{{ .SpaceName }}' expired at {{ .ExpiredAt }}

View File

@@ -1,16 +1,8 @@
Hello {{ .SpaceGrantee }},
{{ .Greeting }}
{{ .SpaceSharer }} has invited you to join "{{ .SpaceName }}".
{{ .MessageBody }}
Click here to view it: {{ .ShareLink }}
----------------------------------------------------------
Hallo {{ .SpaceGrantee }},
{{ .SpaceSharer }} hat dich in den Space "{{ .SpaceName }}" eingeladen.
Klicke hier zum Anzeigen: {{ .ShareLink }}
{{ .CallToAction }}
---

View File

@@ -1 +0,0 @@
{{ .SpaceSharer }} invited you to join {{ .SpaceName }}

View File

@@ -1,16 +1,8 @@
Hello {{ .SpaceGrantee }},
{{ .Greeting }}
{{ .SpaceSharer }} has removed you from "{{ .SpaceName }}".
{{ .MessageBody }}
You might still have access through your other groups or direct membership. Click here to check it: {{ .ShareLink }}
----------------------------------------------------------
Hallo {{ .SpaceGrantee }},
{{ .SpaceSharer }} hat dich aus dem Space "{{ .SpaceName }}" entfernt.
Du könntest über deine anderen Gruppen oder deiner direkten Mitgliedschaft noch Zugriff haben. Klicke hier zum Überprüfen: {{ .ShareLink }}
{{ .CallToAction }}
---

View File

@@ -1 +0,0 @@
{{ .SpaceSharer }} removed you from {{ .SpaceName }}

View File

@@ -54,6 +54,7 @@ type eventsNotifier struct {
gwClient gateway.GatewayAPIClient
machineAuthAPIKey string
emailTemplatePath string
translationPath string
ocisURL string
}
@@ -86,18 +87,9 @@ func (s eventsNotifier) Run() error {
}
}
func (s eventsNotifier) render(bodyTemplate string, subjTemplate string, values map[string]string) (string, string, error) {
msg, err := email.RenderEmailTemplate(bodyTemplate, values, s.emailTemplatePath)
if err != nil {
return "", "", err
}
sub, err := email.RenderEmailTemplate(subjTemplate, values, s.emailTemplatePath)
if err != nil {
return "", "", err
}
return msg, sub, nil
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)
}
func (s eventsNotifier) send(ctx context.Context, u *user.UserId, g *group.GroupId, msg, subj, sender string) error {

View File

@@ -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{}{}

View File

@@ -3,6 +3,7 @@ package service
import (
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/services/notifications/pkg/email"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
@@ -41,7 +42,7 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
}
sharerDisplayName := owner.GetDisplayName()
msg, subj, err := s.render("shares/shareCreated.email.body.tmpl", "shares/shareCreated.email.subject.tmpl", map[string]string{
subj, msg, err := s.render(email.ShareCreated, map[string]interface{}{
"ShareGrantee": shareGrantee,
"ShareSharer": sharerDisplayName,
"ShareFolder": resourceInfo.Name,
@@ -84,7 +85,7 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
return
}
msg, subj, err := s.render("shares/shareExpired.email.body.tmpl", "shares/shareExpired.email.subject.tmpl", map[string]string{
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"),

View File

@@ -4,6 +4,7 @@ import (
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/services/notifications/pkg/email"
)
func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
@@ -54,7 +55,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
}
sharerDisplayName := owner.GetDisplayName()
msg, subj, err := s.render("spaces/sharedSpace.email.body.tmpl", "spaces/sharedSpace.email.subject.tmpl", map[string]string{
subj, msg, err := s.render(email.SharedSpace, map[string]interface{}{
"SpaceGrantee": spaceGrantee,
"SpaceSharer": sharerDisplayName,
"SpaceName": resourceInfo.GetSpace().GetName(),
@@ -116,7 +117,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
}
sharerDisplayName := owner.GetDisplayName()
msg, subj, err := s.render("spaces/unsharedSpace.email.body.tmpl", "spaces/unsharedSpace.email.subject.tmpl", map[string]string{
subj, msg, err := s.render(email.UnsharedSpace, map[string]interface{}{
"SpaceGrantee": spaceGrantee,
"SpaceSharer": sharerDisplayName,
"SpaceName": resourceInfo.GetSpace().Name,
@@ -151,7 +152,7 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
return
}
msg, subj, err := s.render("spaces/membershipExpired.email.body.tmpl", "spaces/membershipExpired.email.subject.tmpl", map[string]string{
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"),

View File

@@ -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"`

View File

@@ -356,7 +356,6 @@ func composeMessage(nt NotificationTemplate, locale string, path string, vars ma
message, err := executeTemplate(messageraw, vars)
return subject, subjectraw, message, messageraw, err
}
func loadTemplates(nt NotificationTemplate, locale string, path string) (string, string) {