Merge pull request #8723 from kobergj/SpaceTemplatesII

Server-Side Space Templates
This commit is contained in:
kobergj
2024-03-28 11:56:15 +01:00
committed by GitHub
23 changed files with 387 additions and 298 deletions

View File

@@ -0,0 +1,5 @@
Enhancement: Make server side space templates production ready
Fixes several smaller bugs and adds some improvements to space templates, introduced with https://github.com/owncloud/ocis/pull/8558
https://github.com/owncloud/ocis/pull/8723

90
ocis-pkg/l10n/l10n.go Normal file
View File

@@ -0,0 +1,90 @@
// package l10n holds translation mechanics that are used by user facing services (notifications, userlog, graph)
package l10n
import (
"context"
"errors"
"io/fs"
"os"
"github.com/leonelquinteros/gotext"
"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/settings/pkg/store/defaults"
micrometadata "go-micro.dev/v4/metadata"
)
// Template marks a string as translatable
func Template(s string) string { return s }
// Translator is able to translate strings
type Translator struct {
fs fs.FS
defaultLocale string
domain string
}
// NewTranslator creates a Translator with library path and language code and load default domain
func NewTranslator(defaultLocale string, domain string, fsys fs.FS) Translator {
return Translator{
fs: fsys,
defaultLocale: defaultLocale,
domain: domain,
}
}
// NewTranslatorFromCommonConfig creates a new Translator from legacy config
func NewTranslatorFromCommonConfig(defaultLocale string, domain string, path string, fsys fs.FS, fsSubPath string) Translator {
var filesystem fs.FS
if path == "" {
filesystem, _ = fs.Sub(fsys, fsSubPath)
} else { // use custom path instead
filesystem = os.DirFS(path)
}
return NewTranslator(defaultLocale, domain, filesystem)
}
// Translate translates a string to the locale
func (t Translator) Translate(str, locale string) string {
return t.Locale(locale).Get(str)
}
// Locale returns the gotext.Locale, use `.Get` method to translate strings
func (t Translator) Locale(locale string) *gotext.Locale {
l := gotext.NewLocaleFS(locale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
if locale != "en" && len(l.GetTranslations()) == 0 {
l = gotext.NewLocaleFS(t.defaultLocale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
}
return l
}
// MustGetUserLocale returns the locale the user wants to use, omitting errors
func MustGetUserLocale(ctx context.Context, userID string, preferedLang string, vc settingssvc.ValueService) string {
if preferedLang != "" {
return preferedLang
}
locale, _ := GetUserLocale(ctx, userID, vc)
return locale
}
// GetUserLocale returns the locale of the user
func GetUserLocale(ctx context.Context, userID string, vc settingssvc.ValueService) (string, error) {
resp, err := vc.GetValueByUniqueIdentifiers(
micrometadata.Set(ctx, middleware.AccountID, userID),
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userID,
SettingId: defaults.SettingUUIDProfileLanguage,
},
)
if err != nil {
return "", err
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return "", errors.New("no language setting found")
}
return val[0].GetStringValue(), nil
}

View File

@@ -1,6 +1,10 @@
SHELL := bash
NAME := graph
# Where to write the files generated by this makefile.
OUTPUT_DIR = ./pkg/service/v0/l10n
TEMPLATE_FILE = ./pkg/service/v0/l10n/graph.pot
include ../../.make/recursion.mk
############ tooling ############
@@ -30,6 +34,27 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this
.PHONY: ci-node-generate
ci-node-generate:
############ translations ########
.PHONY: l10n-pull
l10n-pull:
cd $(OUTPUT_DIR) && tx pull --all --force --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)/graph.pot --keyword=l10n.Template --add-comments -s pkg/service/v0/spacetemplates.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

@@ -46,6 +46,7 @@ type Spaces struct {
UsersCacheTTL int `yaml:"users_cache_ttl" env:"GRAPH_SPACES_USERS_CACHE_TTL" desc:"Max TTL in seconds for the spaces users cache." introductionVersion:"pre5.0"`
GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"pre5.0"`
StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"5.0"`
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"5.0"`
}
type LDAP struct {

View File

@@ -29,6 +29,7 @@ import (
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
@@ -326,7 +327,10 @@ func (g Graph) canCreateSpace(ctx context.Context, ownPersonalHome bool) bool {
func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling create drive")
us, ok := revactx.ContextGetUser(r.Context())
ctx := r.Context()
us, ok := revactx.ContextGetUser(ctx)
if !ok {
logger.Debug().Msg("could not create drive: invalid user")
errorcode.NotAllowed.Render(w, r, http.StatusUnauthorized, "invalid user")
@@ -334,7 +338,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
}
// TODO determine if the user tries to create his own personal space and pass that as a boolean
canCreateSpace := g.canCreateSpace(r.Context(), false)
canCreateSpace := g.canCreateSpace(ctx, false)
if !canCreateSpace {
logger.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions")
// if the permission is not existing for the user in context we can assume we don't have it. Return 401.
@@ -393,7 +397,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
csr.Owner = us
}
resp, err := gatewayClient.CreateStorageSpace(r.Context(), &csr)
resp, err := gatewayClient.CreateStorageSpace(ctx, &csr)
if err != nil {
logger.Error().Err(err).Msg("could not create drive: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -423,27 +427,38 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
return
}
if driveType == _spaceTypeProject {
opaque, err := g.applySpaceTemplate(gatewayClient, resp.GetStorageSpace().GetRoot(), r.URL.Query().Get("template"))
if err != nil {
space := resp.GetStorageSpace()
if t := r.URL.Query().Get(TemplateParameter); t != "" && driveType == _spaceTypeProject {
loc := l10n.MustGetUserLocale(ctx, us.GetId().GetOpaqueId(), r.Header.Get(HeaderAcceptLanguage), g.valueService)
if err := g.applySpaceTemplate(ctx, gatewayClient, space.GetRoot(), t, loc); err != nil {
logger.Error().Err(err).Msg("could not apply template to space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
resp.StorageSpace.Opaque = utils.MergeOpaques(resp.GetStorageSpace().GetOpaque(), opaque)
// refetch the drive to get quota information - should we calculate this ourselves to avoid the extra call?
space, err = utils.GetSpace(ctx, space.GetId().GetOpaqueId(), gatewayClient)
if err != nil {
logger.Error().Err(err).Msg("could not refetch space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
}
newDrive, err := g.cs3StorageSpaceToDrive(r.Context(), webDavBaseURL, resp.GetStorageSpace(), APIVersion_1)
spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, APIVersion_1)
if err != nil {
logger.Debug().Err(err).Msg("could not create drive: error parsing drive")
logger.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
newDrive.Special = g.getSpecialDriveItems(r.Context(), webDavBaseURL, resp.GetStorageSpace())
if len(spaces) == 0 {
logger.Error().Msg("could not convert space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not convert space")
return
}
render.Status(r, http.StatusCreated)
render.JSON(w, r, newDrive)
render.JSON(w, r, spaces[0])
}
// UpdateDrive updates the properties of a storage drive (space).

View File

@@ -764,6 +764,12 @@ var _ = Describe("Graph", func() {
Constraint: v0.Permission_CONSTRAINT_ALL,
},
}, nil)
gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewOK(ctx),
TotalBytes: 500,
}, nil)
jsonBody := []byte(`{"Name": "Test Space", "DriveType": "project", "Description": "This space is for testing", "DriveAlias": "project/testspace"}`)
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
rr := httptest.NewRecorder()

View File

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

View File

@@ -0,0 +1,22 @@
# 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: 2024-03-26 15:11+0100\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"
#: pkg/service/v0/spacetemplates.go:29
msgid "Here you can add a description for this Space."
msgstr ""

View File

@@ -3,7 +3,6 @@ package svc
import (
"crypto/tls"
"crypto/x509"
"embed"
"errors"
"fmt"
"net/http"
@@ -36,11 +35,6 @@ const (
displayNameAttr = "displayName"
)
var (
//go:embed spacetemplate/*
_spaceTemplateFS embed.FS
)
// Service defines the service handlers.
type Service interface {
ServeHTTP(http.ResponseWriter, *http.Request)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
Here you can add a description for this Space.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,133 @@
package svc
import (
"context"
"embed"
"fmt"
"io/fs"
"path/filepath"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
v1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
)
var (
//go:embed spacetemplate/*
_spaceTemplateFS embed.FS
//go:embed l10n/locale
_localeFS embed.FS
// subfolder where the translation files are stored
_localeSubPath = "l10n/locale"
// name of the secret space folder
_spaceFolderName = ".space"
// path to the image file
_imagepath = "spacetemplate/image.png"
// text for the readme.md file
_readmeText = l10n.Template("Here you can add a description for this Space.")
// name of the readme.md file
_readmeName = "readme.md"
// domain of the graph service (transifex)
_domain = "graph"
// HeaderAcceptLanguage is the header key for the accept-language header
HeaderAcceptLanguage = "Accept-Language"
// TemplateParameter is the key for the template parameter in the request
TemplateParameter = "template"
)
func (g Graph) applySpaceTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, template string, locale string) error {
switch template {
default:
fallthrough
case "none":
return nil
case "default":
return g.applyDefaultTemplate(ctx, gwc, root, locale)
}
}
func (g Graph) applyDefaultTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, locale string) error {
mdc := metadata.NewCS3(g.config.Reva.Address, g.config.Spaces.StorageUsersAddress)
mdc.SpaceRoot = root
var opaque *v1beta1.Opaque
// create .space folder
if err := mdc.MakeDirIfNotExist(ctx, _spaceFolderName); err != nil {
return err
}
// upload space image
iid, err := imageUpload(ctx, mdc)
if err != nil {
return err
}
opaque = utils.AppendPlainToOpaque(opaque, SpaceImageSpecialFolderName, iid)
// upload readme.md
rid, err := readmeUpload(ctx, mdc, locale, g.config.Spaces.DefaultLanguage)
if err != nil {
return err
}
opaque = utils.AppendPlainToOpaque(opaque, ReadmeSpecialFolderName, rid)
// update space
resp, err := gwc.UpdateStorageSpace(ctx, &storageprovider.UpdateStorageSpaceRequest{
StorageSpace: &storageprovider.StorageSpace{
Id: &storageprovider.StorageSpaceId{
OpaqueId: storagespace.FormatResourceID(*root),
},
Root: root,
Opaque: opaque,
},
})
switch {
case err != nil:
return err
case resp.GetStatus().GetCode() != rpc.Code_CODE_OK:
return fmt.Errorf("could not update storage space: %s", resp.GetStatus().GetMessage())
default:
return nil
}
}
func imageUpload(ctx context.Context, mdc *metadata.CS3) (string, error) {
b, err := fs.ReadFile(_spaceTemplateFS, _imagepath)
if err != nil {
return "", err
}
res, err := mdc.Upload(ctx, metadata.UploadRequest{
Path: filepath.Join(_spaceFolderName, filepath.Base(_imagepath)),
Content: b,
})
if err != nil {
return "", err
}
return res.FileID, nil
}
func readmeUpload(ctx context.Context, mdc *metadata.CS3, locale string, defaultLocale string) (string, error) {
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
res, err := mdc.Upload(ctx, metadata.UploadRequest{
Path: filepath.Join(_spaceFolderName, _readmeName),
Content: []byte(t.Translate(_readmeText, locale)),
})
if err != nil {
return "", err
}
return res.FileID, nil
}

View File

@@ -3,22 +3,14 @@ package svc
import (
"context"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
v1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"golang.org/x/sync/errgroup"
@@ -445,104 +437,3 @@ func cs3ReceivedShareToLibreGraphPermissions(ctx context.Context, logger *log.Lo
return permission, nil
}
func (g Graph) applySpaceTemplate(gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, template string) (*v1beta1.Opaque, error) {
var fsys fs.ReadDirFS
switch template {
default:
fallthrough
case "none":
return &v1beta1.Opaque{}, nil
case "default":
f, err := fs.Sub(_spaceTemplateFS, "spacetemplate")
if err != nil {
return nil, err
}
fsys = f.(fs.ReadDirFS)
}
mdc := metadata.NewCS3(g.config.Reva.Address, g.config.Spaces.StorageUsersAddress)
mdc.SpaceRoot = root
ctx, err := utils.GetServiceUserContext(g.config.ServiceAccount.ServiceAccountID, gwc, g.config.ServiceAccount.ServiceAccountSecret)
if err != nil {
return nil, err
}
opaque, err := uploadFolder(ctx, mdc, ".", "", nil, fsys)
if err != nil {
return nil, err
}
resp, err := gwc.UpdateStorageSpace(ctx, &storageprovider.UpdateStorageSpaceRequest{
StorageSpace: &storageprovider.StorageSpace{
Id: &storageprovider.StorageSpaceId{
OpaqueId: storagespace.FormatResourceID(*root),
},
Root: root,
Opaque: opaque,
},
})
switch {
case err != nil:
return nil, err
case resp.GetStatus().GetCode() != rpc.Code_CODE_OK:
return nil, fmt.Errorf("could not update storage space: %s", resp.GetStatus().GetMessage())
default:
return opaque, nil
}
}
func uploadFolder(ctx context.Context, mdc *metadata.CS3, pathOnDisc, pathOnSpace string, opaque *v1beta1.Opaque, fsys fs.ReadDirFS) (*v1beta1.Opaque, error) {
entries, err := fsys.ReadDir(pathOnDisc)
if err != nil {
return nil, err
}
for _, entry := range entries {
opaque, err = uploadEntry(ctx, mdc, pathOnDisc, pathOnSpace, opaque, fsys, entry)
if err != nil {
return nil, err
}
}
return opaque, nil
}
func uploadEntry(ctx context.Context, mdc *metadata.CS3, pathOnDisc, pathOnSpace string, opaque *v1beta1.Opaque, fsys fs.ReadDirFS, entry os.DirEntry) (*v1beta1.Opaque, error) {
spacePath := filepath.Join(pathOnSpace, entry.Name())
discPath := filepath.Join(pathOnDisc, entry.Name())
switch {
case entry.IsDir():
err := mdc.MakeDirIfNotExist(ctx, spacePath)
if err != nil {
return nil, err
}
return uploadFolder(ctx, mdc, discPath, spacePath, opaque, fsys)
default:
b, err := fs.ReadFile(fsys, discPath)
if err != nil {
return nil, err
}
res, err := mdc.Upload(ctx, metadata.UploadRequest{
Path: spacePath,
Content: b,
})
if err != nil {
return nil, err
}
identifier := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
for _, special := range []string{ReadmeSpecialFolderName, SpaceImageSpecialFolderName} {
if special == identifier {
opaque = utils.AppendPlainToOpaque(opaque, identifier, res.FileID)
break
}
}
return opaque, nil
}
}

View File

@@ -44,7 +44,7 @@ l10n-push:
.PHONY: l10n-read
l10n-read: $(GO_XGETTEXT)
go-xgettext -o $(OUTPUT_DIR)/notifications.pot --keyword=Template --add-comments -s pkg/email/templates.go
go-xgettext -o $(OUTPUT_DIR)/notifications.pot --keyword=l10n.Template --add-comments -s pkg/email/templates.go
.PHONY: l10n-write
l10n-write:

View File

@@ -2,29 +2,36 @@ package email
import (
"bytes"
"embed"
"strings"
"text/template"
"github.com/owncloud/ocis/v2/services/notifications/pkg/email/l10n"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
)
var (
//go:embed l10n/locale
_translationFS embed.FS
_domain = "notifications"
)
// NewTextTemplate replace the body message template placeholders with the translated template
func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
var err error
t := l10n.NewTranslator(locale, defaultLocale, translationPath)
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars)
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale)
mt.Subject, err = composeMessage(t.Get(mt.Subject), vars)
if err != nil {
return mt, err
}
mt.Greeting, err = composeMessage(t.Translate(mt.Greeting), vars)
mt.Greeting, err = composeMessage(t.Get(mt.Greeting), vars)
if err != nil {
return mt, err
}
mt.MessageBody, err = composeMessage(t.Translate(mt.MessageBody), vars)
mt.MessageBody, err = composeMessage(t.Get(mt.MessageBody), vars)
if err != nil {
return mt, err
}
mt.CallToAction, err = composeMessage(t.Translate(mt.CallToAction), vars)
mt.CallToAction, err = composeMessage(t.Get(mt.CallToAction), vars)
if err != nil {
return mt, err
}
@@ -34,20 +41,20 @@ func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translati
// NewHTMLTemplate replace the body message template placeholders with the translated template
func NewHTMLTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
var err error
t := l10n.NewTranslator(locale, defaultLocale, translationPath)
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars)
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale)
mt.Subject, err = composeMessage(t.Get(mt.Subject), vars)
if err != nil {
return mt, err
}
mt.Greeting, err = composeMessage(newlineToBr(t.Translate(mt.Greeting)), vars)
mt.Greeting, err = composeMessage(newlineToBr(t.Get(mt.Greeting)), vars)
if err != nil {
return mt, err
}
mt.MessageBody, err = composeMessage(newlineToBr(t.Translate(mt.MessageBody)), vars)
mt.MessageBody, err = composeMessage(newlineToBr(t.Get(mt.MessageBody)), vars)
if err != nil {
return mt, err
}
mt.CallToAction, err = composeMessage(callToActionToHTML(t.Translate(mt.CallToAction)), vars)
mt.CallToAction, err = composeMessage(callToActionToHTML(t.Get(mt.CallToAction)), vars)
if err != nil {
return mt, err
}

View File

@@ -1,51 +0,0 @@
// Package l10n implements utility for translation the text templates.
//
// The l10n package use transifex translation for text templates.
package l10n
import (
"embed"
"io/fs"
"github.com/leonelquinteros/gotext"
)
var (
//go:embed locale
_translationFS embed.FS
_domain = "notifications"
)
// Translator is the interface to the translation
type Translator interface {
Translate(str string) string
}
type translator struct {
locale *gotext.Locale
}
// NewTranslator Create Translator with library path and language code and load default domain
func NewTranslator(locale, defaultLocale string, path string) Translator {
l := newLocate(locale, path)
if locale != "en" && len(l.GetTranslations()) == 0 {
l = newLocate(defaultLocale, path)
}
return &translator{locale: l}
}
func newLocate(local string, path string) *gotext.Locale {
var l *gotext.Locale
if path == "" {
filesystem, _ := fs.Sub(_translationFS, "locale")
l = gotext.NewLocaleFS(local, filesystem)
} else { // use custom path instead
l = gotext.NewLocale(path, local)
}
l.AddDomain(_domain) // make domain configurable only if needed
return l
}
func (t *translator) Translate(str string) string {
return t.locale.Get(str)
}

View File

@@ -1,7 +1,6 @@
package email
// Template marks the string as a translatable template
func Template(s string) string { return s }
import "github.com/owncloud/ocis/v2/ocis-pkg/l10n"
// the available templates
var (
@@ -10,24 +9,24 @@ var (
textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl",
// ShareCreated email template, Subject field (resolves directly)
Subject: Template(`{ShareSharer} shared '{ShareFolder}' with you`),
Subject: l10n.Template(`{ShareSharer} shared '{ShareFolder}' with you`),
// ShareCreated email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {ShareGrantee}`),
Greeting: l10n.Template(`Hello {ShareGrantee}`),
// ShareCreated email template, resolves via {{ .MessageBody }}
MessageBody: Template(`{ShareSharer} has shared "{ShareFolder}" with you.`),
MessageBody: l10n.Template(`{ShareSharer} has shared "{ShareFolder}" with you.`),
// ShareCreated email template, resolves via {{ .CallToAction }}
CallToAction: Template(`Click here to view it: {ShareLink}`),
CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
}
ShareExpired = MessageTemplate{
textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl",
// ShareExpired email template, Subject field (resolves directly)
Subject: Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`),
Subject: l10n.Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`),
// ShareExpired email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {ShareGrantee},`),
Greeting: l10n.Template(`Hello {ShareGrantee},`),
// ShareExpired email template, resolves via {{ .MessageBody }}
MessageBody: Template(`Your share to {ShareFolder} has expired at {ExpiredAt}
MessageBody: l10n.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.`),
}
@@ -37,39 +36,39 @@ Even though this share has been revoked you still might have access through othe
textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl",
// SharedSpace email template, Subject field (resolves directly)
Subject: Template("{SpaceSharer} invited you to join {SpaceName}"),
Subject: l10n.Template("{SpaceSharer} invited you to join {SpaceName}"),
// SharedSpace email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {SpaceGrantee},`),
Greeting: l10n.Template(`Hello {SpaceGrantee},`),
// SharedSpace email template, resolves via {{ .MessageBody }}
MessageBody: Template(`{SpaceSharer} has invited you to join "{SpaceName}".`),
MessageBody: l10n.Template(`{SpaceSharer} has invited you to join "{SpaceName}".`),
// SharedSpace email template, resolves via {{ .CallToAction }}
CallToAction: Template(`Click here to view it: {ShareLink}`),
CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
}
UnsharedSpace = MessageTemplate{
textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl",
// UnsharedSpace email template, Subject field (resolves directly)
Subject: Template(`{SpaceSharer} removed you from {SpaceName}`),
Subject: l10n.Template(`{SpaceSharer} removed you from {SpaceName}`),
// UnsharedSpace email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {SpaceGrantee},`),
Greeting: l10n.Template(`Hello {SpaceGrantee},`),
// UnsharedSpace email template, resolves via {{ .MessageBody }}
MessageBody: Template(`{SpaceSharer} has removed you from "{SpaceName}".
MessageBody: l10n.Template(`{SpaceSharer} has removed you from "{SpaceName}".
You might still have access through your other groups or direct membership.`),
// UnsharedSpace email template, resolves via {{ .CallToAction }}
CallToAction: Template(`Click here to check it: {ShareLink}`),
CallToAction: l10n.Template(`Click here to check it: {ShareLink}`),
}
MembershipExpired = MessageTemplate{
textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl",
// MembershipExpired email template, Subject field (resolves directly)
Subject: Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`),
Subject: l10n.Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`),
// MembershipExpired email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {SpaceGrantee},`),
Greeting: l10n.Template(`Hello {SpaceGrantee},`),
// MembershipExpired email template, resolves via {{ .MessageBody }}
MessageBody: Template(`Your membership of space {SpaceName} has expired at {ExpiredAt}
MessageBody: l10n.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`),
}

View File

@@ -18,6 +18,7 @@ import (
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/l10n"
"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"
@@ -28,8 +29,6 @@ import (
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
var _defaultLocale = "en"
// Service should be named `Runner`
type Service interface {
Run() error
@@ -108,7 +107,7 @@ func (s eventsNotifier) render(ctx context.Context, template email.MessageTempla
// Render the Email Template for each user
messageList := make([]*channels.Message, len(granteeList))
for i, usr := range granteeList {
locale := s.getUserLang(ctx, usr.GetId())
locale := l10n.MustGetUserLocale(ctx, usr.GetId().GetOpaqueId(), "", s.valueService)
fields[granteeFieldName] = usr.GetDisplayName()
rendered, err := email.RenderEmailTemplate(template, locale, s.defaultLanguage, s.emailTemplatePath, s.translationPath, fields)
@@ -215,22 +214,6 @@ func (s eventsNotifier) getUser(ctx context.Context, u *user.UserId) (*user.User
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,

View File

@@ -44,7 +44,7 @@ l10n-push:
.PHONY: l10n-read
l10n-read: $(GO_XGETTEXT)
go-xgettext -o $(OUTPUT_DIR)/userlog.pot --keyword=Template -s pkg/service/templates.go
go-xgettext -o $(OUTPUT_DIR)/userlog.pot --keyword=l10n.Template -s pkg/service/templates.go
.PHONY: l10n-write
l10n-write:

View File

@@ -6,7 +6,6 @@ import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"strings"
"text/template"
"time"
@@ -18,7 +17,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/leonelquinteros/gotext"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
)
//go:embed l10n/locale
@@ -338,7 +337,7 @@ func (c *Converter) getResource(ctx context.Context, resourceID *storageprovider
return resource, err
}
func (c *Converter) getUser(ctx context.Context, userID *user.UserId) (*user.User, error) {
func (c *Converter) getUser(_ context.Context, userID *user.UserId) (*user.User, error) {
if u, ok := c.users[userID.GetOpaqueId()]; ok {
return u, nil
}
@@ -361,25 +360,9 @@ func composeMessage(nt NotificationTemplate, locale, defaultLocale, path string,
return subject, subjectraw, message, messageraw, err
}
func newLocate(locale string, path string) *gotext.Locale {
// 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
}
func loadTemplates(nt NotificationTemplate, locale, defaultLocale, path string) (string, string) {
l := newLocate(locale, path)
if locale != "en" && len(l.GetTranslations()) == 0 {
l = newLocate(defaultLocale, path)
}
return l.Get(nt.Subject), l.Get(nt.Message)
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, path, _translationFS, "l10n/locale").Locale(locale)
return t.Get(nt.Subject), t.Get(nt.Message)
}
func executeTemplate(raw string, vars map[string]interface{}) (string, error) {

View File

@@ -11,20 +11,18 @@ import (
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/go-chi/chi/v5"
micrometadata "go-micro.dev/v4/metadata"
"go-micro.dev/v4/store"
"go.opentelemetry.io/otel/trace"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0"
ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
"github.com/owncloud/ocis/v2/services/userlog/pkg/config"
)
@@ -343,7 +341,7 @@ func (ul *UserlogService) sendSSE(ctx context.Context, userIDs []string, event e
m := make(map[string]events.SendSSE)
for _, userid := range userIDs {
loc := ul.getUserLocale(userid)
loc := l10n.MustGetUserLocale(ctx, userid, "", ul.valueClient)
if ev, ok := m[loc]; ok {
ev.UserIDs = append(m[loc].UserIDs, userid)
m[loc] = ev
@@ -455,25 +453,6 @@ func (ul *UserlogService) alterGlobalEvents(ctx context.Context, alter func(map[
})
}
func (ul *UserlogService) getUserLocale(userid string) string {
resp, err := ul.valueClient.GetValueByUniqueIdentifiers(
micrometadata.Set(context.Background(), middleware.AccountID, userid),
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userid,
SettingId: defaults.SettingUUIDProfileLanguage,
},
)
if err != nil {
ul.log.Error().Err(err).Str("userid", userid).Msg("cannot get users locale")
return ""
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return ""
}
return val[0].GetStringValue()
}
func removeExecutant(users []string, executant *user.UserId) []string {
var usrs []string
for _, u := range users {

View File

@@ -1,63 +1,62 @@
package service
// Template marks the string as a translatable template
func Template(s string) string { return s }
import "github.com/owncloud/ocis/v2/ocis-pkg/l10n"
// the available templates
var (
VirusFound = NotificationTemplate{
Subject: Template("Virus found"),
Message: Template("Virus found in {resource}. Upload not possible. Virus: {virus}"),
Subject: l10n.Template("Virus found"),
Message: l10n.Template("Virus found in {resource}. Upload not possible. Virus: {virus}"),
}
PoliciesEnforced = NotificationTemplate{
Subject: Template("Policies enforced"),
Message: Template("File {resource} was deleted because it violates the policies"),
Subject: l10n.Template("Policies enforced"),
Message: l10n.Template("File {resource} was deleted because it violates the policies"),
}
SpaceShared = NotificationTemplate{
Subject: Template("Space shared"),
Message: Template("{user} added you to Space {space}"),
Subject: l10n.Template("Space shared"),
Message: l10n.Template("{user} added you to Space {space}"),
}
SpaceUnshared = NotificationTemplate{
Subject: Template("Removed from Space"),
Message: Template("{user} removed you from Space {space}"),
Subject: l10n.Template("Removed from Space"),
Message: l10n.Template("{user} removed you from Space {space}"),
}
SpaceDisabled = NotificationTemplate{
Subject: Template("Space disabled"),
Message: Template("{user} disabled Space {space}"),
Subject: l10n.Template("Space disabled"),
Message: l10n.Template("{user} disabled Space {space}"),
}
SpaceDeleted = NotificationTemplate{
Subject: Template("Space deleted"),
Message: Template("{user} deleted Space {space}"),
Subject: l10n.Template("Space deleted"),
Message: l10n.Template("{user} deleted Space {space}"),
}
SpaceMembershipExpired = NotificationTemplate{
Subject: Template("Membership expired"),
Message: Template("Access to Space {space} lost"),
Subject: l10n.Template("Membership expired"),
Message: l10n.Template("Access to Space {space} lost"),
}
ShareCreated = NotificationTemplate{
Subject: Template("Resource shared"),
Message: Template("{user} shared {resource} with you"),
Subject: l10n.Template("Resource shared"),
Message: l10n.Template("{user} shared {resource} with you"),
}
ShareRemoved = NotificationTemplate{
Subject: Template("Resource unshared"),
Message: Template("{user} unshared {resource} with you"),
Subject: l10n.Template("Resource unshared"),
Message: l10n.Template("{user} unshared {resource} with you"),
}
ShareExpired = NotificationTemplate{
Subject: Template("Share expired"),
Message: Template("Access to {resource} expired"),
Subject: l10n.Template("Share expired"),
Message: l10n.Template("Access to {resource} expired"),
}
PlatformDeprovision = NotificationTemplate{
Subject: Template("Instance will be shut down and deprovisioned"),
Message: Template("Attention! The instance will be shut down and deprovisioned on {date}. Download all your data before that date as no access past that date is possible."),
Subject: l10n.Template("Instance will be shut down and deprovisioned"),
Message: l10n.Template("Attention! The instance will be shut down and deprovisioned on {date}. Download all your data before that date as no access past that date is possible."),
}
)