diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 2dfa10bf8..559efc036 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -1,4 +1,4 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.7. DO NOT EDIT. +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.6. DO NOT EDIT. # All tools are designed to be build inside $GOBIN. BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST))) GOPATH ?= $(shell go env GOPATH) @@ -21,95 +21,101 @@ BINGO := $(GOBIN)/bingo-v0.7.0 $(BINGO): $(BINGO_DIR)/bingo.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/bingo-v0.7.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=bingo.mod -o=$(GOBIN)/bingo-v0.7.0 "github.com/bwplotka/bingo" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=bingo.mod -o=$(GOBIN)/bingo-v0.7.0 "github.com/bwplotka/bingo" BUF := $(GOBIN)/buf-v1.3.1 $(BUF): $(BINGO_DIR)/buf.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/buf-v1.3.1" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=buf.mod -o=$(GOBIN)/buf-v1.3.1 "github.com/bufbuild/buf/cmd/buf" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=buf.mod -o=$(GOBIN)/buf-v1.3.1 "github.com/bufbuild/buf/cmd/buf" BUILDIFIER := $(GOBIN)/buildifier-v0.0.0-20220323134444-a9f46b2bb3de $(BUILDIFIER): $(BINGO_DIR)/buildifier.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/buildifier-v0.0.0-20220323134444-a9f46b2bb3de" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=buildifier.mod -o=$(GOBIN)/buildifier-v0.0.0-20220323134444-a9f46b2bb3de "github.com/bazelbuild/buildtools/buildifier" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=buildifier.mod -o=$(GOBIN)/buildifier-v0.0.0-20220323134444-a9f46b2bb3de "github.com/bazelbuild/buildtools/buildifier" CALENS := $(GOBIN)/calens-v0.2.0 $(CALENS): $(BINGO_DIR)/calens.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/calens-v0.2.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=calens.mod -o=$(GOBIN)/calens-v0.2.0 "github.com/restic/calens" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=calens.mod -o=$(GOBIN)/calens-v0.2.0 "github.com/restic/calens" GO_LICENSES := $(GOBIN)/go-licenses-v1.5.0 $(GO_LICENSES): $(BINGO_DIR)/go-licenses.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/go-licenses-v1.5.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=go-licenses.mod -o=$(GOBIN)/go-licenses-v1.5.0 "github.com/google/go-licenses" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=go-licenses.mod -o=$(GOBIN)/go-licenses-v1.5.0 "github.com/google/go-licenses" + +GO_XGETTEXT := $(GOBIN)/go-xgettext-v0.0.0-20160830220431-74466a0a0c4a +$(GO_XGETTEXT): $(BINGO_DIR)/go-xgettext.mod + @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. + @echo "(re)installing $(GOBIN)/go-xgettext-v0.0.0-20160830220431-74466a0a0c4a" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=go-xgettext.mod -o=$(GOBIN)/go-xgettext-v0.0.0-20160830220431-74466a0a0c4a "github.com/gosexy/gettext/go-xgettext" GOLANGCI_LINT := $(GOBIN)/golangci-lint-v1.47.3 $(GOLANGCI_LINT): $(BINGO_DIR)/golangci-lint.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/golangci-lint-v1.47.3" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v1.47.3 "github.com/golangci/golangci-lint/cmd/golangci-lint" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v1.47.3 "github.com/golangci/golangci-lint/cmd/golangci-lint" HUGO := $(GOBIN)/hugo-v0.94.0 $(HUGO): $(BINGO_DIR)/hugo.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/hugo-v0.94.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=hugo.mod -o=$(GOBIN)/hugo-v0.94.0 "github.com/gohugoio/hugo" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=hugo.mod -o=$(GOBIN)/hugo-v0.94.0 "github.com/gohugoio/hugo" MOCKERY := $(GOBIN)/mockery-v2.14.1 $(MOCKERY): $(BINGO_DIR)/mockery.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/mockery-v2.14.1" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=mockery.mod -o=$(GOBIN)/mockery-v2.14.1 "github.com/vektra/mockery/v2" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=mockery.mod -o=$(GOBIN)/mockery-v2.14.1 "github.com/vektra/mockery/v2" MUTAGEN := $(GOBIN)/mutagen-v0.13.1 $(MUTAGEN): $(BINGO_DIR)/mutagen.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/mutagen-v0.13.1" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=mutagen.mod -o=$(GOBIN)/mutagen-v0.13.1 "github.com/mutagen-io/mutagen/cmd/mutagen" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=mutagen.mod -o=$(GOBIN)/mutagen-v0.13.1 "github.com/mutagen-io/mutagen/cmd/mutagen" PROTOC_GEN_DOC := $(GOBIN)/protoc-gen-doc-v1.5.1 $(PROTOC_GEN_DOC): $(BINGO_DIR)/protoc-gen-doc.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/protoc-gen-doc-v1.5.1" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=protoc-gen-doc.mod -o=$(GOBIN)/protoc-gen-doc-v1.5.1 "github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=protoc-gen-doc.mod -o=$(GOBIN)/protoc-gen-doc-v1.5.1 "github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc" PROTOC_GEN_GO := $(GOBIN)/protoc-gen-go-v1.28.1 $(PROTOC_GEN_GO): $(BINGO_DIR)/protoc-gen-go.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/protoc-gen-go-v1.28.1" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=protoc-gen-go.mod -o=$(GOBIN)/protoc-gen-go-v1.28.1 "google.golang.org/protobuf/cmd/protoc-gen-go" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=protoc-gen-go.mod -o=$(GOBIN)/protoc-gen-go-v1.28.1 "google.golang.org/protobuf/cmd/protoc-gen-go" PROTOC_GEN_MICRO := $(GOBIN)/protoc-gen-micro-v1.0.0 $(PROTOC_GEN_MICRO): $(BINGO_DIR)/protoc-gen-micro.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/protoc-gen-micro-v1.0.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=protoc-gen-micro.mod -o=$(GOBIN)/protoc-gen-micro-v1.0.0 "github.com/go-micro/generator/cmd/protoc-gen-micro" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=protoc-gen-micro.mod -o=$(GOBIN)/protoc-gen-micro-v1.0.0 "github.com/go-micro/generator/cmd/protoc-gen-micro" PROTOC_GEN_MICROWEB := $(GOBIN)/protoc-gen-microweb-v0.0.0-20220808092353-b5d6c3960e19 $(PROTOC_GEN_MICROWEB): $(BINGO_DIR)/protoc-gen-microweb.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/protoc-gen-microweb-v0.0.0-20220808092353-b5d6c3960e19" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=protoc-gen-microweb.mod -o=$(GOBIN)/protoc-gen-microweb-v0.0.0-20220808092353-b5d6c3960e19 "github.com/owncloud/protoc-gen-microweb" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=protoc-gen-microweb.mod -o=$(GOBIN)/protoc-gen-microweb-v0.0.0-20220808092353-b5d6c3960e19 "github.com/owncloud/protoc-gen-microweb" PROTOC_GEN_OPENAPIV2 := $(GOBIN)/protoc-gen-openapiv2-v2.13.0 $(PROTOC_GEN_OPENAPIV2): $(BINGO_DIR)/protoc-gen-openapiv2.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/protoc-gen-openapiv2-v2.13.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=protoc-gen-openapiv2.mod -o=$(GOBIN)/protoc-gen-openapiv2-v2.13.0 "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=protoc-gen-openapiv2.mod -o=$(GOBIN)/protoc-gen-openapiv2-v2.13.0 "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" PROTOC_GO_INJECT_TAG := $(GOBIN)/protoc-go-inject-tag-v1.4.0 $(PROTOC_GO_INJECT_TAG): $(BINGO_DIR)/protoc-go-inject-tag.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/protoc-go-inject-tag-v1.4.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=protoc-go-inject-tag.mod -o=$(GOBIN)/protoc-go-inject-tag-v1.4.0 "github.com/favadi/protoc-go-inject-tag" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=protoc-go-inject-tag.mod -o=$(GOBIN)/protoc-go-inject-tag-v1.4.0 "github.com/favadi/protoc-go-inject-tag" REFLEX := $(GOBIN)/reflex-v0.3.1 $(REFLEX): $(BINGO_DIR)/reflex.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. @echo "(re)installing $(GOBIN)/reflex-v0.3.1" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=reflex.mod -o=$(GOBIN)/reflex-v0.3.1 "github.com/cespare/reflex" + @cd $(BINGO_DIR) && $(GO) build -mod=mod -modfile=reflex.mod -o=$(GOBIN)/reflex-v0.3.1 "github.com/cespare/reflex" diff --git a/.bingo/variables.env b/.bingo/variables.env index 2044ef411..6ccf239b2 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -1,4 +1,4 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.7. DO NOT EDIT. +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.6. DO NOT EDIT. # All tools are designed to be build inside $GOBIN. # Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk. GOBIN=${GOBIN:=$(go env GOBIN)} @@ -18,6 +18,8 @@ CALENS="${GOBIN}/calens-v0.2.0" GO_LICENSES="${GOBIN}/go-licenses-v1.5.0" +GO_XGETTEXT="${GOBIN}/go-xgettext-v0.0.0-20160830220431-74466a0a0c4a" + GOLANGCI_LINT="${GOBIN}/golangci-lint-v1.47.3" HUGO="${GOBIN}/hugo-v0.94.0" diff --git a/.gitignore b/.gitignore index 68c73746e..6a402cb68 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ vendor-php # drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml .drone.yml -**/l10n/locale **/l10n/template.pot # protogen autogenerated diff --git a/Makefile b/Makefile index 1e30910b7..a09fc7d2c 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,9 @@ WHITE := $(shell tput -Txterm setaf 7) RESET := $(shell tput -Txterm sgr0) -L10N_MODULES := $(shell find . -path '*.tx*' -name 'config' | sed 's|/[^/]*$$||' | sed 's|/[^/]*$$||' | sed 's|/[^/]*$$||') +# add a service here when it uses transifex +L10N_MODULES := \ + services/userlog # if you add a module here please also add it to the .drone.star file OCIS_MODULES = \ diff --git a/go.mod b/go.mod index 0d8c052ed..8aebff910 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/jellydator/ttlcache/v2 v2.11.1 github.com/jellydator/ttlcache/v3 v3.0.1 github.com/justinas/alice v1.2.0 + github.com/leonelquinteros/gotext v1.5.2 github.com/libregraph/idm v0.4.1-0.20230221143410-3503963047a5 github.com/libregraph/lico v0.59.4 github.com/mitchellh/mapstructure v1.5.0 @@ -295,3 +296,5 @@ require ( ) replace github.com/cs3org/go-cs3apis => github.com/c0rby/go-cs3apis v0.0.0-20230110100311-5b424f1baa35 + +replace github.com/leonelquinteros/gotext => github.com/kobergj/gotext v0.0.0-20230309141732-b909eb0b8956 diff --git a/go.sum b/go.sum index 1009b517b..698dca8f5 100644 --- a/go.sum +++ b/go.sum @@ -866,6 +866,8 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kobergj/gotext v0.0.0-20230309141732-b909eb0b8956 h1:b3MBTVgsfKXD/CdnAAqD7czu9Npx9QG+rHZycBIXJGE= +github.com/kobergj/gotext v0.0.0-20230309141732-b909eb0b8956/go.mod h1:AT4NpQrOmyj1L/+hLja6aR0lk81yYYL4ePnj2kp7d6M= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/services/userlog/Makefile b/services/userlog/Makefile index 12a1d0a7d..f5a0bb752 100644 --- a/services/userlog/Makefile +++ b/services/userlog/Makefile @@ -1,6 +1,10 @@ SHELL := bash NAME := userlog +# Where to write the files generated by this makefile. +OUTPUT_DIR = ./pkg/service/l10n +TEMPLATE_FILE = ./pkg/service/l10n/locale/userlog.pot + include ../../.make/recursion.mk ############ tooling ############ @@ -30,6 +34,26 @@ 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 -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)/locale/userlog.pot --keyword=Template -s pkg/service/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: diff --git a/services/userlog/README.md b/services/userlog/README.md index 352e76fa9..a9c3476f3 100644 --- a/services/userlog/README.md +++ b/services/userlog/README.md @@ -31,3 +31,20 @@ The `userlog` service provides an API to retrieve configured events. For now, th ## Deleting To delete events for an user, use a `DELETE` request to `ocs/v2.php/apps/notifications/api/v1/notifications` containing the IDs to delete. + +## Translations + +The `userlog` 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 `USERLOG_TRANSLATION_PATH` environment variable needs to point to a base folder that will further contain the translation files. This path must be available from all instances of the userlog 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 `userlog.po` (or `userlog.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 +{USERLOG_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/userlog.po +``` + +The language-code pattern is composed as `language[_territory]` where `language` is the base language and `_territory` is optional defining a country. + +As example, for the language `de_DE`, one needs to place the corresponding translation files to `{USERLOG_TRANSLATION_PATH}/de_DE/LC_MESSAGES/userlog.po`. + +### Translation Rules + +* If a requested language-code is not available, the service tries to fallback to the base language if available. As example, if `de_DE` is not available, the service tries to fall back to translations in `de` folder. +* If the base language is also not available like when the language code is `de_DE` and neither `de_DE` nor the `de` folder is available, the service falls back to the systems default `en`, which is the source of the texts provided by the code. diff --git a/services/userlog/pkg/config/config.go b/services/userlog/pkg/config/config.go index 04e74cf3b..c7d18c005 100644 --- a/services/userlog/pkg/config/config.go +++ b/services/userlog/pkg/config/config.go @@ -22,6 +22,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:"REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata"` + 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."` Events Events `yaml:"events"` Store Store `yaml:"store"` diff --git a/services/userlog/pkg/service/conversion.go b/services/userlog/pkg/service/conversion.go index a34635089..15b6600ef 100644 --- a/services/userlog/pkg/service/conversion.go +++ b/services/userlog/pkg/service/conversion.go @@ -2,22 +2,32 @@ package service import ( "bytes" + "context" + "embed" "errors" + "strings" "text/template" "time" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "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" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ) +//go:embed l10n/locale +var _translationFS embed.FS + var ( _resourceTypeSpace = "storagespace" _resourceTypeShare = "share" + + _domain = "userlog" ) // OC10Notification is the oc10 style representation of an event @@ -36,9 +46,41 @@ type OC10Notification struct { MessageDetails map[string]interface{} `json:"messageRichParameters"` } +// Converter is responsible for converting eventhistory events to OC10Notifications +type Converter struct { + locale string + gwClient gateway.GatewayAPIClient + machineAuthAPIKey string + serviceName string + registeredEvents map[string]events.Unmarshaller + translationPath string + + // cached within one request not to query other service too much + spaces map[string]*storageprovider.StorageSpace + users map[string]*user.User + resources map[string]*storageprovider.ResourceInfo + contexts map[string]context.Context +} + +// NewConverter returns a new Converter +func NewConverter(loc string, gwc gateway.GatewayAPIClient, machineAuthAPIKey string, name string, translationPath string, registeredEvents map[string]events.Unmarshaller) *Converter { + return &Converter{ + locale: loc, + gwClient: gwc, + machineAuthAPIKey: machineAuthAPIKey, + serviceName: name, + registeredEvents: registeredEvents, + translationPath: translationPath, + spaces: make(map[string]*storageprovider.StorageSpace), + users: make(map[string]*user.User), + resources: make(map[string]*storageprovider.ResourceInfo), + contexts: make(map[string]context.Context), + } +} + // ConvertEvent converts an eventhistory event to an OC10Notification -func (ul *UserlogService) ConvertEvent(event *ehmsg.Event) (OC10Notification, error) { - etype, ok := ul.registeredEvents[event.Type] +func (c *Converter) ConvertEvent(event *ehmsg.Event) (OC10Notification, error) { + etype, ok := c.registeredEvents[event.Type] if !ok { // this should not happen return OC10Notification{}, errors.New("eventtype not registered") @@ -55,50 +97,46 @@ func (ul *UserlogService) ConvertEvent(event *ehmsg.Event) (OC10Notification, er return OC10Notification{}, errors.New("unknown event type") // space related case events.SpaceDisabled: - return ul.spaceMessage(event.Id, SpaceDisabled, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) + return c.spaceMessage(event.Id, SpaceDisabled, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) case events.SpaceDeleted: - return ul.spaceDeletedMessage(event.Id, ev.Executant, ev.ID.GetOpaqueId(), ev.SpaceName, ev.Timestamp) + return c.spaceDeletedMessage(event.Id, ev.Executant, ev.ID.GetOpaqueId(), ev.SpaceName, ev.Timestamp) case events.SpaceShared: - return ul.spaceMessage(event.Id, SpaceShared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) + return c.spaceMessage(event.Id, SpaceShared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) case events.SpaceUnshared: - return ul.spaceMessage(event.Id, SpaceUnshared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) + return c.spaceMessage(event.Id, SpaceUnshared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) case events.SpaceMembershipExpired: - return ul.spaceMessage(event.Id, SpaceMembershipExpired, ev.SpaceOwner, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt) + return c.spaceMessage(event.Id, SpaceMembershipExpired, ev.SpaceOwner, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt) // share related case events.ShareCreated: - return ul.shareMessage(event.Id, ShareCreated, ev.Executant, ev.ItemID, ev.ShareID, utils.TSToTime(ev.CTime)) + return c.shareMessage(event.Id, ShareCreated, ev.Executant, ev.ItemID, ev.ShareID, utils.TSToTime(ev.CTime)) case events.ShareExpired: - return ul.shareMessage(event.Id, ShareExpired, ev.ShareOwner, ev.ItemID, ev.ShareID, ev.ExpiredAt) + return c.shareMessage(event.Id, ShareExpired, ev.ShareOwner, ev.ItemID, ev.ShareID, ev.ExpiredAt) case events.ShareRemoved: - return ul.shareMessage(event.Id, ShareRemoved, ev.Executant, ev.ItemID, ev.ShareID, ev.Timestamp) + return c.shareMessage(event.Id, ShareRemoved, ev.Executant, ev.ItemID, ev.ShareID, ev.Timestamp) } } -func (ul *UserlogService) spaceDeletedMessage(eventid string, executant *user.UserId, spaceid string, spacename string, ts time.Time) (OC10Notification, error) { - _, user, err := utils.Impersonate(executant, ul.gwClient, ul.cfg.MachineAuthAPIKey) +func (c *Converter) spaceDeletedMessage(eventid string, executant *user.UserId, spaceid string, spacename string, ts time.Time) (OC10Notification, error) { + usr, err := c.getUser(context.Background(), executant) if err != nil { return OC10Notification{}, err } - subj, subjraw, msg, msgraw, err := ul.composeMessage(SpaceDeleted, map[string]string{ - "username": user.GetDisplayName(), + subj, subjraw, msg, msgraw, err := composeMessage(SpaceDeleted, c.locale, c.translationPath, map[string]interface{}{ + "username": usr.GetDisplayName(), "spacename": spacename, }) if err != nil { return OC10Notification{}, err } - details := ul.getDetails(user, nil, nil, nil) - details["space"] = map[string]string{ - "id": spaceid, - "name": spacename, - } + space := &storageprovider.StorageSpace{Id: &storageprovider.StorageSpaceId{OpaqueId: spaceid}, Name: spacename} return OC10Notification{ EventID: eventid, - Service: ul.cfg.Service.Name, - UserName: user.GetUsername(), + Service: c.serviceName, + UserName: usr.GetUsername(), Timestamp: ts.Format(time.RFC3339Nano), ResourceID: spaceid, ResourceType: _resourceTypeSpace, @@ -106,23 +144,28 @@ func (ul *UserlogService) spaceDeletedMessage(eventid string, executant *user.Us SubjectRaw: subjraw, Message: msg, MessageRaw: msgraw, - MessageDetails: details, + MessageDetails: generateDetails(usr, space, nil, nil), }, nil } -func (ul *UserlogService) spaceMessage(eventid string, eventname string, executant *user.UserId, spaceid string, ts time.Time) (OC10Notification, error) { - ctx, user, err := utils.Impersonate(executant, ul.gwClient, ul.cfg.MachineAuthAPIKey) +func (c *Converter) spaceMessage(eventid string, nt NotificationTemplate, executant *user.UserId, spaceid string, ts time.Time) (OC10Notification, error) { + usr, err := c.getUser(context.Background(), executant) if err != nil { return OC10Notification{}, err } - space, err := ul.getSpace(ctx, spaceid) + ctx, err := c.authenticate(usr) if err != nil { return OC10Notification{}, err } - subj, subjraw, msg, msgraw, err := ul.composeMessage(eventname, map[string]string{ - "username": user.GetDisplayName(), + space, err := c.getSpace(ctx, spaceid) + if err != nil { + return OC10Notification{}, err + } + + subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{ + "username": usr.GetDisplayName(), "spacename": space.GetName(), }) if err != nil { @@ -131,8 +174,8 @@ func (ul *UserlogService) spaceMessage(eventid string, eventname string, executa return OC10Notification{ EventID: eventid, - Service: ul.cfg.Service.Name, - UserName: user.GetUsername(), + Service: c.serviceName, + UserName: usr.GetUsername(), Timestamp: ts.Format(time.RFC3339Nano), ResourceID: spaceid, ResourceType: _resourceTypeSpace, @@ -140,23 +183,28 @@ func (ul *UserlogService) spaceMessage(eventid string, eventname string, executa SubjectRaw: subjraw, Message: msg, MessageRaw: msgraw, - MessageDetails: ul.getDetails(user, space, nil, nil), + MessageDetails: generateDetails(usr, space, nil, nil), }, nil } -func (ul *UserlogService) shareMessage(eventid string, eventname string, executant *user.UserId, resourceid *storageprovider.ResourceId, shareid *collaboration.ShareId, ts time.Time) (OC10Notification, error) { - ctx, user, err := utils.Impersonate(executant, ul.gwClient, ul.cfg.MachineAuthAPIKey) +func (c *Converter) shareMessage(eventid string, nt NotificationTemplate, executant *user.UserId, resourceid *storageprovider.ResourceId, shareid *collaboration.ShareId, ts time.Time) (OC10Notification, error) { + usr, err := c.getUser(context.Background(), executant) if err != nil { return OC10Notification{}, err } - info, err := ul.getResource(ctx, resourceid) + ctx, err := c.authenticate(usr) if err != nil { return OC10Notification{}, err } - subj, subjraw, msg, msgraw, err := ul.composeMessage(eventname, map[string]string{ - "username": user.GetDisplayName(), + info, err := c.getResource(ctx, resourceid) + if err != nil { + return OC10Notification{}, err + } + + subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{ + "username": usr.GetDisplayName(), "resourcename": info.GetName(), }) if err != nil { @@ -165,8 +213,8 @@ func (ul *UserlogService) shareMessage(eventid string, eventname string, executa return OC10Notification{ EventID: eventid, - Service: ul.cfg.Service.Name, - UserName: user.GetUsername(), + Service: c.serviceName, + UserName: usr.GetUsername(), Timestamp: ts.Format(time.RFC3339Nano), ResourceID: storagespace.FormatResourceID(*info.GetId()), ResourceType: _resourceTypeShare, @@ -174,37 +222,96 @@ func (ul *UserlogService) shareMessage(eventid string, eventname string, executa SubjectRaw: subjraw, Message: msg, MessageRaw: msgraw, - MessageDetails: ul.getDetails(user, nil, info, shareid), + MessageDetails: generateDetails(usr, nil, info, shareid), }, nil } -func (ul *UserlogService) composeMessage(eventname string, vars map[string]string) (string, string, string, string, error) { - tpl, ok := _templates[eventname] - if !ok { - return "", "", "", "", errors.New("unknown template name") +func (c *Converter) authenticate(usr *user.User) (context.Context, error) { + if ctx, ok := c.contexts[usr.GetId().GetOpaqueId()]; ok { + return ctx, nil + } + ctx, err := authenticate(usr, c.gwClient, c.machineAuthAPIKey) + if err == nil { + c.contexts[usr.GetId().GetOpaqueId()] = ctx + } + return ctx, err +} + +func (c *Converter) getSpace(ctx context.Context, spaceID string) (*storageprovider.StorageSpace, error) { + if space, ok := c.spaces[spaceID]; ok { + return space, nil + } + space, err := getSpace(ctx, spaceID, c.gwClient) + if err == nil { + c.spaces[spaceID] = space + } + return space, err +} + +func (c *Converter) getResource(ctx context.Context, resourceID *storageprovider.ResourceId) (*storageprovider.ResourceInfo, error) { + if r, ok := c.resources[resourceID.GetOpaqueId()]; ok { + return r, nil + } + resource, err := getResource(ctx, resourceID, c.gwClient) + if err == nil { + c.resources[resourceID.GetOpaqueId()] = resource + } + return resource, err +} + +func (c *Converter) getUser(ctx context.Context, userID *user.UserId) (*user.User, error) { + if u, ok := c.users[userID.GetOpaqueId()]; ok { + return u, nil + } + usr, err := getUser(ctx, userID, c.gwClient) + if err == nil { + c.users[userID.GetOpaqueId()] = usr + } + return usr, err +} + +func composeMessage(nt NotificationTemplate, locale string, path string, vars map[string]interface{}) (string, string, string, string, error) { + subjectraw, messageraw := loadTemplates(nt, locale, path) + + subject, err := executeTemplate(subjectraw, vars) + if err != nil { + return "", "", "", "", err } - subject := ul.executeTemplate(tpl.Subject, vars) - - subjectraw := ul.executeTemplate(tpl.Subject, map[string]string{ - "username": "{user}", - "spacename": "{space}", - "resourcename": "{resource}", - }) - - message := ul.executeTemplate(tpl.Message, vars) - - messageraw := ul.executeTemplate(tpl.Message, map[string]string{ - "username": "{user}", - "spacename": "{space}", - "resourcename": "{resource}", - }) - - return subject, subjectraw, message, messageraw, nil + message, err := executeTemplate(messageraw, vars) + return subject, subjectraw, message, messageraw, err } -func (ul *UserlogService) getDetails(user *user.User, space *storageprovider.StorageSpace, item *storageprovider.ResourceInfo, shareid *collaboration.ShareId) map[string]interface{} { +func loadTemplates(nt NotificationTemplate, locale string, path string) (string, string) { + // Create Locale with library path and language code and load default domain + var l *gotext.Locale + if path == "" { + l = gotext.NewLocaleFS("l10n/locale", locale, _translationFS) + } else { // use custom path instead + l = gotext.NewLocale(path, locale) + } + l.AddDomain(_domain) // make domain configurable only if needed + return l.Get(nt.Subject), l.Get(nt.Message) +} + +func executeTemplate(raw string, vars map[string]interface{}) (string, error) { + for o, n := range _placeholders { + raw = strings.ReplaceAll(raw, o, n) + } + tpl, err := template.New("").Parse(raw) + if err != nil { + return "", err + } + var writer bytes.Buffer + if err := tpl.Execute(&writer, vars); err != nil { + return "", err + } + + return writer.String(), nil +} + +func generateDetails(user *user.User, space *storageprovider.StorageSpace, item *storageprovider.ResourceInfo, shareid *collaboration.ShareId) map[string]interface{} { details := make(map[string]interface{}) if user != nil { @@ -237,13 +344,3 @@ func (ul *UserlogService) getDetails(user *user.User, space *storageprovider.Sto return details } - -func (ul *UserlogService) executeTemplate(tpl *template.Template, vars map[string]string) string { - var writer bytes.Buffer - if err := tpl.Execute(&writer, vars); err != nil { - ul.log.Error().Err(err).Str("templateName", tpl.Name()).Msg("cannot execute template") - return "" - } - - return writer.String() -} diff --git a/services/userlog/pkg/service/http.go b/services/userlog/pkg/service/http.go index 725275e04..7f316cc78 100644 --- a/services/userlog/pkg/service/http.go +++ b/services/userlog/pkg/service/http.go @@ -7,6 +7,9 @@ import ( revactx "github.com/cs3org/reva/v2/pkg/ctx" ) +// HeaderPreferedLanguage is the header where the client can set the locale +var HeaderPreferedLanguage = "Prefered-Language" + // ServeHTTP fulfills Handler interface func (ul *UserlogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { ul.m.ServeHTTP(w, r) @@ -28,9 +31,11 @@ func (ul *UserlogService) HandleGetEvents(w http.ResponseWriter, r *http.Request return } + conv := NewConverter(r.Header.Get(HeaderPreferedLanguage), ul.gwClient, ul.cfg.MachineAuthAPIKey, ul.cfg.Service.Name, ul.cfg.TranslationPath, ul.registeredEvents) + resp := GetEventResponseOC10{} for _, e := range evs { - noti, err := ul.ConvertEvent(e) + noti, err := conv.ConvertEvent(e) if err != nil { ul.log.Error().Err(err).Str("eventid", e.Id).Str("eventtype", e.Type).Msg("failed to convert event") continue diff --git a/services/userlog/pkg/service/l10n/.tx/config b/services/userlog/pkg/service/l10n/.tx/config new file mode 100755 index 000000000..c8380df8d --- /dev/null +++ b/services/userlog/pkg/service/l10n/.tx/config @@ -0,0 +1,10 @@ +[main] +host = https://www.transifex.com + +[o:owncloud-org:p:owncloud:r:ocis-userlog] +file_filter = locale//LC_MESSAGES/userlog.po +minimum_perc = 0 +source_file = userlog.pot +source_lang = en +type = PO + diff --git a/services/userlog/pkg/service/l10n/locale/de_DE/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/de_DE/LC_MESSAGES/userlog.po new file mode 100644 index 000000000..d38f57fea --- /dev/null +++ b/services/userlog/pkg/service/l10n/locale/de_DE/LC_MESSAGES/userlog.po @@ -0,0 +1,83 @@ +# German translations for package +# German translation for . +# Copyright (C) 2023 THE 'S COPYRIGHT HOLDER +# This file is distributed under the same license as the package. +# Michael Barz , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: EMAIL\n" +"POT-Creation-Date: 2023-03-13 23:36+0100\n" +"PO-Revision-Date: 2023-03-13 23:36+0100\n" +"Last-Translator: Michael Barz \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: pkg/service/templates.go:30 +msgid "Access to Space {space} lost" +msgstr "" + +#: pkg/service/templates.go:45 +msgid "Access to {resource} expired" +msgstr "" + +#: pkg/service/templates.go:29 +msgid "Membership expired" +msgstr "" + +#: pkg/service/templates.go:14 +msgid "Removed from Space" +msgstr "" + +#: pkg/service/templates.go:34 +msgid "Resource shared" +msgstr "" + +#: pkg/service/templates.go:39 +msgid "Resource unshared" +msgstr "" + +#: pkg/service/templates.go:44 +msgid "Share expired" +msgstr "" + +#: pkg/service/templates.go:24 +msgid "Space deleted" +msgstr "" + +#: pkg/service/templates.go:19 +msgid "Space disabled" +msgstr "" + +#: pkg/service/templates.go:9 +msgid "Space shared" +msgstr "" + +#: pkg/service/templates.go:10 +msgid "{user} added you to Space {space}" +msgstr "" + +#: pkg/service/templates.go:25 +msgid "{user} deleted Space {space}" +msgstr "" + +#: pkg/service/templates.go:20 +msgid "{user} disabled Space {space}" +msgstr "" + +#: pkg/service/templates.go:15 +msgid "{user} removed you from Space {space}" +msgstr "" + +#: pkg/service/templates.go:35 +msgid "{user} shared {resource} with you" +msgstr "" + +#: pkg/service/templates.go:40 +msgid "{user} unshared {resource} with you" +msgstr "" diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go index 3a8e85599..2c6ee2837 100644 --- a/services/userlog/pkg/service/service.go +++ b/services/userlog/pkg/service/service.go @@ -12,6 +12,7 @@ import ( user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + revactx "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/chi/v5" @@ -20,6 +21,7 @@ import ( ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" "github.com/owncloud/ocis/v2/services/userlog/pkg/config" "go-micro.dev/v4/store" + "google.golang.org/grpc/metadata" ) // UserlogService is the service responsible for user activities @@ -31,6 +33,7 @@ type UserlogService struct { historyClient ehsvc.EventHistoryService gwClient gateway.GatewayAPIClient registeredEvents map[string]events.Unmarshaller + translationPath string } // NewUserlogService returns an EventHistory service @@ -91,7 +94,7 @@ func (ul *UserlogService) MemorizeEvents(ch <-chan events.Event) { case events.SpaceDisabled: users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), viewer) case events.SpaceDeleted: - for u, _ := range e.FinalMembers { + for u := range e.FinalMembers { users = append(users, u) } case events.SpaceShared: @@ -251,7 +254,7 @@ func (ul *UserlogService) findSpaceMembers(ctx context.Context, spaceID string, return nil, errors.New("need authenticated context to find space members") } - space, err := ul.getSpace(ctx, spaceID) + space, err := getSpace(ctx, spaceID, ul.gwClient) if err != nil { return nil, err } @@ -325,7 +328,7 @@ func (ul *UserlogService) resolveID(ctx context.Context, userid *user.UserId, gr // resolves the users of a group func (ul *UserlogService) resolveGroup(ctx context.Context, groupID string) ([]string, error) { - grp, err := ul.getGroup(ctx, groupID) + grp, err := getGroup(ctx, groupID, ul.gwClient) if err != nil { return nil, err } @@ -338,28 +341,51 @@ func (ul *UserlogService) resolveGroup(ctx context.Context, groupID string) ([]s return userIDs, nil } -func (ul *UserlogService) impersonate(u *user.UserId) context.Context { - if u == nil { - ul.log.Debug().Msg("cannot impersonate nil user") +func (ul *UserlogService) impersonate(uid *user.UserId) context.Context { + if uid == nil { + ul.log.Error().Msg("cannot impersonate nil user") return nil } - ctx, _, err := utils.Impersonate(u, ul.gwClient, ul.cfg.MachineAuthAPIKey) + u, err := getUser(context.Background(), uid, ul.gwClient) if err != nil { - ul.log.Error().Err(err).Str("userid", u.GetOpaqueId()).Msg("failed to impersonate user") + ul.log.Error().Err(err).Msg("cannot get user") + return nil + } + + ctx, err := authenticate(u, ul.gwClient, ul.cfg.MachineAuthAPIKey) + if err != nil { + ul.log.Error().Err(err).Str("userid", u.GetId().GetOpaqueId()).Msg("failed to impersonate user") return nil } return ctx } -func (ul *UserlogService) getSpace(ctx context.Context, spaceID string) (*storageprovider.StorageSpace, error) { - res, err := ul.gwClient.ListStorageSpaces(ctx, listStorageSpaceRequest(spaceID)) +func authenticate(usr *user.User, gwc gateway.GatewayAPIClient, machineAuthAPIKey string) (context.Context, error) { + ctx := revactx.ContextSetUser(context.Background(), usr) + authRes, err := gwc.Authenticate(ctx, &gateway.AuthenticateRequest{ + Type: "machine", + ClientId: "userid:" + usr.GetId().GetOpaqueId(), + ClientSecret: machineAuthAPIKey, + }) + if err != nil { + return nil, err + } + if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK { + return nil, fmt.Errorf("error impersonating user: %s", authRes.Status.Message) + } + + return metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, authRes.Token), nil +} + +func getSpace(ctx context.Context, spaceID string, gwc gateway.GatewayAPIClient) (*storageprovider.StorageSpace, error) { + res, err := gwc.ListStorageSpaces(ctx, listStorageSpaceRequest(spaceID)) if err != nil { return nil, err } if res.GetStatus().GetCode() != rpc.Code_CODE_OK { - return nil, fmt.Errorf("Unexpected status code while getting space: %v", res.GetStatus().GetCode()) + return nil, fmt.Errorf("Error while getting space: (%v) %s", res.GetStatus().GetCode(), res.GetStatus().GetMessage()) } if len(res.StorageSpaces) == 0 { @@ -369,8 +395,8 @@ func (ul *UserlogService) getSpace(ctx context.Context, spaceID string) (*storag return res.StorageSpaces[0], nil } -func (ul *UserlogService) getUser(ctx context.Context, userid *user.UserId) (*user.User, error) { - getUserResponse, err := ul.gwClient.GetUser(context.Background(), &user.GetUserRequest{ +func getUser(ctx context.Context, userid *user.UserId, gwc gateway.GatewayAPIClient) (*user.User, error) { + getUserResponse, err := gwc.GetUser(context.Background(), &user.GetUserRequest{ UserId: userid, }) if err != nil { @@ -384,8 +410,8 @@ func (ul *UserlogService) getUser(ctx context.Context, userid *user.UserId) (*us return getUserResponse.GetUser(), nil } -func (ul *UserlogService) getGroup(ctx context.Context, groupid string) (*group.Group, error) { - r, err := ul.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: &group.GroupId{OpaqueId: groupid}}) +func getGroup(ctx context.Context, groupid string, gwc gateway.GatewayAPIClient) (*group.Group, error) { + r, err := gwc.GetGroup(ctx, &group.GetGroupRequest{GroupId: &group.GroupId{OpaqueId: groupid}}) if err != nil { return nil, err } @@ -397,8 +423,8 @@ func (ul *UserlogService) getGroup(ctx context.Context, groupid string) (*group. return r.GetGroup(), nil } -func (ul *UserlogService) getResource(ctx context.Context, resourceid *storageprovider.ResourceId) (*storageprovider.ResourceInfo, error) { - res, err := ul.gwClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: resourceid}}) +func getResource(ctx context.Context, resourceid *storageprovider.ResourceId, gwc gateway.GatewayAPIClient) (*storageprovider.ResourceInfo, error) { + res, err := gwc.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: resourceid}}) if err != nil { return nil, err } diff --git a/services/userlog/pkg/service/templates.go b/services/userlog/pkg/service/templates.go index bc9bf33d2..412b6ee4e 100644 --- a/services/userlog/pkg/service/templates.go +++ b/services/userlog/pkg/service/templates.go @@ -1,65 +1,60 @@ package service -import "text/template" +// Template marks the string as a translatable template +func Template(s string) string { return s } // the available templates var ( - SpaceShared = "space-shared" - SpaceSharedSubject = "Space shared" - SpaceSharedMessage = "{{ .username }} added you to Space {{ .spacename }}" + SpaceShared = NotificationTemplate{ + Subject: Template("Space shared"), + Message: Template("{user} added you to Space {space}"), + } - SpaceUnshared = "space-unshared" - SpaceUnsharedSubject = "Removed from Space" - SpaceUnsharedMessage = "{{ .username }} removed you from Space {{ .spacename }}" + SpaceUnshared = NotificationTemplate{ + Subject: Template("Removed from Space"), + Message: Template("{user} removed you from Space {space}"), + } - SpaceDisabled = "space-disabled" - SpaceDisabledSubject = "Space disabled" - SpaceDisabledMessage = "{{ .username }} disabled Space {{ .spacename }}" + SpaceDisabled = NotificationTemplate{ + Subject: Template("Space disabled"), + Message: Template("{user} disabled Space {space}"), + } - SpaceDeleted = "space-deleted" - SpaceDeletedSubject = "Space deleted" - SpaceDeletedMessage = "{{ .username }} deleted Space {{ .spacename }}" + SpaceDeleted = NotificationTemplate{ + Subject: Template("Space deleted"), + Message: Template("{user} deleted Space {space}"), + } - SpaceMembershipExpired = "space-membership-expired" - SpaceMembershipExpiredSubject = "Membership expired" - SpaceMembershipExpiredMessage = "Access to Space {{ .spacename }} lost" + SpaceMembershipExpired = NotificationTemplate{ + Subject: Template("Membership expired"), + Message: Template("Access to Space {space} lost"), + } - ShareCreated = "item-shared" - ShareCreatedSubject = "Resource shared" - ShareCreatedMessage = "{{ .username }} shared {{ .resourcename }} with you" + ShareCreated = NotificationTemplate{ + Subject: Template("Resource shared"), + Message: Template("{user} shared {resource} with you"), + } - ShareRemoved = "item-unshared" - ShareRemovedSubject = "Resource unshared" - ShareRemovedMessage = "{{ .username }} unshared {{ .resourcename }} with you" + ShareRemoved = NotificationTemplate{ + Subject: Template("Resource unshared"), + Message: Template("{user} unshared {resource} with you"), + } - ShareExpired = "share-expired" - ShareExpiredSubject = "Share expired" - ShareExpiredMessage = "Access to {{ .resourcename }} expired" -) - -// rendered templates -var ( - _templates = map[string]NotificationTemplate{ - SpaceShared: notiTmpl(SpaceSharedSubject, SpaceSharedMessage), - SpaceUnshared: notiTmpl(SpaceUnsharedSubject, SpaceUnsharedMessage), - SpaceDisabled: notiTmpl(SpaceDisabledSubject, SpaceDisabledMessage), - SpaceDeleted: notiTmpl(SpaceDeletedSubject, SpaceDeletedMessage), - SpaceMembershipExpired: notiTmpl(SpaceMembershipExpiredSubject, SpaceMembershipExpiredMessage), - ShareCreated: notiTmpl(ShareCreatedSubject, ShareCreatedMessage), - ShareRemoved: notiTmpl(ShareRemovedSubject, ShareRemovedMessage), - ShareExpired: notiTmpl(ShareExpiredSubject, ShareExpiredMessage), + ShareExpired = NotificationTemplate{ + Subject: Template("Share expired"), + Message: Template("Access to {resource} expired"), } ) +// holds the information to turn the raw template into a parseable go template +var _placeholders = map[string]string{ + "{user}": "{{ .username }}", + "{space}": "{{ .spacename }}", + "{resource}": "{{ .resourcename }}", +} + // NotificationTemplate is the data structure for the notifications type NotificationTemplate struct { - Subject *template.Template - Message *template.Template -} - -func notiTmpl(subjectname string, messagename string) NotificationTemplate { - return NotificationTemplate{ - Subject: template.Must(template.New("").Parse(subjectname)), - Message: template.Must(template.New("").Parse(messagename)), - } + Subject string + Message string }