diff --git a/ocis-pkg/l10n/l10n.go b/ocis-pkg/l10n/l10n.go index f9b89528f..5c99c399a 100644 --- a/ocis-pkg/l10n/l10n.go +++ b/ocis-pkg/l10n/l10n.go @@ -14,6 +14,9 @@ import ( micrometadata "go-micro.dev/v4/metadata" ) +// HeaderAcceptLanguage is the header key for the accept-language header +var HeaderAcceptLanguage = "Accept-Language" + // Template marks a string as translatable func Template(s string) string { return s } diff --git a/services/activitylog/Makefile b/services/activitylog/Makefile index dd41e6af7..8d4cc0e15 100644 --- a/services/activitylog/Makefile +++ b/services/activitylog/Makefile @@ -1,6 +1,10 @@ SHELL := bash NAME := activitylog +# Where to write the files generated by this makefile. +OUTPUT_DIR = ./pkg/service/l10n +TEMPLATE_FILE = ./pkg/service/l10n/activitylog.pot + include ../../.make/recursion.mk ############ tooling ############ @@ -29,6 +33,26 @@ ci-go-generate: # CI runs ci-node-generate automatically before this target .PHONY: ci-node-generate ci-node-generate: +############ translations ######## +.PHONY: l10n-pull +l10n-pull: + cd $(OUTPUT_DIR) && tx pull --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)/userlog.pot --keyword=l10n.Template -s pkg/service/response.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/activitylog/pkg/command/server.go b/services/activitylog/pkg/command/server.go index f80c7b4b8..50e4a05d6 100644 --- a/services/activitylog/pkg/command/server.go +++ b/services/activitylog/pkg/command/server.go @@ -18,6 +18,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/tracing" "github.com/owncloud/ocis/v2/ocis-pkg/version" 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/activitylog/pkg/config" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config/parser" "github.com/owncloud/ocis/v2/services/activitylog/pkg/logging" @@ -119,6 +120,7 @@ func Server(cfg *config.Config) *cli.Command { } hClient := ehsvc.NewEventHistoryService("com.owncloud.api.eventhistory", grpcClient) + vClient := settingssvc.NewValueService("com.owncloud.api.settings", grpcClient) { svc, err := http.Server( @@ -130,7 +132,8 @@ func Server(cfg *config.Config) *cli.Command { http.RegisteredEvents(_registeredEvents), http.Store(evStore), http.GatewaySelector(gatewaySelector), - http.History(hClient), + http.HistoryClient(hClient), + http.ValueClient(vClient), http.RegisteredEvents(_registeredEvents), ) diff --git a/services/activitylog/pkg/server/http/option.go b/services/activitylog/pkg/server/http/option.go index 5d415be3f..760cb131b 100644 --- a/services/activitylog/pkg/server/http/option.go +++ b/services/activitylog/pkg/server/http/option.go @@ -8,6 +8,7 @@ import ( "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/ocis/v2/ocis-pkg/log" 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/activitylog/pkg/config" "github.com/owncloud/ocis/v2/services/activitylog/pkg/metrics" "github.com/urfave/cli/v2" @@ -31,6 +32,7 @@ type Options struct { GatewaySelector pool.Selectable[gateway.GatewayAPIClient] TraceProvider trace.TracerProvider HistoryClient ehsvc.EventHistoryService + ValueClient settingssvc.ValueService RegisteredEvents []events.Unmarshaller } @@ -108,8 +110,8 @@ func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) } } -// History provides a function to configure the event history client -func History(h ehsvc.EventHistoryService) Option { +// HistoryClient provides a function to configure the event history client +func HistoryClient(h ehsvc.EventHistoryService) Option { return func(o *Options) { o.HistoryClient = h } @@ -128,3 +130,10 @@ func TraceProvider(val trace.TracerProvider) Option { o.TraceProvider = val } } + +// ValueClient provides a function to set the ValueClient options +func ValueClient(val settingssvc.ValueService) Option { + return func(o *Options) { + o.ValueClient = val + } +} diff --git a/services/activitylog/pkg/server/http/server.go b/services/activitylog/pkg/server/http/server.go index 455f2125c..86a239b28 100644 --- a/services/activitylog/pkg/server/http/server.go +++ b/services/activitylog/pkg/server/http/server.go @@ -86,6 +86,7 @@ func Server(opts ...Option) (http.Service, error) { svc.GatewaySelector(options.GatewaySelector), svc.TraceProvider(options.TraceProvider), svc.HistoryClient(options.HistoryClient), + svc.ValueClient(options.ValueClient), svc.RegisteredEvents(options.RegisteredEvents), ) if err != nil { diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index a39b11645..6cf8ba551 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -1,20 +1,34 @@ package service import ( + "embed" "encoding/json" "net/http" "net/url" provider "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/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/chi/v5" + "github.com/owncloud/ocis/v2/ocis-pkg/l10n" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" ) +var ( + //go:embed l10n/locale + _localeFS embed.FS + + // subfolder where the translation files are stored + _localeSubPath = "l10n/locale" + + // domain of the activitylog service (transifex) + _domain = "activitylog" +) + // ServeHTTP implements the http.Handler interface. func (s *ActivitylogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) @@ -22,6 +36,12 @@ func (s *ActivitylogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { // HandleGetItemActivities handles the request to get the activities of an item. func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *http.Request) { + activeUser, ok := revactx.ContextGetUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + // TODO: Compare driveid with itemid to avoid bad requests rid, err := parseIDParam(r, "item-id") if err != nil { @@ -68,49 +88,49 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h // error already logged in unwrapEvent continue case events.UploadReady: - message = "{user} created {resource}" + message = MessageResourceCreated res, act, ts, err = s.ResponseData(ev.FileRef, ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName(), utils.TSToTime(ev.Timestamp)) case events.FileTouched: - message = "{user} created {resource}" + message = MessageResourceCreated res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ContainerCreated: - message = "{user} created {resource}" + message = MessageResourceCreated res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ItemTrashed: - message = "{user} trashed {resource}" + message = MessageResourceTrashed res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ItemPurged: - message = "{user} purged {resource}" + message = MessageResourcePurged res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ItemMoved: - message = "{user} moved {resource}" + message = MessageResourceMoved res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ShareCreated: - message = "{user} shared {resource}" + message = MessageShareCreated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) case events.ShareUpdated: - message = "{user} updated share of {resource}" + message = MessageShareUpdated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.MTime)) case events.ShareRemoved: - message = "{user} removed share of {resource}" + message = MessageShareDeleted res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", ev.Timestamp) case events.LinkCreated: - message = "{user} created link to {resource}" + message = MessageLinkCreated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) case events.LinkUpdated: - message = "{user} updated link to {resource}" + message = MessageLinkUpdated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) case events.LinkRemoved: - message = "{user} removed link to {resource}" + message = MessageLinkDeleted res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.SpaceShared: - message = "{user} shared space {resource}" + message = MessageSpaceShared res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) case events.SpaceShareUpdated: - message = "{user} updated share of space {resource}" + message = MessageSpaceShareUpdated res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) case events.SpaceUnshared: - message = "{user} unshared space {resource}" + message = MessageSpaceUnshared res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) } @@ -119,7 +139,11 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h continue } - resp.Activities = append(resp.Activities, NewActivity(message, res, act, ts.RecordedTime, e.GetId())) + // todo: configurable default locale? + loc := l10n.MustGetUserLocale(r.Context(), activeUser.GetId().GetOpaqueId(), r.Header.Get(l10n.HeaderAcceptLanguage), s.valService) + t := l10n.NewTranslatorFromCommonConfig("en", _domain, "", _localeFS, _localeSubPath) + + resp.Activities = append(resp.Activities, NewActivity(t.Translate(message, loc), res, act, ts, e.GetId())) } b, err := json.Marshal(resp) diff --git a/services/activitylog/pkg/service/l10n/locale/tmp.txt b/services/activitylog/pkg/service/l10n/locale/tmp.txt new file mode 100644 index 000000000..e69de29bb diff --git a/services/activitylog/pkg/service/l10n/userlog.pot b/services/activitylog/pkg/service/l10n/userlog.pot new file mode 100644 index 000000000..606d31510 --- /dev/null +++ b/services/activitylog/pkg/service/l10n/userlog.pot @@ -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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: \n" + "Report-Msgid-Bugs-To: EMAIL\n" + "POT-Creation-Date: 2024-06-14 15:29+0200\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=CHARSET\n" + "Content-Transfer-Encoding: 8bit\n" + +#: pkg/service/response.go:15 +msgid "{user} created {resource}" +msgstr "" + diff --git a/services/activitylog/pkg/service/options.go b/services/activitylog/pkg/service/options.go index 8c9fef3fe..0aa97abfc 100644 --- a/services/activitylog/pkg/service/options.go +++ b/services/activitylog/pkg/service/options.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/v2/ocis-pkg/log" 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/activitylog/pkg/config" microstore "go-micro.dev/v4/store" "go.opentelemetry.io/otel/trace" @@ -26,6 +27,7 @@ type Options struct { GatewaySelector pool.Selectable[gateway.GatewayAPIClient] Mux *chi.Mux HistoryClient ehsvc.EventHistoryService + ValueClient settingssvc.ValueService } // Logger configures a logger for the activitylog service @@ -90,3 +92,10 @@ func HistoryClient(hc ehsvc.EventHistoryService) Option { o.HistoryClient = hc } } + +// ValueClient adds a grpc client for the value service +func ValueClient(vs settingssvc.ValueService) Option { + return func(o *Options) { + o.ValueClient = vs + } +} diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index 44273145e..ce185cc20 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -7,6 +7,24 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" + "github.com/owncloud/ocis/v2/ocis-pkg/l10n" +) + +// Translations +var ( + MessageResourceCreated = l10n.Template("{user} created {resource}") + MessageResourceTrashed = l10n.Template("{user} trashed {resource}") + MessageResourcePurged = l10n.Template("{user} purged {resource}") + MessageResourceMoved = l10n.Template("{user} moved {resource}") + MessageShareCreated = l10n.Template("{user} shared {resource}") + MessageShareUpdated = l10n.Template("{user} updated share of {resource}") + MessageShareDeleted = l10n.Template("{user} deleted share of {resource}") + MessageLinkCreated = l10n.Template("{user} created link to {resource}") + MessageLinkUpdated = l10n.Template("{user} updated link to {resource}") + MessageLinkDeleted = l10n.Template("{user} deleted link to {resource}") + MessageSpaceShared = l10n.Template("{user} shared space {resource}") + MessageSpaceShareUpdated = l10n.Template("{user} updated share of space {resource}") + MessageSpaceUnshared = l10n.Template("{user} unshared space {resource}") ) // GetActivitiesResponse is the response on GET activities requests @@ -45,12 +63,10 @@ type Template struct { } // NewActivity creates a new activity -func NewActivity(message string, res Resource, user Actor, ts time.Time, eventID string) Activity { +func NewActivity(message string, res Resource, user Actor, ts Times, eventID string) Activity { return Activity{ - ID: eventID, - Times: Times{ - RecordedTime: ts, - }, + ID: eventID, + Times: ts, Template: Template{ Message: message, Variables: map[string]interface{}{ diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 00b50c814..9cfbb7ff2 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -17,6 +17,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/v2/ocis-pkg/log" 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/activitylog/pkg/config" microstore "go-micro.dev/v4/store" ) @@ -30,13 +31,14 @@ type RawActivity struct { // ActivitylogService logs events per resource type ActivitylogService struct { - cfg *config.Config - log log.Logger - events <-chan events.Event - store microstore.Store - gws pool.Selectable[gateway.GatewayAPIClient] - mux *chi.Mux - evHistory ehsvc.EventHistoryService + cfg *config.Config + log log.Logger + events <-chan events.Event + store microstore.Store + gws pool.Selectable[gateway.GatewayAPIClient] + mux *chi.Mux + evHistory ehsvc.EventHistoryService + valService settingssvc.ValueService registeredEvents map[string]events.Unmarshaller } @@ -69,6 +71,7 @@ func New(opts ...Option) (*ActivitylogService, error) { gws: o.GatewaySelector, mux: o.Mux, evHistory: o.HistoryClient, + valService: o.ValueClient, registeredEvents: make(map[string]events.Unmarshaller), }