enhancement: make collaboration mention functionality public

This commit is contained in:
Florian Schade
2026-06-09 17:15:23 +02:00
committed by Benedikt Kulmann
parent 159785a3b5
commit f1208cfa32
27 changed files with 521 additions and 37 deletions

View File

@@ -3215,6 +3215,7 @@ def wopiCollaborationService(name):
"COLLABORATION_CS3API_DATAGATEWAY_INSECURE": True,
"OC_JWT_SECRET": "some-opencloud-jwt-secret",
"COLLABORATION_WOPI_SECRET": "some-wopi-secret",
"COLLABORATION_EVENTS_ENDPOINT": "%s:9233" % OC_SERVER_NAME,
}
if name == "collabora":

22
pkg/events/events.go Normal file
View File

@@ -0,0 +1,22 @@
package events
import (
"encoding/json"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
type ResourceMention struct {
Executant *user.UserId
UserIDs []*user.UserId
Ref *provider.Reference
Timestamp time.Time
}
func (ResourceMention) Unmarshal(v []byte) (interface{}, error) {
e := ResourceMention{}
err := json.Unmarshal(v, &e)
return e, err
}

View File

@@ -14,7 +14,8 @@ import (
type Permission string
const (
PermissionCollaborationManageFonts Permission = "Collaboration.Fonts.Manage"
PermissionCollaborationManageFonts Permission = "Collaboration.Fonts.Manage"
PermissionCollaborationPublishNotification Permission = "Collaboration.Notification.Publish"
)
func CheckPermissions(gatewayClient gateway.GatewayAPIClient, ctx context.Context, permission Permission) (*userpb.User, bool, error) {

View File

@@ -8,6 +8,7 @@ import (
"os/signal"
"time"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/spf13/afero"
@@ -17,6 +18,7 @@ import (
microstore "go-micro.dev/v4/store"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
"github.com/opencloud-eu/opencloud/pkg/generators"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/runner"
@@ -27,6 +29,7 @@ import (
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/connector"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/font"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/helpers"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/notification"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/server/debug"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/server/grpc"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/server/http"
@@ -171,6 +174,24 @@ func Server(cfg *config.Config) *cobra.Command {
fontService = service
}
var notificationService notification.Service
{
connName := generators.GenerateConnectionName(cfg.Service.Name, generators.NTypeBus)
natsStream, err := stream.NatsFromConfig(connName, true, stream.NatsConfig(cfg.Events))
if err != nil {
return err
}
service, err := notification.NewService(
notification.ServiceOptions{}.
WithLogger(logger).
WithGatewaySelector(gatewaySelector).
WithEventPublisher(natsStream).
WithMachineAuthAPIKey(cfg.MachineAuthAPIKey),
)
notificationService = service
}
// start HTTP server
httpServer, err := http.Server(
http.Adapter(connector.NewHttpAdapter(gatewaySelector, cfg, st, selector.NewSelector(selector.Registry(registry.GetRegistry())))),
@@ -180,6 +201,7 @@ func Server(cfg *config.Config) *cobra.Command {
http.TracerProvider(traceProvider),
http.Store(st),
http.FontService(fontService),
http.NotificationService(notificationService),
)
if err != nil {
logger.Info().Err(err).Str("transport", "http").Msg("Failed to initialize server")

View File

@@ -14,6 +14,7 @@ type Config struct {
App App `yaml:"app"`
Font Font `yaml:"font"`
Store Store `yaml:"store"`
Events Events `yaml:"events"`
TokenManager *TokenManager `yaml:"token_manager"`
@@ -27,4 +28,6 @@ type Config struct {
Debug Debug `yaml:"debug"`
Context context.Context `yaml:"-"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;COLLABORATION_MACHINE_AUTH_API_KEY" desc:"The machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"%%NEXT%%"`
}

View File

@@ -39,6 +39,10 @@ func DefaultConfig() *config.Config {
AssetPath: filepath.Join(defaults.BaseDataPath(), "collaboration/fonts"),
PreviewText: "OpenCloud",
},
Events: config.Events{
Endpoint: "127.0.0.1:9233",
Cluster: "opencloud-cluster",
},
Store: config.Store{
Store: "nats-js-kv",
Nodes: []string{"127.0.0.1:9233"},
@@ -92,6 +96,10 @@ func EnsureDefaults(cfg *config.Config) {
if cfg.CS3Api.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.CS3Api.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
}
// Sanitize sanitized the configuration

View File

@@ -0,0 +1,12 @@
package config
// Events combines the configuration options for the event bus.
type Events struct {
Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT;COLLABORATION_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"%%NEXT%%"`
Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER;COLLABORATION_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"%%NEXT%%"`
TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE;COLLABORATION_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"%%NEXT%%"`
TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE;COLLABORATION_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided COLLABORATION_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"%%NEXT%%"`
EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS;COLLABORATION_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"`
AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME;COLLABORATION_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"`
AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD;COLLABORATION_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"`
}

View File

@@ -0,0 +1,10 @@
package notification
import (
"github.com/go-playground/validator/v10"
)
var validate = validator.New(
validator.WithPrivateFieldValidation(),
validator.WithRequiredStructEnabled(),
)

View File

@@ -0,0 +1,152 @@
package notification
import (
"context"
"encoding/json"
"io"
"net/http"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"google.golang.org/grpc/metadata"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/collaboration"
)
type ServiceOptions struct {
logger log.Logger `validate:"required"`
eventPublisher events.Publisher `validate:"required"`
gatewaySelector pool.Selectable[gateway.GatewayAPIClient] `validate:"required"`
machineAuthAPIKey string `validate:"required,min=1"`
}
func (o ServiceOptions) WithLogger(logger log.Logger) ServiceOptions {
o.logger = logger
return o
}
func (o ServiceOptions) WithEventPublisher(eventPublisher events.Publisher) ServiceOptions {
o.eventPublisher = eventPublisher
return o
}
func (o ServiceOptions) WithMachineAuthAPIKey(key string) ServiceOptions {
o.machineAuthAPIKey = key
return o
}
func (o ServiceOptions) WithGatewaySelector(gws pool.Selectable[gateway.GatewayAPIClient]) ServiceOptions {
o.gatewaySelector = gws
return o
}
type Service struct {
log log.Logger
eventPublisher events.Publisher
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
machineAuthAPIKey string
}
func NewService(options ServiceOptions) (Service, error) {
if err := validate.Struct(options); err != nil {
return Service{}, err
}
return Service{
log: options.logger,
eventPublisher: options.eventPublisher,
gatewaySelector: options.gatewaySelector,
machineAuthAPIKey: options.machineAuthAPIKey,
}, nil
}
func (s Service) HandleNotification(w http.ResponseWriter, r *http.Request) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
requestUser, canManage, err := collaboration.CheckPermissions(gatewayClient, r.Context(), collaboration.PermissionCollaborationPublishNotification)
switch {
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
return
case !canManage:
w.WriteHeader(http.StatusForbidden)
return
}
defer func() { _ = r.Body.Close() }()
body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
var data = struct {
Type string `json:"type" validate:"required"`
UserIDs []string `json:"userIDs" validate:"required"`
FileID string `json:"fileID" validate:"required"`
}{}
if err := json.Unmarshal(body, &data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := validate.Struct(data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
event := ocEvents.ResourceMention{
Executant: requestUser.GetId(),
Timestamp: time.Now(),
}
for _, userID := range data.UserIDs {
authResponse, err := gatewayClient.Authenticate(context.Background(), &gateway.AuthenticateRequest{
Type: "machine",
ClientId: "userid:" + userID,
ClientSecret: s.machineAuthAPIKey,
})
if err != nil || authResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
w.WriteHeader(http.StatusInternalServerError)
return
}
resourceID, err := storagespace.ParseID(data.FileID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
statResponse, err := gatewayClient.Stat(
metadata.AppendToOutgoingContext(context.Background(), revactx.TokenHeader, authResponse.GetToken()),
&storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &resourceID}},
)
if err != nil || statResponse.Status.Code != rpcv1beta1.Code_CODE_OK {
w.WriteHeader(http.StatusInternalServerError)
return
}
event.UserIDs = append(event.UserIDs, authResponse.User.GetId())
event.Ref = &storageprovider.Reference{
ResourceId: statResponse.GetInfo().GetId(),
}
}
if err := events.Publish(r.Context(), s.eventPublisher, event); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/config"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/connector"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/font"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/notification"
)
// Option defines a single option function.
@@ -17,13 +18,14 @@ type Option func(o *Options)
// Options define the available options for this package.
type Options struct {
Adapter *connector.HttpAdapter
Logger log.Logger
Context context.Context
Config *config.Config
TracerProvider trace.TracerProvider
Store microstore.Store
FontService font.Service
Adapter *connector.HttpAdapter
Logger log.Logger
Context context.Context
Config *config.Config
TracerProvider trace.TracerProvider
Store microstore.Store
FontService font.Service
NotificationService notification.Service
}
// newOptions initializes the available default options.
@@ -85,3 +87,10 @@ func FontService(val font.Service) Option {
o.FontService = val
}
}
// NotificationService provides a function to set the NotificationService option
func NotificationService(val notification.Service) Option {
return func(o *Options) {
o.NotificationService = val
}
}

View File

@@ -96,6 +96,7 @@ func Server(opts ...Option) (http.Service, error) {
// prepareRoutes will prepare all the implemented routes
func prepareRoutes(r *chi.Mux, options Options) {
fontService := options.FontService
notificationService := options.NotificationService
adapter := options.Adapter
logger := options.Logger
// prepare basic logger for the request
@@ -214,15 +215,18 @@ func prepareRoutes(r *chi.Mux, options Options) {
})
r.Route("/collaboration", func(r chi.Router) {
auth := middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret),
)
r.With(auth).Route("/notify", func(r chi.Router) {
r.Post("/", notificationService.HandleNotification)
})
r.Route("/fonts", func(r chi.Router) {
r.Get("/", fontService.ListFonts)
r.Get("/{id}", fontService.GetFont)
r.Get("/preview/{id}", fontService.PreviewFont)
r.Route("/manage", func(r chi.Router) {
r.Use(middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret),
))
r.With(auth).Route("/manage", func(r chi.Router) {
r.Post("/", fontService.UploadFont)
r.Delete("/{id}", fontService.DeleteFont)
})

View File

@@ -6,7 +6,12 @@ import (
"os/signal"
"reflect"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
"github.com/opencloud-eu/opencloud/pkg/generators"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/registry"
@@ -19,14 +24,12 @@ import (
"github.com/opencloud-eu/opencloud/services/notifications/pkg/config/parser"
"github.com/opencloud-eu/opencloud/services/notifications/pkg/server/debug"
"github.com/opencloud-eu/opencloud/services/notifications/pkg/service"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
ehsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/eventhistory/v0"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/spf13/cobra"
microstore "go-micro.dev/v4/store"
ehsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/eventhistory/v0"
)
// Server is the entrypoint for the server command.
@@ -86,6 +89,7 @@ func Server(cfg *config.Config) *cobra.Command {
events.SpaceMembershipExpired{},
events.ScienceMeshInviteTokenGenerated{},
events.SendEmailsEvent{},
ocEvents.ResourceMention{},
}
registeredEvents := make(map[string]events.Unmarshaller)
for _, e := range evs {

View File

@@ -3,6 +3,7 @@ package email
import (
"bytes"
"embed"
"fmt"
"strings"
"text/template"
@@ -150,6 +151,11 @@ func callToActionToHTML(s string) string {
if strings.TrimSpace(s) == "" {
return ""
}
s = strings.TrimSuffix(s, "{ShareLink}")
return s + `<a href="{ShareLink}">{ShareLink}</a>`
// substitute links
for _, token := range []string{"ShareLink", "ResourceLink"} {
s = strings.ReplaceAll(s, "{"+token+"}", fmt.Sprintf(`<a href="{%s}">{%s}</a>`, token, token))
}
return s
}

View File

@@ -118,6 +118,15 @@ Please visit your federation settings and use the following details:
Greeting: l10n.Template(`Hi {DisplayName},`),
MessageBody: "", // is generated using the GroupedTemplates
}
Mention = MessageTemplate{
textTemplate: _textTemplate,
htmlTemplate: _htmlTemplate,
Subject: l10n.Template(`You were mentioned in '{ResourceName}'`),
Greeting: l10n.Template(`Hello {RecipientName},`),
MessageBody: l10n.Template(`{AuthorName} mentioned you in "{ResourceName}".`),
CallToAction: l10n.Template(`You can view the mention here: {ResourceLink}`),
}
)
// holds the information to turn the raw template into a parseable go template
@@ -134,6 +143,10 @@ var _placeholders = map[string]string{
"{ProviderDomain}": "{{ .ProviderDomain }}",
"{Token}": "{{ .Token }}",
"{DisplayName}": "{{ .DisplayName }}",
"{AuthorName}": "{{ .AuthorName }}",
"{RecipientName}": "{{ .RecipientName }}",
"{ResourceName}": "{{ .ResourceName }}",
"{ResourceLink}": "{{ .ResourceLink }}",
}
// MessageTemplate is the data structure for the email

View File

@@ -0,0 +1,99 @@
package service
import (
"context"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
"github.com/opencloud-eu/opencloud/pkg/l10n"
"github.com/opencloud-eu/opencloud/services/notifications/pkg/channels"
"github.com/opencloud-eu/opencloud/services/notifications/pkg/email"
"github.com/opencloud-eu/opencloud/services/settings/pkg/store/defaults"
)
func (s eventsNotifier) handleResourceMention(e ocEvents.ResourceMention, eventId string) {
logger := s.logger.With().
Str("event", "Mention").
Str("resourceid", e.Ref.GetResourceId().GetOpaqueId()).
Logger()
_ = logger
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return
}
ctx, err := utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("could not select next gateway client")
return
}
var data = struct {
resourceLink string `validate:"required,url"`
resourceName string `validate:"required,min=1"`
author *user.User `validate:"required"`
recipients []*user.User `validate:"required,min=1"`
}{}
// fill the data struct with the info we need to render the email
{
resourceInfo, err := s.getResourceInfo(ctx, e.Ref.GetResourceId(), nil)
if err != nil {
return
}
data.resourceName = resourceInfo.GetName()
data.resourceLink, err = urlJoinPath(s.openCloudURL, "f", storagespace.FormatResourceID(resourceInfo.GetId()))
if err != nil {
logger.Error().Err(err).Msg("failed to generate resource link.")
return
}
for _, userID := range append([]*user.UserId{e.Executant}, e.UserIDs...) {
switch u, err := s.getUser(ctx, userID); {
case err != nil:
logger.Error().Err(err).Msg("could not get user")
return
case userID.GetOpaqueId() == e.Executant.GetOpaqueId():
data.author = u
default:
data.recipients = append(data.recipients, u)
}
}
recipients := s.filter.execute(ctx, data.recipients, defaults.SettingUUIDProfileEventResourceMention)
recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, recipients)
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...)
recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...)
data.recipients = recipientsInstant
}
if err := validate.Struct(data); err != nil {
logger.Error().Err(err).Msg("data struct validation failed")
return
}
messages := make([]*channels.Message, len(data.recipients))
for i, recipient := range data.recipients {
locale := l10n.MustGetUserLocale(ctx, recipient.GetId().GetOpaqueId(), "", s.valueService)
message, err := email.RenderEmailTemplate(email.Mention, locale, s.defaultLanguage, s.emailTemplatePath, s.translationPath, map[string]string{
"AuthorName": data.author.GetDisplayName(),
"RecipientName": recipient.GetDisplayName(),
"ResourceName": data.resourceName,
"ResourceLink": data.resourceLink,
})
if err != nil {
logger.Error().Err(err).Msg("could not render email-template")
return
}
message.Sender = data.author.GetDisplayName()
message.Recipient = []string{recipient.GetMail()}
messages[i] = message
}
s.send(ctx, messages)
}

View File

@@ -10,9 +10,11 @@ import (
"sync"
"sync/atomic"
ehsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/eventhistory/v0"
"go-micro.dev/v4/store"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
ehsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/eventhistory/v0"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
@@ -22,6 +24,9 @@ import (
"go-micro.dev/v4/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/opencloud/pkg/l10n"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/middleware"
@@ -29,16 +34,13 @@ import (
"github.com/opencloud-eu/opencloud/services/notifications/pkg/channels"
"github.com/opencloud-eu/opencloud/services/notifications/pkg/email"
"github.com/opencloud-eu/opencloud/services/settings/pkg/store/defaults"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
// validate is the package level validator instance
var validate *validator.Validate
func init() {
validate = validator.New()
}
var validate = validator.New(
validator.WithPrivateFieldValidation(),
validator.WithRequiredStructEnabled(),
)
// Service should be named `Runner`
type Service interface {
@@ -131,6 +133,8 @@ EventLoop:
s.handleScienceMeshInviteTokenGenerated(e)
case events.SendEmailsEvent:
s.sendGroupedEmailsJob(e, evt.ID)
case ocEvents.ResourceMention:
s.handleResourceMention(e, evt.ID)
}
})

View File

@@ -292,7 +292,12 @@ func DefaultPolicies() []config.Policy {
SkipXAccessToken: true,
},
{
Endpoint: "/collaboration/fonts/manage/",
Endpoint: "/collaboration/fonts/manage",
Service: "eu.opencloud.web.collaboration",
// Method: "POST" // toDo: fails with method, WHY???
},
{
Endpoint: "/collaboration/notify",
Service: "eu.opencloud.web.collaboration",
// Method: "POST" // toDo: fails with method, WHY???
},

View File

@@ -10,6 +10,12 @@ import (
cs3permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/leonelquinteros/gotext"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/opencloud-eu/opencloud/pkg/l10n"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/middleware"
@@ -20,11 +26,6 @@ import (
"github.com/opencloud-eu/opencloud/services/settings/pkg/settings"
"github.com/opencloud-eu/opencloud/services/settings/pkg/store/defaults"
metastore "github.com/opencloud-eu/opencloud/services/settings/pkg/store/metadata"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
"google.golang.org/protobuf/types/known/emptypb"
)
//go:embed l10n/locale
@@ -708,6 +709,7 @@ func translateBundle(bundle *settingsmsg.Bundle, t *gotext.Locale) *settingsmsg.
defaults.SettingUUIDProfileEventSpaceUnshared,
defaults.SettingUUIDProfileEventSpaceMembershipExpired,
defaults.SettingUUIDProfileEventSpaceDisabled,
defaults.SettingUUIDProfileEventResourceMention,
defaults.SettingUUIDProfileEventSpaceDeleted:
// translate event names ('Share Received', 'Share Removed', ...)
set.DisplayName = t.Get(set.GetDisplayName(), []any{}...)

View File

@@ -193,5 +193,6 @@ func getDefaultValueList() map[string]*settingsmsg.ValueWithIdentifier {
defaults.SettingUUIDProfileEventSpaceDeleted: nil,
defaults.SettingUUIDProfileEventPostprocessingStepFinished: nil,
defaults.SettingUUIDProfileEmailSendingInterval: nil,
defaults.SettingUUIDProfileEventResourceMention: nil,
}
}

View File

@@ -47,6 +47,8 @@ const (
SettingUUIDProfileEventSpaceDeleted = "094ceca9-5a00-40ba-bb1a-bbc7bccd39ee"
// SettingUUIDProfileEventPostprocessingStepFinished is the hardcoded setting UUID for the send in mail setting
SettingUUIDProfileEventPostprocessingStepFinished = "fe0a3011-d886-49c8-b797-33d02fa426ef"
// SettingUUIDProfileEventResourceMention is the hardcoded setting UUID for the send in mail setting
SettingUUIDProfileEventResourceMention = "08aaa973-a622-449d-97dc-3857160d1e97"
)
// GenerateBundlesDefaultRoles bootstraps the default roles.
@@ -79,6 +81,7 @@ func ServiceAccountBundle() *settingsmsg.Bundle {
Settings: []*settingsmsg.Setting{
AccountManagementPermission(All),
ChangeLogoPermission(All),
CollaborationPublishNotificationPermission(All),
CollaborationManageFontsPermission(All),
CreatePublicLinkPermission(All),
CreateSharePermission(All),
@@ -116,6 +119,7 @@ func generateBundleAdminRole() *settingsmsg.Bundle {
AccountManagementPermission(All),
AutoAcceptSharesPermission(Own),
ChangeLogoPermission(All),
CollaborationPublishNotificationPermission(All),
CollaborationManageFontsPermission(All),
CreatePublicLinkPermission(All),
CreateSharePermission(All),
@@ -180,6 +184,7 @@ func generateBundleSpaceAdminRole() *settingsmsg.Bundle {
ProfileEventPostprocessingStepFinishedPermission(Own),
LanguageManagementPermission(Own),
ListFavoritesPermission(Own),
CollaborationPublishNotificationPermission(All),
ListSpacesPermission(All),
ManageSpacePropertiesPermission(All),
SelfManagementPermission(Own),
@@ -218,6 +223,7 @@ func generateBundleUserRole() *settingsmsg.Bundle {
ProfileEventPostprocessingStepFinishedPermission(Own),
LanguageManagementPermission(Own),
ListFavoritesPermission(Own),
CollaborationPublishNotificationPermission(All),
SelfManagementPermission(Own),
WriteFavoritesPermission(Own),
},
@@ -345,6 +351,23 @@ func generateBundleProfileRequest() *settingsmsg.Bundle {
},
},
},
{
Id: SettingUUIDProfileEventResourceMention,
Name: "event-resource-mention-options",
DisplayName: TemplateResourceMention,
Description: TemplateResourceMentionDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailTrue,
},
},
},
},
{
Id: SettingUUIDProfileEventSpaceShared,
Name: "event-space-shared-options",

View File

@@ -67,7 +67,7 @@ func ChangeLogoPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Sett
}
}
// ManageFontsPermission is the permission to manage fonts
// CollaborationManageFontsPermission is the permission to manage fonts
func CollaborationManageFontsPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "ed83fc10-1f54-4a9e-b5a7-fb517f5f3e01",
@@ -86,6 +86,25 @@ func CollaborationManageFontsPermission(c settingsmsg.Permission_Constraint) *se
}
}
// CollaborationPublishNotificationPermission is the permission to manage fonts
func CollaborationPublishNotificationPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "43e5948e-8238-41d6-9ef1-f259f00591db",
Name: "Collaboration.Notification.Publish",
DisplayName: "Publish collaboration notifications",
Description: "This permission permits to publish collaboration related notifications.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// CreatePublicLinkPermission is the permission to create public links
func CreatePublicLinkPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{

View File

@@ -12,6 +12,10 @@ var (
TemplateShareRemoved = l10n.Template("Share Removed")
// description of the notification option 'Share Removed'
TemplateShareRemovedDescription = l10n.Template("Notify when a received share has been removed")
// name of the notification option 'Resource Mention'
TemplateResourceMention = l10n.Template("Resource Mention")
// description of the notification option 'Resource Mention'
TemplateResourceMentionDescription = l10n.Template("Notify on resource mentions")
// name of the notification option 'Share Expired'
TemplateShareExpired = l10n.Template("Share Expired")
// description of the notification option 'Share Expired'

View File

@@ -6,6 +6,7 @@ import (
"os/signal"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
"github.com/opencloud-eu/opencloud/pkg/generators"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/registry"
@@ -45,6 +46,9 @@ var _registeredEvents = []events.Unmarshaller{
events.ShareCreated{},
events.ShareRemoved{},
events.ShareExpired{},
// misc
ocEvents.ResourceMention{},
}
// Server is the entrypoint for the server command.

View File

@@ -14,11 +14,13 @@ import (
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/opencloud-eu/opencloud/pkg/l10n"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
"github.com/opencloud-eu/opencloud/pkg/l10n"
)
//go:embed l10n/locale
@@ -115,7 +117,12 @@ func (c *Converter) ConvertEvent(eventid string, event any) (OC10Notification, e
return c.shareMessage(eventid, ShareExpired, ev.ShareOwner, ev.ItemID, ev.ShareID, ev.ExpiredAt)
case events.ShareRemoved:
return c.shareMessage(eventid, ShareRemoved, ev.Executant, ev.ItemID, ev.ShareID, ev.Timestamp)
// misc
case ocEvents.ResourceMention:
return c.resourceMention(eventid, Mention, ev.Executant, ev.Ref.GetResourceId(), ev.Timestamp)
}
}
// ConvertGlobalEvent converts a global event to an OC10Notification
@@ -199,6 +206,40 @@ func (c *Converter) spaceMessage(eventid string, nt NotificationTemplate, execut
}, nil
}
func (c *Converter) resourceMention(eventid string, nt NotificationTemplate, executant *user.UserId, resourceid *storageprovider.ResourceId, ts time.Time) (OC10Notification, error) {
usr, err := c.getUser(context.Background(), executant)
if err != nil {
return OC10Notification{}, err
}
info, err := c.getResource(c.serviceAccountContext, resourceid)
if err != nil {
return OC10Notification{}, err
}
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]any{
"username": usr.GetDisplayName(),
"resourcename": info.GetName(),
})
if err != nil {
return OC10Notification{}, err
}
return OC10Notification{
EventID: eventid,
Service: c.serviceName,
UserName: usr.GetUsername(),
Timestamp: ts.Format(time.RFC3339Nano),
ResourceID: storagespace.FormatResourceID(info.GetId()),
ResourceType: "mention",
Subject: subj,
SubjectRaw: subjraw,
Message: msg,
MessageRaw: msgraw,
MessageDetails: generateDetails(usr, nil, info, nil),
}, nil
}
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 {

View File

@@ -5,12 +5,14 @@ import (
"errors"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/events"
micrometadata "go-micro.dev/v4/metadata"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/middleware"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
"github.com/opencloud-eu/opencloud/services/settings/pkg/store/defaults"
"github.com/opencloud-eu/reva/v2/pkg/events"
micrometadata "go-micro.dev/v4/metadata"
)
type userlogFilter struct {
@@ -61,6 +63,8 @@ func (ulf userlogFilter) filterUsersBySettings(ctx context.Context, users []stri
settingId = defaults.SettingUUIDProfileEventSpaceDisabled
case events.SpaceDeleted:
settingId = defaults.SettingUUIDProfileEventSpaceDeleted
case ocEvents.ResourceMention:
settingId = defaults.SettingUUIDProfileEventResourceMention
default:
// event that cannot be disabled
return users

View File

@@ -17,6 +17,7 @@ import (
"go-micro.dev/v4/store"
"go.opentelemetry.io/otel/trace"
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
"github.com/opencloud-eu/opencloud/pkg/l10n"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/roles"
@@ -168,6 +169,11 @@ func (ul *UserlogService) processEvent(event events.Event) {
case events.SpaceShared:
executant = e.Executant
users, err = utils.ResolveID(ctx, e.GranteeUserID, e.GranteeGroupID, gwc)
case ocEvents.ResourceMention:
executant = e.Executant
for _, userID := range e.UserIDs {
users = append(users, userID.GetOpaqueId())
}
case events.SpaceUnshared:
executant = e.Executant
users, err = utils.ResolveID(ctx, e.GranteeUserID, e.GranteeGroupID, gwc)

View File

@@ -49,6 +49,11 @@ var (
Message: l10n.Template("{user} unshared {resource} with you"),
}
Mention = NotificationTemplate{
Subject: l10n.Template("You have been mentioned"),
Message: l10n.Template("{user} mentioned you in {resource}"),
}
ShareExpired = NotificationTemplate{
Subject: l10n.Template("Share expired"),
Message: l10n.Template("Access to {resource} expired"),