Merge pull request #5753 from kobergj/TranslateNotifications

Translate Notifications
This commit is contained in:
kobergj
2023-03-14 16:02:37 +01:00
committed by GitHub
15 changed files with 427 additions and 155 deletions

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

@@ -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 = \

3
go.mod
View File

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

2
go.sum
View File

@@ -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=

View File

@@ -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:

View File

@@ -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.

View File

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

View File

@@ -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()
}

View File

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

View File

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

View File

@@ -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 <mbarz@owncloud.com>, 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 <mbarz@owncloud.com>\n"
"Language-Team: German <translation-team-de@lists.sourceforge.net>\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 ""

View File

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

View File

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