mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-31 01:10:20 -06:00
@@ -2039,6 +2039,7 @@ def ocisServer(storage, accounts_hash_difficulty = 4, volumes = [], depends_on =
|
||||
"NATS_NATS_HOST": "0.0.0.0",
|
||||
"NATS_NATS_PORT": 9233,
|
||||
"OCIS_JWT_SECRET": "some-ocis-jwt-secret",
|
||||
"EVENTHISTORY_STORE": "memory",
|
||||
}
|
||||
|
||||
if deploy_type == "":
|
||||
|
||||
5
changelog/unreleased/activity-api.md
Normal file
5
changelog/unreleased/activity-api.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: Activitylog API
|
||||
|
||||
Adds an api to the `activitylog` service which allows retrieving data by clients to show item activities
|
||||
|
||||
https://github.com/owncloud/ocis/pull/9361
|
||||
2
go.mod
2
go.mod
@@ -72,7 +72,7 @@ require (
|
||||
github.com/onsi/gomega v1.33.1
|
||||
github.com/open-policy-agent/opa v0.65.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3
|
||||
github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/xattr v0.4.9
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1805,8 +1805,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk
|
||||
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
||||
github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3 h1:2pMI03tU/IWc+Y4p1doTaTE1zrAR55RYOzYrt57/aPA=
|
||||
github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3/go.mod h1:yXI+rmE8yYx+ZsGVrnCpprw/gZMcxjwntnX2y2+VKxY=
|
||||
github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1 h1:w1OhLyFevK8NCYn50TEsDpInk/T6qVk/v1LDca6Zx8Y=
|
||||
github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1/go.mod h1:yXI+rmE8yYx+ZsGVrnCpprw/gZMcxjwntnX2y2+VKxY=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pablodz/inotifywaitgo v0.0.6 h1:BTjQfnixXwG7oYmlIiyhWA6iyO9BtxatB3YgiibOTFc=
|
||||
|
||||
@@ -4,8 +4,7 @@ package test
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
)
|
||||
|
||||
// DiffAst returns a human-readable report of the differences between two values
|
||||
@@ -5,9 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/now"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
func toNode[T ast.Node](in interface{}) (T, error) {
|
||||
@@ -3,7 +3,7 @@ package kql
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
)
|
||||
|
||||
// connectNodes connects given nodes
|
||||
@@ -6,12 +6,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/now"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast/test"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
tAssert "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast/test"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
)
|
||||
|
||||
func TestParse_Spec(t *testing.T) {
|
||||
@@ -3,7 +3,7 @@ package kql
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
)
|
||||
|
||||
func base(text []byte, pos position) (*ast.Base, error) {
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
)
|
||||
|
||||
// The operator node value definition
|
||||
@@ -3,11 +3,10 @@ package kql_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
tAssert "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
)
|
||||
|
||||
func TestNewAST(t *testing.T) {
|
||||
@@ -1,8 +1,8 @@
|
||||
package kql
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
func validateAst(a *ast.Ast) error {
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)/activitylog.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:
|
||||
|
||||
@@ -14,13 +14,16 @@ import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/handlers"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
|
||||
ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
"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"
|
||||
"github.com/owncloud/ocis/v2/services/activitylog/pkg/metrics"
|
||||
"github.com/owncloud/ocis/v2/services/activitylog/pkg/service"
|
||||
"github.com/owncloud/ocis/v2/services/activitylog/pkg/server/http"
|
||||
"github.com/urfave/cli/v2"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
@@ -30,20 +33,13 @@ var _registeredEvents = []events.Unmarshaller{
|
||||
events.FileTouched{},
|
||||
events.ContainerCreated{},
|
||||
events.ItemTrashed{},
|
||||
events.ItemPurged{},
|
||||
events.ItemMoved{},
|
||||
events.ShareCreated{},
|
||||
events.ShareUpdated{},
|
||||
events.ShareRemoved{},
|
||||
events.LinkCreated{},
|
||||
events.LinkUpdated{},
|
||||
events.LinkRemoved{},
|
||||
events.SpaceShared{},
|
||||
events.SpaceShareUpdated{},
|
||||
events.SpaceUnshared{},
|
||||
|
||||
// TODO: file downloaded only for public links. How to do this?
|
||||
events.FileDownloaded{},
|
||||
}
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
@@ -109,15 +105,29 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
return fmt.Errorf("could not get reva client selector: %s", err)
|
||||
}
|
||||
|
||||
grpcClient, err := ogrpc.NewClient(
|
||||
append(ogrpc.GetClientOptions(cfg.GRPCClientTLS), ogrpc.WithTraceProvider(tracerProvider))...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hClient := ehsvc.NewEventHistoryService("com.owncloud.api.eventhistory", grpcClient)
|
||||
vClient := settingssvc.NewValueService("com.owncloud.api.settings", grpcClient)
|
||||
|
||||
{
|
||||
svc, err := service.New(
|
||||
service.Logger(logger),
|
||||
service.Config(cfg),
|
||||
service.TraceProvider(tracerProvider),
|
||||
service.Stream(evStream),
|
||||
service.RegisteredEvents(_registeredEvents),
|
||||
service.Store(evStore),
|
||||
service.GatewaySelector(gatewaySelector),
|
||||
svc, err := http.Server(
|
||||
http.Logger(logger),
|
||||
http.Config(cfg),
|
||||
http.Context(ctx), // NOTE: not passing this "option" leads to a panic in go-micro
|
||||
http.TraceProvider(tracerProvider),
|
||||
http.Stream(evStream),
|
||||
http.RegisteredEvents(_registeredEvents),
|
||||
http.Store(evStore),
|
||||
http.GatewaySelector(gatewaySelector),
|
||||
http.HistoryClient(hClient),
|
||||
http.ValueClient(vClient),
|
||||
http.RegisteredEvents(_registeredEvents),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -23,6 +23,9 @@ type Config struct {
|
||||
RevaGateway string `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"5.0"`
|
||||
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
|
||||
|
||||
HTTP HTTP `yaml:"http"`
|
||||
TokenManager *TokenManager `yaml:"token_manager"`
|
||||
|
||||
ServiceAccount ServiceAccount `yaml:"service_account"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
@@ -46,7 +49,7 @@ type Store struct {
|
||||
Database string `yaml:"database" env:"ACTIVITYLOG_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"pre5.0"`
|
||||
Table string `yaml:"table" env:"ACTIVITYLOG_STORE_TABLE" desc:"The database table the store should use." introductionVersion:"pre5.0"`
|
||||
TTL time.Duration `yaml:"ttl" env:"OCIS_PERSISTENT_STORE_TTL;ACTIVITYLOG_STORE_TTL" desc:"Time to live for events in the store. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
Size int `yaml:"size" env:"OCIS_PERSISTENT_STORE_SIZE;ACTIVITYLOG_STORE_SIZE" desc:"The maximum quantity of items in the store. Only applies when store type 'ocmem' is configured. Defaults to 512 which is derived from the ocmem package though not exclicitly set as default." introductionVersion:"pre5.0"`
|
||||
Size int `yaml:"size" env:"OCIS_PERSISTENT_STORE_SIZE;ACTIVITYLOG_STORE_SIZE" desc:"The maximum quantity of items in the store. Only applies when store type 'ocmem' is configured. Defaults to 512 which is derived from the ocmem package though not explicitly set as default." introductionVersion:"pre5.0"`
|
||||
AuthUsername string `yaml:"username" env:"OCIS_PERSISTENT_STORE_AUTH_USERNAME;ACTIVITYLOG_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"5.0"`
|
||||
AuthPassword string `yaml:"password" env:"OCIS_PERSISTENT_STORE_AUTH_PASSWORD;ACTIVITYLOG_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"5.0"`
|
||||
}
|
||||
@@ -56,3 +59,25 @@ type ServiceAccount struct {
|
||||
ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;ACTIVITYLOG_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"5.0"`
|
||||
ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;ACTIVITYOG_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"5.0"`
|
||||
}
|
||||
|
||||
// CORS defines the available cors configuration.
|
||||
type CORS struct {
|
||||
AllowedOrigins []string `yaml:"allow_origins" env:"OCIS_CORS_ALLOW_ORIGINS;ACTIVITYLOG_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
AllowedMethods []string `yaml:"allow_methods" env:"OCIS_CORS_ALLOW_METHODS;ACTIVITYLOG_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
AllowedHeaders []string `yaml:"allow_headers" env:"OCIS_CORS_ALLOW_HEADERS;ACTIVITYLOG_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
AllowCredentials bool `yaml:"allow_credentials" env:"OCIS_CORS_ALLOW_CREDENTIALS;ACTIVITYLOG_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"pre5.0"`
|
||||
}
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"ACTIVITYLOG_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"pre5.0"`
|
||||
Namespace string `yaml:"-"`
|
||||
Root string `yaml:"root" env:"ACTIVITYLOG_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"pre5.0"`
|
||||
CORS CORS `yaml:"cors"`
|
||||
TLS shared.HTTPServiceTLS `yaml:"tls"`
|
||||
}
|
||||
|
||||
// TokenManager is the config for using the reva token manager
|
||||
type TokenManager struct {
|
||||
JWTSecret string `yaml:"jwt_secret" env:"OCIS_JWT_SECRET;ACTIVITYLOG_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"pre5.0"`
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"CLIENTLOG_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"5.0"`
|
||||
Token string `yaml:"token" env:"CLIENTLOG_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"5.0"`
|
||||
Pprof bool `yaml:"pprof" env:"CLIENTLOG_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"5.0"`
|
||||
Zpages bool `yaml:"zpages" env:"CLIENTLOG_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"5.0"`
|
||||
Addr string `yaml:"addr" env:"ACTIVITYLOG_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"5.0"`
|
||||
Token string `yaml:"token" env:"ACTIVITYLOG_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"5.0"`
|
||||
Pprof bool `yaml:"pprof" env:"ACTIVITYLOG_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"5.0"`
|
||||
Zpages bool `yaml:"zpages" env:"ACTIVITYLOG_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"5.0"`
|
||||
}
|
||||
|
||||
@@ -38,6 +38,17 @@ func DefaultConfig() *config.Config {
|
||||
Table: "",
|
||||
},
|
||||
RevaGateway: shared.DefaultRevaConfig().Address,
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:0",
|
||||
Root: "/",
|
||||
Namespace: "com.owncloud.web",
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET"},
|
||||
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Ocs-Apirequest"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +66,22 @@ func EnsureDefaults(cfg *config.Config) {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
|
||||
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
|
||||
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
|
||||
}
|
||||
|
||||
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
|
||||
cfg.TokenManager = &config.TokenManager{
|
||||
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
|
||||
}
|
||||
} else if cfg.TokenManager == nil {
|
||||
cfg.TokenManager = &config.TokenManager{}
|
||||
}
|
||||
|
||||
if cfg.Commons != nil {
|
||||
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
|
||||
}
|
||||
|
||||
// provide with defaults for shared tracing, since we need a valid destination address for "envdecode".
|
||||
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
|
||||
cfg.Tracing = &config.Tracing{
|
||||
@@ -67,9 +94,6 @@ func EnsureDefaults(cfg *config.Config) {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
|
||||
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
|
||||
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize sanitizes the config
|
||||
|
||||
@@ -2,8 +2,8 @@ package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;CLIENTLOG_USERLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"`
|
||||
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;CLIENTLOG_USERLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"`
|
||||
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;CLIENTLOG_USERLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"`
|
||||
File string `mapstructure:"file" env:"OCIS_LOG_FILE;CLIENTLOG_USERLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"`
|
||||
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;ACTIVITYLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"`
|
||||
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;ACTIVITYLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"`
|
||||
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;ACTIVITYLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"`
|
||||
File string `mapstructure:"file" env:"OCIS_LOG_FILE;ACTIVITYLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"`
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
|
||||
// Tracing defines the available tracing configuration.
|
||||
type Tracing struct {
|
||||
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;CLIENTLOG_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"5.0"`
|
||||
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;CLIENTLOG_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"5.0"`
|
||||
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;CLIENTLOG_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"5.0"`
|
||||
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;CLIENTLOG_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset." introductionVersion:"5.0"`
|
||||
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;ACTIVITYLOG_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"5.0"`
|
||||
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;ACTIVITYLOG_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"5.0"`
|
||||
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;ACTIVITYLOG_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"5.0"`
|
||||
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;ACTIVITYLOG_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset." introductionVersion:"5.0"`
|
||||
}
|
||||
|
||||
// Convert Tracing to the tracing package's Config struct.
|
||||
|
||||
139
services/activitylog/pkg/server/http/option.go
Normal file
139
services/activitylog/pkg/server/http/option.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"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"
|
||||
"go-micro.dev/v4/store"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Metrics *metrics.Metrics
|
||||
Flags []cli.Flag
|
||||
Namespace string
|
||||
Store store.Store
|
||||
Stream events.Stream
|
||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
TraceProvider trace.TracerProvider
|
||||
HistoryClient ehsvc.EventHistoryService
|
||||
ValueClient settingssvc.ValueService
|
||||
RegisteredEvents []events.Unmarshaller
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics provides a function to set the metrics option.
|
||||
func Metrics(val *metrics.Metrics) Option {
|
||||
return func(o *Options) {
|
||||
o.Metrics = val
|
||||
}
|
||||
}
|
||||
|
||||
// Flags provides a function to set the flags option.
|
||||
func Flags(val []cli.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, val...)
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides a function to set the Namespace option.
|
||||
func Namespace(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = val
|
||||
}
|
||||
}
|
||||
|
||||
// Store provides a function to configure the store
|
||||
func Store(store store.Store) Option {
|
||||
return func(o *Options) {
|
||||
o.Store = store
|
||||
}
|
||||
}
|
||||
|
||||
// Stream provides a function to configure the stream
|
||||
func Stream(stream events.Stream) Option {
|
||||
return func(o *Options) {
|
||||
o.Stream = stream
|
||||
}
|
||||
}
|
||||
|
||||
// GatewaySelector provides a function to configure the gateway client selector
|
||||
func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) Option {
|
||||
return func(o *Options) {
|
||||
o.GatewaySelector = gatewaySelector
|
||||
}
|
||||
}
|
||||
|
||||
// HistoryClient provides a function to configure the event history client
|
||||
func HistoryClient(h ehsvc.EventHistoryService) Option {
|
||||
return func(o *Options) {
|
||||
o.HistoryClient = h
|
||||
}
|
||||
}
|
||||
|
||||
// RegisteredEvents provides a function to register events
|
||||
func RegisteredEvents(evs []events.Unmarshaller) Option {
|
||||
return func(o *Options) {
|
||||
o.RegisteredEvents = evs
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider provides a function to set the TracerProvider option
|
||||
func TraceProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
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
|
||||
}
|
||||
}
|
||||
101
services/activitylog/pkg/server/http/server.go
Normal file
101
services/activitylog/pkg/server/http/server.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
stdhttp "net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/account"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/cors"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
svc "github.com/owncloud/ocis/v2/services/activitylog/pkg/service"
|
||||
"github.com/riandyrn/otelchi"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Service is the service interface
|
||||
type Service interface{}
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (http.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
service, err := http.NewService(
|
||||
http.TLSConfig(options.Config.HTTP.TLS),
|
||||
http.Logger(options.Logger),
|
||||
http.Namespace(options.Config.HTTP.Namespace),
|
||||
http.Name(options.Config.Service.Name),
|
||||
http.Version(version.GetString()),
|
||||
http.Address(options.Config.HTTP.Addr),
|
||||
http.Context(options.Context),
|
||||
http.Flags(options.Flags...),
|
||||
http.TraceProvider(options.TraceProvider),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing http service")
|
||||
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
|
||||
}
|
||||
|
||||
middlewares := []func(stdhttp.Handler) stdhttp.Handler{
|
||||
chimiddleware.RequestID,
|
||||
middleware.Version(
|
||||
options.Config.Service.Name,
|
||||
version.GetString(),
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
middleware.ExtractAccountUUID(
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.TokenManager.JWTSecret),
|
||||
),
|
||||
middleware.Cors(
|
||||
cors.Logger(options.Logger),
|
||||
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
),
|
||||
}
|
||||
|
||||
mux := chi.NewMux()
|
||||
mux.Use(middlewares...)
|
||||
|
||||
mux.Use(
|
||||
otelchi.Middleware(
|
||||
"actitivylog",
|
||||
otelchi.WithChiRoutes(mux),
|
||||
otelchi.WithTracerProvider(options.TraceProvider),
|
||||
otelchi.WithPropagators(tracing.GetPropagator()),
|
||||
),
|
||||
)
|
||||
|
||||
handle, err := svc.New(
|
||||
svc.Logger(options.Logger),
|
||||
svc.Stream(options.Stream),
|
||||
svc.Mux(mux),
|
||||
svc.Store(options.Store),
|
||||
svc.Config(options.Config),
|
||||
svc.GatewaySelector(options.GatewaySelector),
|
||||
svc.TraceProvider(options.TraceProvider),
|
||||
svc.HistoryClient(options.HistoryClient),
|
||||
svc.ValueClient(options.ValueClient),
|
||||
svc.RegisteredEvents(options.RegisteredEvents),
|
||||
)
|
||||
if err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
298
services/activitylog/pkg/service/http.go
Normal file
298
services/activitylog/pkg/service/http.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
"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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// HandleGetItemActivities handles the request to get the activities of an item.
|
||||
func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, r.Header.Get("X-Access-Token"))
|
||||
|
||||
activeUser, ok := revactx.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rid, limit, rawActivityAccepted, activityAccepted, err := s.getFilters(r.URL.Query().Get("kql"))
|
||||
if err != nil {
|
||||
s.log.Info().Str("query", r.URL.Query().Get("kql")).Err(err).Msg("error getting filters")
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := s.Activities(rid)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msg("error getting activities")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(raw))
|
||||
toDelete := make(map[string]struct{}, len(raw))
|
||||
for _, a := range raw {
|
||||
if !rawActivityAccepted(a) {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, a.EventID)
|
||||
toDelete[a.EventID] = struct{}{}
|
||||
}
|
||||
|
||||
evRes, err := s.evHistory.GetEvents(r.Context(), &ehsvc.GetEventsRequest{Ids: ids})
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msg("error getting events")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var resp GetActivitiesResponse
|
||||
for _, e := range evRes.GetEvents() {
|
||||
delete(toDelete, e.GetId())
|
||||
|
||||
if limit != 0 && len(resp.Activities) >= limit {
|
||||
continue
|
||||
}
|
||||
|
||||
if !activityAccepted(e) {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
message string
|
||||
ts time.Time
|
||||
vars map[string]interface{}
|
||||
)
|
||||
|
||||
switch ev := s.unwrapEvent(e).(type) {
|
||||
case nil:
|
||||
// error already logged in unwrapEvent
|
||||
continue
|
||||
case events.UploadReady:
|
||||
message = MessageResourceCreated
|
||||
ts = utils.TSToTime(ev.Timestamp)
|
||||
vars, err = s.GetVars(ctx, WithResource(ev.FileRef, true), WithUser(ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName()))
|
||||
case events.FileTouched:
|
||||
message = MessageResourceCreated
|
||||
ts = utils.TSToTime(ev.Timestamp)
|
||||
vars, err = s.GetVars(ctx, WithResource(ev.Ref, true), WithUser(ev.Executant, ""))
|
||||
case events.ContainerCreated:
|
||||
message = MessageResourceCreated
|
||||
ts = utils.TSToTime(ev.Timestamp)
|
||||
vars, err = s.GetVars(ctx, WithResource(ev.Ref, true), WithUser(ev.Executant, ""))
|
||||
case events.ItemTrashed:
|
||||
message = MessageResourceTrashed
|
||||
ts = utils.TSToTime(ev.Timestamp)
|
||||
vars, err = s.GetVars(ctx, WithTrashedResource(ev.Ref, ev.ID), WithUser(ev.Executant, ""), WithSpace(toSpace(ev.Ref)))
|
||||
case events.ItemMoved:
|
||||
switch isRename(ev.OldReference, ev.Ref) {
|
||||
case true:
|
||||
message = MessageResourceRenamed
|
||||
vars, err = s.GetVars(ctx, WithResource(ev.Ref, false), WithOldResource(ev.OldReference), WithUser(ev.Executant, ""))
|
||||
case false:
|
||||
message = MessageResourceMoved
|
||||
vars, err = s.GetVars(ctx, WithResource(ev.Ref, true), WithUser(ev.Executant, ""))
|
||||
}
|
||||
ts = utils.TSToTime(ev.Timestamp)
|
||||
case events.ShareCreated:
|
||||
message = MessageShareCreated
|
||||
ts = utils.TSToTime(ev.CTime)
|
||||
vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID))
|
||||
case events.ShareRemoved:
|
||||
message = MessageShareDeleted
|
||||
ts = ev.Timestamp
|
||||
vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID))
|
||||
case events.LinkCreated:
|
||||
message = MessageLinkCreated
|
||||
ts = utils.TSToTime(ev.CTime)
|
||||
vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""))
|
||||
case events.LinkRemoved:
|
||||
message = MessageLinkDeleted
|
||||
ts = utils.TSToTime(ev.Timestamp)
|
||||
vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""))
|
||||
case events.SpaceShared:
|
||||
message = MessageSpaceShared
|
||||
ts = ev.Timestamp
|
||||
vars, err = s.GetVars(ctx, WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID))
|
||||
case events.SpaceUnshared:
|
||||
message = MessageSpaceUnshared
|
||||
ts = ev.Timestamp
|
||||
vars, err = s.GetVars(ctx, WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msg("error getting response data")
|
||||
continue
|
||||
}
|
||||
|
||||
// FIXME: 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), ts, e.GetId(), vars))
|
||||
}
|
||||
|
||||
// delete activities in separate go routine
|
||||
if len(toDelete) > 0 {
|
||||
go func() {
|
||||
err := s.RemoveActivities(rid, toDelete)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msg("error removing activities")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msg("error marshalling activities")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(b); err != nil {
|
||||
s.log.Error().Err(err).Msg("error writing response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *ActivitylogService) unwrapEvent(e *ehmsg.Event) interface{} {
|
||||
etype, ok := s.registeredEvents[e.GetType()]
|
||||
if !ok {
|
||||
s.log.Error().Str("eventid", e.GetId()).Str("eventtype", e.GetType()).Msg("event not registered")
|
||||
return nil
|
||||
}
|
||||
|
||||
einterface, err := etype.Unmarshal(e.GetEvent())
|
||||
if err != nil {
|
||||
s.log.Error().Str("eventid", e.GetId()).Str("eventtype", e.GetType()).Msg("failed to umarshal event")
|
||||
return nil
|
||||
}
|
||||
|
||||
return einterface
|
||||
}
|
||||
|
||||
func (s *ActivitylogService) getFilters(query string) (*provider.ResourceId, int, func(RawActivity) bool, func(*ehmsg.Event) bool, error) {
|
||||
qast, err := kql.Builder{}.Build(query)
|
||||
if err != nil {
|
||||
return nil, 0, nil, nil, err
|
||||
}
|
||||
|
||||
prefilters := make([]func(RawActivity) bool, 0)
|
||||
postfilters := make([]func(*ehmsg.Event) bool, 0)
|
||||
|
||||
var (
|
||||
itemID string
|
||||
limit int
|
||||
)
|
||||
|
||||
for _, n := range qast.Nodes {
|
||||
switch v := n.(type) {
|
||||
case *ast.StringNode:
|
||||
switch strings.ToLower(v.Key) {
|
||||
case "itemid":
|
||||
itemID = v.Value
|
||||
case "depth":
|
||||
depth, err := strconv.Atoi(v.Value)
|
||||
if err != nil {
|
||||
return nil, limit, nil, nil, err
|
||||
}
|
||||
|
||||
prefilters = append(prefilters, func(a RawActivity) bool {
|
||||
return a.Depth <= depth
|
||||
})
|
||||
case "limit":
|
||||
l, err := strconv.Atoi(v.Value)
|
||||
if err != nil {
|
||||
return nil, limit, nil, nil, err
|
||||
}
|
||||
|
||||
limit = l
|
||||
}
|
||||
case *ast.DateTimeNode:
|
||||
switch v.Operator.Value {
|
||||
case "<", "<=":
|
||||
prefilters = append(prefilters, func(a RawActivity) bool {
|
||||
return a.Timestamp.Before(v.Value)
|
||||
})
|
||||
case ">", ">=":
|
||||
prefilters = append(prefilters, func(a RawActivity) bool {
|
||||
return a.Timestamp.After(v.Value)
|
||||
})
|
||||
}
|
||||
case *ast.OperatorNode:
|
||||
if v.Value != "AND" {
|
||||
return nil, limit, nil, nil, errors.New("only AND operator is supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rid, err := storagespace.ParseID(itemID)
|
||||
if err != nil {
|
||||
return nil, limit, nil, nil, err
|
||||
}
|
||||
pref := func(a RawActivity) bool {
|
||||
for _, f := range prefilters {
|
||||
if !f(a) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
postf := func(e *ehmsg.Event) bool {
|
||||
for _, f := range postfilters {
|
||||
if !f(e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return &rid, limit, pref, postf, nil
|
||||
}
|
||||
|
||||
// returns true if this is just a rename
|
||||
func isRename(o, n *provider.Reference) bool {
|
||||
// if resourceids are different we assume it is a move
|
||||
if !utils.ResourceIDEqual(o.GetResourceId(), n.GetResourceId()) {
|
||||
return false
|
||||
}
|
||||
return filepath.Base(o.GetPath()) != filepath.Base(n.GetPath())
|
||||
}
|
||||
22
services/activitylog/pkg/service/l10n/userlog.pot
Normal file
22
services/activitylog/pkg/service/l10n/userlog.pot
Normal file
@@ -0,0 +1,22 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr "Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2024-06-14 15:29+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: pkg/service/response.go:15
|
||||
msgid "{user} created {resource}"
|
||||
msgstr ""
|
||||
|
||||
@@ -4,7 +4,10 @@ import (
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"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"
|
||||
@@ -22,6 +25,9 @@ type Options struct {
|
||||
RegisteredEvents []events.Unmarshaller
|
||||
Store microstore.Store
|
||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
Mux *chi.Mux
|
||||
HistoryClient ehsvc.EventHistoryService
|
||||
ValueClient settingssvc.ValueService
|
||||
}
|
||||
|
||||
// Logger configures a logger for the activitylog service
|
||||
@@ -72,3 +78,24 @@ func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient])
|
||||
o.GatewaySelector = gatewaySelector
|
||||
}
|
||||
}
|
||||
|
||||
// Mux defines the muxer for the service
|
||||
func Mux(m *chi.Mux) Option {
|
||||
return func(o *Options) {
|
||||
o.Mux = m
|
||||
}
|
||||
}
|
||||
|
||||
// HistoryClient adds a grpc client for the eventhistory service
|
||||
func HistoryClient(hc ehsvc.EventHistoryService) Option {
|
||||
return func(o *Options) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
217
services/activitylog/pkg/service/response.go
Normal file
217
services/activitylog/pkg/service/response.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
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"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
)
|
||||
|
||||
// Translations
|
||||
var (
|
||||
MessageResourceCreated = l10n.Template("{user} added {resource} to {space}")
|
||||
MessageResourceTrashed = l10n.Template("{user} deleted {resource} from {space}")
|
||||
MessageResourceMoved = l10n.Template("{user} moved {resource} to {space}")
|
||||
MessageResourceRenamed = l10n.Template("{user} renamed {oldResource} to {resource}")
|
||||
MessageShareCreated = l10n.Template("{user} shared {resource} with {sharee}")
|
||||
MessageShareDeleted = l10n.Template("{user} removed {sharee} from {resource}")
|
||||
MessageLinkCreated = l10n.Template("{user} shared {resource} via link")
|
||||
MessageLinkDeleted = l10n.Template("{user} removed link to {resource}")
|
||||
MessageSpaceShared = l10n.Template("{user} added {sharee} as member of {space}")
|
||||
MessageSpaceUnshared = l10n.Template("{user} removed {sharee} from {space}")
|
||||
)
|
||||
|
||||
// GetActivitiesResponse is the response on GET activities requests
|
||||
type GetActivitiesResponse struct {
|
||||
Activities []libregraph.Activity `json:"value"`
|
||||
}
|
||||
|
||||
// Resource represents an item such as a file or folder
|
||||
type Resource struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Actor represents a user
|
||||
type Actor struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
// ActivityOption allows setting variables for an activity
|
||||
type ActivityOption func(context.Context, gateway.GatewayAPIClient, map[string]interface{}) error
|
||||
|
||||
// WithResource sets the resource variable for an activity
|
||||
func WithResource(ref *provider.Reference, addSpace bool) ActivityOption {
|
||||
return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error {
|
||||
info, err := utils.GetResource(ctx, ref, gwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vars["resource"] = Resource{
|
||||
ID: storagespace.FormatResourceID(*info.GetId()),
|
||||
Name: info.GetName(),
|
||||
}
|
||||
|
||||
if addSpace {
|
||||
vars["space"] = Resource{
|
||||
ID: info.GetSpace().GetId().GetOpaqueId(),
|
||||
Name: info.GetSpace().GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithOldResource sets the oldResource variable for an activity
|
||||
func WithOldResource(ref *provider.Reference) ActivityOption {
|
||||
return func(_ context.Context, _ gateway.GatewayAPIClient, vars map[string]interface{}) error {
|
||||
name := filepath.Base(ref.GetPath())
|
||||
vars["oldResource"] = Resource{
|
||||
Name: name,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrashedResource sets the resource variable if the resource is trashed
|
||||
func WithTrashedResource(ref *provider.Reference, rid *provider.ResourceId) ActivityOption {
|
||||
return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error {
|
||||
resp, err := gwc.ListRecycle(ctx, &provider.ListRecycleRequest{
|
||||
Ref: ref,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return fmt.Errorf("error listing recycle: %s", resp.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
for _, item := range resp.GetRecycleItems() {
|
||||
if item.GetKey() == rid.GetOpaqueId() {
|
||||
|
||||
vars["resource"] = Resource{
|
||||
ID: storagespace.FormatResourceID(*rid),
|
||||
Name: filepath.Base(item.GetRef().GetPath()),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUser sets the user variable for an Activity
|
||||
func WithUser(uid *user.UserId, username string) ActivityOption {
|
||||
return func(_ context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error {
|
||||
if username == "" {
|
||||
u, err := utils.GetUser(uid, gwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
username = u.GetUsername()
|
||||
}
|
||||
|
||||
vars["user"] = Actor{
|
||||
ID: uid.GetOpaqueId(),
|
||||
DisplayName: username,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSharee sets the sharee variable for an activity
|
||||
func WithSharee(uid *user.UserId, gid *group.GroupId) ActivityOption {
|
||||
return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error {
|
||||
switch {
|
||||
case uid != nil:
|
||||
u, err := utils.GetUser(uid, gwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vars["sharee"] = Actor{
|
||||
ID: uid.GetOpaqueId(),
|
||||
DisplayName: u.GetUsername(),
|
||||
}
|
||||
case gid != nil:
|
||||
r, err := gwc.GetGroup(ctx, &group.GetGroupRequest{GroupId: gid})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting group: %w", err)
|
||||
}
|
||||
|
||||
if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return fmt.Errorf("error getting group: %s", r.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
vars["sharee"] = Actor{
|
||||
ID: gid.GetOpaqueId(),
|
||||
DisplayName: r.GetGroup().GetDisplayName(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSpace sets the space variable for an activity
|
||||
func WithSpace(spaceid *provider.StorageSpaceId) ActivityOption {
|
||||
return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error {
|
||||
s, err := utils.GetSpace(ctx, spaceid.GetOpaqueId(), gwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vars["space"] = Resource{
|
||||
ID: s.GetId().GetOpaqueId(),
|
||||
Name: s.GetName(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewActivity creates a new activity
|
||||
func NewActivity(message string, ts time.Time, eventID string, vars map[string]interface{}) libregraph.Activity {
|
||||
return libregraph.Activity{
|
||||
Id: eventID,
|
||||
Times: libregraph.ActivityTimes{RecordedTime: ts},
|
||||
Template: libregraph.ActivityTemplate{
|
||||
Message: message,
|
||||
Variables: vars,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetVars calls other service to gather the required data for the activity variables
|
||||
func (s *ActivitylogService) GetVars(ctx context.Context, opts ...ActivityOption) (map[string]interface{}, error) {
|
||||
gwc, err := s.gws.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vars := make(map[string]interface{})
|
||||
for _, opt := range opts {
|
||||
if err := opt(ctx, gwc, vars); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return vars, nil
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
@@ -13,13 +15,17 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/activitylog/pkg/config"
|
||||
"github.com/go-chi/chi/v5"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// Activity represents an activity
|
||||
type Activity struct {
|
||||
// RawActivity represents an activity as it is stored in the activitylog store
|
||||
type RawActivity struct {
|
||||
EventID string `json:"event_id"`
|
||||
Depth int `json:"depth"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
@@ -27,11 +33,17 @@ type Activity 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]
|
||||
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
|
||||
lock sync.RWMutex
|
||||
|
||||
registeredEvents map[string]events.Unmarshaller
|
||||
}
|
||||
|
||||
// New creates a new ActivitylogService
|
||||
@@ -55,18 +67,32 @@ func New(opts ...Option) (*ActivitylogService, error) {
|
||||
}
|
||||
|
||||
s := &ActivitylogService{
|
||||
log: o.Logger,
|
||||
cfg: o.Config,
|
||||
events: ch,
|
||||
store: o.Store,
|
||||
gws: o.GatewaySelector,
|
||||
log: o.Logger,
|
||||
cfg: o.Config,
|
||||
events: ch,
|
||||
store: o.Store,
|
||||
gws: o.GatewaySelector,
|
||||
mux: o.Mux,
|
||||
evHistory: o.HistoryClient,
|
||||
valService: o.ValueClient,
|
||||
lock: sync.RWMutex{},
|
||||
registeredEvents: make(map[string]events.Unmarshaller),
|
||||
}
|
||||
|
||||
s.mux.Get("/graph/v1beta1/extensions/org.libregraph/activities", s.HandleGetItemActivities)
|
||||
|
||||
for _, e := range o.RegisteredEvents {
|
||||
typ := reflect.TypeOf(e)
|
||||
s.registeredEvents[typ.String()] = e
|
||||
}
|
||||
|
||||
go s.Run()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Run runs the service
|
||||
func (a *ActivitylogService) Run() error {
|
||||
func (a *ActivitylogService) Run() {
|
||||
for e := range a.events {
|
||||
var err error
|
||||
switch ev := e.Event.(type) {
|
||||
@@ -78,35 +104,26 @@ func (a *ActivitylogService) Run() error {
|
||||
err = a.AddActivity(ev.Ref, e.ID, utils.TSToTime(ev.Timestamp))
|
||||
case events.ItemTrashed:
|
||||
err = a.AddActivityTrashed(ev.ID, ev.Ref, e.ID, utils.TSToTime(ev.Timestamp))
|
||||
case events.ItemPurged:
|
||||
err = a.AddActivity(ev.Ref, e.ID, utils.TSToTime(ev.Timestamp))
|
||||
case events.ItemMoved:
|
||||
err = a.AddActivity(ev.Ref, e.ID, utils.TSToTime(ev.Timestamp))
|
||||
case events.ShareCreated:
|
||||
err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.CTime))
|
||||
case events.ShareUpdated:
|
||||
err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.MTime))
|
||||
case events.ShareRemoved:
|
||||
err = a.AddActivity(toRef(ev.ItemID), e.ID, ev.Timestamp)
|
||||
case events.LinkCreated:
|
||||
err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.CTime))
|
||||
case events.LinkUpdated:
|
||||
err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.CTime))
|
||||
case events.LinkRemoved:
|
||||
err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.Timestamp))
|
||||
case events.SpaceShared:
|
||||
err = a.AddActivity(sToRef(ev.ID), e.ID, ev.Timestamp)
|
||||
case events.SpaceShareUpdated:
|
||||
err = a.AddActivity(sToRef(ev.ID), e.ID, ev.Timestamp)
|
||||
err = a.AddSpaceActivity(ev.ID, e.ID, ev.Timestamp)
|
||||
case events.SpaceUnshared:
|
||||
err = a.AddActivity(sToRef(ev.ID), e.ID, ev.Timestamp)
|
||||
err = a.AddSpaceActivity(ev.ID, e.ID, ev.Timestamp)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
a.log.Error().Err(err).Interface("event", e).Msg("could not process event")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddActivity adds the activity to the given resource and all its parents
|
||||
@@ -139,7 +156,7 @@ func (a *ActivitylogService) AddActivityTrashed(resourceID *provider.ResourceId,
|
||||
}
|
||||
|
||||
// store activity on trashed item
|
||||
if err := a.storeActivity(resourceID, eventID, 0, timestamp); err != nil {
|
||||
if err := a.storeActivity(storagespace.FormatResourceID(*resourceID), eventID, 0, timestamp); err != nil {
|
||||
return fmt.Errorf("could not store activity: %w", err)
|
||||
}
|
||||
|
||||
@@ -154,8 +171,57 @@ func (a *ActivitylogService) AddActivityTrashed(resourceID *provider.ResourceId,
|
||||
})
|
||||
}
|
||||
|
||||
// AddSpaceActivity adds the activity to the given spaceroot
|
||||
func (a *ActivitylogService) AddSpaceActivity(spaceID *provider.StorageSpaceId, eventID string, timestamp time.Time) error {
|
||||
// spaceID is in format <providerid>$<spaceid>
|
||||
// activitylog service uses format <providerid>$<spaceid>!<resourceid>
|
||||
// lets do some converting, shall we?
|
||||
rid, err := storagespace.ParseID(spaceID.GetOpaqueId())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse space id: %w", err)
|
||||
}
|
||||
rid.OpaqueId = rid.GetSpaceId()
|
||||
return a.storeActivity(storagespace.FormatResourceID(rid), eventID, 0, timestamp)
|
||||
|
||||
}
|
||||
|
||||
// Activities returns the activities for the given resource
|
||||
func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]Activity, error) {
|
||||
func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]RawActivity, error) {
|
||||
a.lock.RLock()
|
||||
defer a.lock.RUnlock()
|
||||
|
||||
return a.activities(rid)
|
||||
}
|
||||
|
||||
// RemoveActivities removes the activities from the given resource
|
||||
func (a *ActivitylogService) RemoveActivities(rid *provider.ResourceId, toDelete map[string]struct{}) error {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
curActivities, err := a.activities(rid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var acts []RawActivity
|
||||
for _, a := range curActivities {
|
||||
if _, ok := toDelete[a.EventID]; !ok {
|
||||
acts = append(acts, a)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(acts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.store.Write(µstore.Record{
|
||||
Key: storagespace.FormatResourceID(*rid),
|
||||
Value: b,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ActivitylogService) activities(rid *provider.ResourceId) ([]RawActivity, error) {
|
||||
resourceID := storagespace.FormatResourceID(*rid)
|
||||
|
||||
records, err := a.store.Read(resourceID)
|
||||
@@ -164,10 +230,10 @@ func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]Activity, e
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return []Activity{}, nil
|
||||
return []RawActivity{}, nil
|
||||
}
|
||||
|
||||
var activities []Activity
|
||||
var activities []RawActivity
|
||||
if err := json.Unmarshal(records[0].Value, &activities); err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal activities: %w", err)
|
||||
}
|
||||
@@ -189,7 +255,7 @@ func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID st
|
||||
return fmt.Errorf("could not get resource info: %w", err)
|
||||
}
|
||||
|
||||
if err := a.storeActivity(info.GetId(), eventID, depth, timestamp); err != nil {
|
||||
if err := a.storeActivity(storagespace.FormatResourceID(*info.GetId()), eventID, depth, timestamp); err != nil {
|
||||
return fmt.Errorf("could not store activity: %w", err)
|
||||
}
|
||||
|
||||
@@ -202,19 +268,16 @@ func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID st
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ActivitylogService) storeActivity(rid *provider.ResourceId, eventID string, depth int, timestamp time.Time) error {
|
||||
if rid == nil {
|
||||
return errors.New("resource id is required")
|
||||
}
|
||||
|
||||
resourceID := storagespace.FormatResourceID(*rid)
|
||||
func (a *ActivitylogService) storeActivity(resourceID string, eventID string, depth int, timestamp time.Time) error {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
records, err := a.store.Read(resourceID)
|
||||
if err != nil && err != microstore.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
var activities []Activity
|
||||
var activities []RawActivity
|
||||
if len(records) > 0 {
|
||||
if err := json.Unmarshal(records[0].Value, &activities); err != nil {
|
||||
return err
|
||||
@@ -222,7 +285,7 @@ func (a *ActivitylogService) storeActivity(rid *provider.ResourceId, eventID str
|
||||
}
|
||||
|
||||
// TODO: max len check?
|
||||
activities = append(activities, Activity{
|
||||
activities = append(activities, RawActivity{
|
||||
EventID: eventID,
|
||||
Depth: depth,
|
||||
Timestamp: timestamp,
|
||||
@@ -245,11 +308,8 @@ func toRef(r *provider.ResourceId) *provider.Reference {
|
||||
}
|
||||
}
|
||||
|
||||
func sToRef(s *provider.StorageSpaceId) *provider.Reference {
|
||||
return &provider.Reference{
|
||||
ResourceId: &provider.ResourceId{
|
||||
OpaqueId: s.GetOpaqueId(),
|
||||
SpaceId: s.GetOpaqueId(),
|
||||
},
|
||||
func toSpace(r *provider.Reference) *provider.StorageSpaceId {
|
||||
return &provider.StorageSpaceId{
|
||||
OpaqueId: storagespace.FormatStorageID(r.GetResourceId().GetStorageId(), r.GetResourceId().GetSpaceId()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestAddActivity(t *testing.T) {
|
||||
Name string
|
||||
Tree map[string]*provider.ResourceInfo
|
||||
Activities map[string]string
|
||||
Expected map[string][]Activity
|
||||
Expected map[string][]RawActivity
|
||||
}{
|
||||
{
|
||||
Name: "simple",
|
||||
@@ -26,7 +26,7 @@ func TestAddActivity(t *testing.T) {
|
||||
Activities: map[string]string{
|
||||
"activity": "base",
|
||||
},
|
||||
Expected: map[string][]Activity{
|
||||
Expected: map[string][]RawActivity{
|
||||
"base": activitites("activity", 0),
|
||||
"parent": activitites("activity", 1),
|
||||
"spaceid": activitites("activity", 2),
|
||||
@@ -43,7 +43,7 @@ func TestAddActivity(t *testing.T) {
|
||||
"activity1": "base",
|
||||
"activity2": "base",
|
||||
},
|
||||
Expected: map[string][]Activity{
|
||||
Expected: map[string][]RawActivity{
|
||||
"base": activitites("activity1", 0, "activity2", 0),
|
||||
"parent": activitites("activity1", 1, "activity2", 1),
|
||||
"spaceid": activitites("activity1", 2, "activity2", 2),
|
||||
@@ -61,7 +61,7 @@ func TestAddActivity(t *testing.T) {
|
||||
"activity1": "base1",
|
||||
"activity2": "base2",
|
||||
},
|
||||
Expected: map[string][]Activity{
|
||||
Expected: map[string][]RawActivity{
|
||||
"base1": activitites("activity1", 0),
|
||||
"base2": activitites("activity2", 0),
|
||||
"parent": activitites("activity1", 1, "activity2", 1),
|
||||
@@ -83,7 +83,7 @@ func TestAddActivity(t *testing.T) {
|
||||
"activity2": "base2",
|
||||
"activity3": "base3",
|
||||
},
|
||||
Expected: map[string][]Activity{
|
||||
Expected: map[string][]RawActivity{
|
||||
"base1": activitites("activity1", 0),
|
||||
"base2": activitites("activity2", 0),
|
||||
"base3": activitites("activity3", 0),
|
||||
@@ -109,7 +109,7 @@ func TestAddActivity(t *testing.T) {
|
||||
"activity3": "base3",
|
||||
"activity4": "parent2",
|
||||
},
|
||||
Expected: map[string][]Activity{
|
||||
Expected: map[string][]RawActivity{
|
||||
"base1": activitites("activity1", 0),
|
||||
"base2": activitites("activity2", 0),
|
||||
"base3": activitites("activity3", 0),
|
||||
@@ -143,9 +143,9 @@ func TestAddActivity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func activitites(acts ...interface{}) []Activity {
|
||||
var activities []Activity
|
||||
act := Activity{}
|
||||
func activitites(acts ...interface{}) []RawActivity {
|
||||
var activities []RawActivity
|
||||
act := RawActivity{}
|
||||
for _, a := range acts {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
|
||||
@@ -33,9 +33,10 @@ func DefaultConfig() *config.Config {
|
||||
EnableTLS: false,
|
||||
},
|
||||
Store: config.Store{
|
||||
Store: "memory",
|
||||
Store: "nats-js-kv",
|
||||
Nodes: []string{"127.0.0.1:9233"},
|
||||
Database: "eventhistory",
|
||||
Table: "events",
|
||||
Table: "",
|
||||
TTL: 336 * time.Hour,
|
||||
},
|
||||
GRPC: config.GRPCConfig{
|
||||
|
||||
@@ -130,6 +130,10 @@ func (eh *EventHistoryService) getEvent(id string) (*ehmsg.Event, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(evs) == 0 {
|
||||
return nil, store.ErrNotFound
|
||||
}
|
||||
|
||||
var ev StoreEvent
|
||||
if err := json.Unmarshal(evs[0].Value, &ev); err != nil {
|
||||
eh.log.Error().Err(err).Str("eventid", id).Msg("could not unmarshal event")
|
||||
|
||||
@@ -236,6 +236,10 @@ func DefaultPolicies() []config.Policy {
|
||||
Endpoint: "/app/", // /app or /apps? ocdav only handles /apps
|
||||
Service: "com.owncloud.web.frontend",
|
||||
},
|
||||
{
|
||||
Endpoint: "/graph/v1beta1/extensions/org.libregraph/activities",
|
||||
Service: "com.owncloud.web.activitylog",
|
||||
},
|
||||
{
|
||||
Endpoint: "/graph/v1.0/invitations",
|
||||
Service: "com.owncloud.web.invitations",
|
||||
|
||||
@@ -26,7 +26,7 @@ include ../../.make/generate.mk
|
||||
.PHONY: ci-go-generate
|
||||
ci-go-generate: $(PIGEON) $(MOCKERY) # CI runs ci-node-generate automatically before this target
|
||||
$(MOCKERY)
|
||||
$(PIGEON) -optimize-grammar -optimize-parser -o pkg/query/kql/dictionary_gen.go pkg/query/kql/dictionary.peg
|
||||
$(PIGEON) -optimize-grammar -optimize-parser -o ../../ocis-pkg/kql/dictionary_gen.go ../../ocis-pkg/kql/dictionary.peg
|
||||
|
||||
.PHONY: ci-node-generate
|
||||
ci-node-generate:
|
||||
|
||||
@@ -4,8 +4,8 @@ package bleve
|
||||
import (
|
||||
bQuery "github.com/blevesearch/bleve/v2/search/query"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
)
|
||||
|
||||
// Creator is combines a Builder and a Compiler which is used to Create the query.
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
bleveQuery "github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
)
|
||||
|
||||
var _fields = map[string]string{
|
||||
|
||||
@@ -5,9 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
tAssert "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
var timeMustParse = func(t *testing.T, ts string) time.Time {
|
||||
|
||||
@@ -3,7 +3,7 @@ package query
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
)
|
||||
|
||||
// StartsWithBinaryOperatorError records an error and the operation that caused it.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Package query provides functions to work with the different search query flavours.
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
import "github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
|
||||
// Builder is the interface that wraps the basic Build method.
|
||||
type Builder interface {
|
||||
|
||||
5
vendor/github.com/owncloud/libre-graph-api-go/README.md
generated
vendored
5
vendor/github.com/owncloud/libre-graph-api-go/README.md
generated
vendored
@@ -78,6 +78,7 @@ All URIs are relative to *https://ocis.ocis-traefik.latest.owncloud.works/graph*
|
||||
|
||||
Class | Method | HTTP request | Description
|
||||
------------ | ------------- | ------------- | -------------
|
||||
*ActivitiesApi* | [**GetActivities**](docs/ActivitiesApi.md#getactivities) | **Get** /v1beta1/extensions/org.libregraph/activities | Get activities
|
||||
*ApplicationsApi* | [**GetApplication**](docs/ApplicationsApi.md#getapplication) | **Get** /v1.0/applications/{application-id} | Get application by id
|
||||
*ApplicationsApi* | [**ListApplications**](docs/ApplicationsApi.md#listapplications) | **Get** /v1.0/applications | Get all applications
|
||||
*DriveItemApi* | [**DeleteDriveItem**](docs/DriveItemApi.md#deletedriveitem) | **Delete** /v1beta1/drives/{drive-id}/items/{item-id} | Delete a DriveItem.
|
||||
@@ -168,6 +169,9 @@ Class | Method | HTTP request | Description
|
||||
|
||||
## Documentation For Models
|
||||
|
||||
- [Activity](docs/Activity.md)
|
||||
- [ActivityTemplate](docs/ActivityTemplate.md)
|
||||
- [ActivityTimes](docs/ActivityTimes.md)
|
||||
- [AppRole](docs/AppRole.md)
|
||||
- [AppRoleAssignment](docs/AppRoleAssignment.md)
|
||||
- [Application](docs/Application.md)
|
||||
@@ -175,6 +179,7 @@ Class | Method | HTTP request | Description
|
||||
- [ClassMemberReference](docs/ClassMemberReference.md)
|
||||
- [ClassReference](docs/ClassReference.md)
|
||||
- [ClassTeacherReference](docs/ClassTeacherReference.md)
|
||||
- [CollectionOfActivities](docs/CollectionOfActivities.md)
|
||||
- [CollectionOfAppRoleAssignments](docs/CollectionOfAppRoleAssignments.md)
|
||||
- [CollectionOfApplications](docs/CollectionOfApplications.md)
|
||||
- [CollectionOfClass](docs/CollectionOfClass.md)
|
||||
|
||||
137
vendor/github.com/owncloud/libre-graph-api-go/api_activities.go
generated
vendored
Normal file
137
vendor/github.com/owncloud/libre-graph-api-go/api_activities.go
generated
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Libre Graph API
|
||||
|
||||
Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
|
||||
|
||||
API version: v1.0.4
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package libregraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ActivitiesApiService ActivitiesApi service
|
||||
type ActivitiesApiService service
|
||||
|
||||
type ApiGetActivitiesRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *ActivitiesApiService
|
||||
kql *string
|
||||
}
|
||||
|
||||
func (r ApiGetActivitiesRequest) Kql(kql string) ApiGetActivitiesRequest {
|
||||
r.kql = &kql
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiGetActivitiesRequest) Execute() (*CollectionOfActivities, *http.Response, error) {
|
||||
return r.ApiService.GetActivitiesExecute(r)
|
||||
}
|
||||
|
||||
/*
|
||||
GetActivities Get activities
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@return ApiGetActivitiesRequest
|
||||
*/
|
||||
func (a *ActivitiesApiService) GetActivities(ctx context.Context) ApiGetActivitiesRequest {
|
||||
return ApiGetActivitiesRequest{
|
||||
ApiService: a,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes the request
|
||||
//
|
||||
// @return CollectionOfActivities
|
||||
func (a *ActivitiesApiService) GetActivitiesExecute(r ApiGetActivitiesRequest) (*CollectionOfActivities, *http.Response, error) {
|
||||
var (
|
||||
localVarHTTPMethod = http.MethodGet
|
||||
localVarPostBody interface{}
|
||||
formFiles []formFile
|
||||
localVarReturnValue *CollectionOfActivities
|
||||
)
|
||||
|
||||
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ActivitiesApiService.GetActivities")
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||
}
|
||||
|
||||
localVarPath := localBasePath + "/v1beta1/extensions/org.libregraph/activities"
|
||||
|
||||
localVarHeaderParams := make(map[string]string)
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
if r.kql != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "kql", r.kql, "")
|
||||
}
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
// set Content-Type header
|
||||
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||
if localVarHTTPContentType != "" {
|
||||
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||
}
|
||||
|
||||
// to determine the Accept header
|
||||
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||
|
||||
// set Accept header
|
||||
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||
if localVarHTTPHeaderAccept != "" {
|
||||
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||
}
|
||||
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, err
|
||||
}
|
||||
|
||||
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||
if err != nil || localVarHTTPResponse == nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||
localVarHTTPResponse.Body.Close()
|
||||
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||
if err != nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
if localVarHTTPResponse.StatusCode >= 300 {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: localVarHTTPResponse.Status,
|
||||
}
|
||||
var v OdataError
|
||||
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr.error = err.Error()
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
|
||||
newErr.model = v
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: err.Error(),
|
||||
}
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
return localVarReturnValue, localVarHTTPResponse, nil
|
||||
}
|
||||
3
vendor/github.com/owncloud/libre-graph-api-go/client.go
generated
vendored
3
vendor/github.com/owncloud/libre-graph-api-go/client.go
generated
vendored
@@ -48,6 +48,8 @@ type APIClient struct {
|
||||
|
||||
// API Services
|
||||
|
||||
ActivitiesApi *ActivitiesApiService
|
||||
|
||||
ApplicationsApi *ApplicationsApiService
|
||||
|
||||
DriveItemApi *DriveItemApiService
|
||||
@@ -111,6 +113,7 @@ func NewAPIClient(cfg *Configuration) *APIClient {
|
||||
c.common.client = c
|
||||
|
||||
// API Services
|
||||
c.ActivitiesApi = (*ActivitiesApiService)(&c.common)
|
||||
c.ApplicationsApi = (*ApplicationsApiService)(&c.common)
|
||||
c.DriveItemApi = (*DriveItemApiService)(&c.common)
|
||||
c.DrivesApi = (*DrivesApiService)(&c.common)
|
||||
|
||||
213
vendor/github.com/owncloud/libre-graph-api-go/model_activity.go
generated
vendored
Normal file
213
vendor/github.com/owncloud/libre-graph-api-go/model_activity.go
generated
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
Libre Graph API
|
||||
|
||||
Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
|
||||
|
||||
API version: v1.0.4
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package libregraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the Activity type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &Activity{}
|
||||
|
||||
// Activity Represents activity.
|
||||
type Activity struct {
|
||||
// Activity ID.
|
||||
Id string `json:"id"`
|
||||
Times ActivityTimes `json:"times"`
|
||||
Template ActivityTemplate `json:"template"`
|
||||
}
|
||||
|
||||
type _Activity Activity
|
||||
|
||||
// NewActivity instantiates a new Activity object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewActivity(id string, times ActivityTimes, template ActivityTemplate) *Activity {
|
||||
this := Activity{}
|
||||
this.Id = id
|
||||
this.Times = times
|
||||
this.Template = template
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewActivityWithDefaults instantiates a new Activity object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewActivityWithDefaults() *Activity {
|
||||
this := Activity{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetId returns the Id field value
|
||||
func (o *Activity) GetId() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Id
|
||||
}
|
||||
|
||||
// GetIdOk returns a tuple with the Id field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *Activity) GetIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Id, true
|
||||
}
|
||||
|
||||
// SetId sets field value
|
||||
func (o *Activity) SetId(v string) {
|
||||
o.Id = v
|
||||
}
|
||||
|
||||
// GetTimes returns the Times field value
|
||||
func (o *Activity) GetTimes() ActivityTimes {
|
||||
if o == nil {
|
||||
var ret ActivityTimes
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Times
|
||||
}
|
||||
|
||||
// GetTimesOk returns a tuple with the Times field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *Activity) GetTimesOk() (*ActivityTimes, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Times, true
|
||||
}
|
||||
|
||||
// SetTimes sets field value
|
||||
func (o *Activity) SetTimes(v ActivityTimes) {
|
||||
o.Times = v
|
||||
}
|
||||
|
||||
// GetTemplate returns the Template field value
|
||||
func (o *Activity) GetTemplate() ActivityTemplate {
|
||||
if o == nil {
|
||||
var ret ActivityTemplate
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Template
|
||||
}
|
||||
|
||||
// GetTemplateOk returns a tuple with the Template field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *Activity) GetTemplateOk() (*ActivityTemplate, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Template, true
|
||||
}
|
||||
|
||||
// SetTemplate sets field value
|
||||
func (o *Activity) SetTemplate(v ActivityTemplate) {
|
||||
o.Template = v
|
||||
}
|
||||
|
||||
func (o Activity) MarshalJSON() ([]byte, error) {
|
||||
toSerialize, err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o Activity) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["id"] = o.Id
|
||||
toSerialize["times"] = o.Times
|
||||
toSerialize["template"] = o.Template
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *Activity) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"id",
|
||||
"times",
|
||||
"template",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, requiredProperty := range requiredProperties {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varActivity := _Activity{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varActivity)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = Activity(varActivity)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableActivity struct {
|
||||
value *Activity
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableActivity) Get() *Activity {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableActivity) Set(val *Activity) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableActivity) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableActivity) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableActivity(val *Activity) *NullableActivity {
|
||||
return &NullableActivity{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableActivity) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableActivity) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
194
vendor/github.com/owncloud/libre-graph-api-go/model_activity_template.go
generated
vendored
Normal file
194
vendor/github.com/owncloud/libre-graph-api-go/model_activity_template.go
generated
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
Libre Graph API
|
||||
|
||||
Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
|
||||
|
||||
API version: v1.0.4
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package libregraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the ActivityTemplate type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &ActivityTemplate{}
|
||||
|
||||
// ActivityTemplate struct for ActivityTemplate
|
||||
type ActivityTemplate struct {
|
||||
// Activity description.
|
||||
Message string `json:"message"`
|
||||
// Activity description variables.
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
type _ActivityTemplate ActivityTemplate
|
||||
|
||||
// NewActivityTemplate instantiates a new ActivityTemplate object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewActivityTemplate(message string) *ActivityTemplate {
|
||||
this := ActivityTemplate{}
|
||||
this.Message = message
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewActivityTemplateWithDefaults instantiates a new ActivityTemplate object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewActivityTemplateWithDefaults() *ActivityTemplate {
|
||||
this := ActivityTemplate{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetMessage returns the Message field value
|
||||
func (o *ActivityTemplate) GetMessage() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Message
|
||||
}
|
||||
|
||||
// GetMessageOk returns a tuple with the Message field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *ActivityTemplate) GetMessageOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Message, true
|
||||
}
|
||||
|
||||
// SetMessage sets field value
|
||||
func (o *ActivityTemplate) SetMessage(v string) {
|
||||
o.Message = v
|
||||
}
|
||||
|
||||
// GetVariables returns the Variables field value if set, zero value otherwise.
|
||||
func (o *ActivityTemplate) GetVariables() map[string]interface{} {
|
||||
if o == nil || IsNil(o.Variables) {
|
||||
var ret map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
return o.Variables
|
||||
}
|
||||
|
||||
// GetVariablesOk returns a tuple with the Variables field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *ActivityTemplate) GetVariablesOk() (map[string]interface{}, bool) {
|
||||
if o == nil || IsNil(o.Variables) {
|
||||
return map[string]interface{}{}, false
|
||||
}
|
||||
return o.Variables, true
|
||||
}
|
||||
|
||||
// HasVariables returns a boolean if a field has been set.
|
||||
func (o *ActivityTemplate) HasVariables() bool {
|
||||
if o != nil && !IsNil(o.Variables) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetVariables gets a reference to the given map[string]interface{} and assigns it to the Variables field.
|
||||
func (o *ActivityTemplate) SetVariables(v map[string]interface{}) {
|
||||
o.Variables = v
|
||||
}
|
||||
|
||||
func (o ActivityTemplate) MarshalJSON() ([]byte, error) {
|
||||
toSerialize, err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o ActivityTemplate) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["message"] = o.Message
|
||||
if !IsNil(o.Variables) {
|
||||
toSerialize["variables"] = o.Variables
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *ActivityTemplate) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"message",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, requiredProperty := range requiredProperties {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varActivityTemplate := _ActivityTemplate{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varActivityTemplate)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = ActivityTemplate(varActivityTemplate)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableActivityTemplate struct {
|
||||
value *ActivityTemplate
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableActivityTemplate) Get() *ActivityTemplate {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableActivityTemplate) Set(val *ActivityTemplate) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableActivityTemplate) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableActivityTemplate) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableActivityTemplate(val *ActivityTemplate) *NullableActivityTemplate {
|
||||
return &NullableActivityTemplate{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableActivityTemplate) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableActivityTemplate) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
158
vendor/github.com/owncloud/libre-graph-api-go/model_activity_times.go
generated
vendored
Normal file
158
vendor/github.com/owncloud/libre-graph-api-go/model_activity_times.go
generated
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
Libre Graph API
|
||||
|
||||
Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
|
||||
|
||||
API version: v1.0.4
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package libregraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// checks if the ActivityTimes type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &ActivityTimes{}
|
||||
|
||||
// ActivityTimes struct for ActivityTimes
|
||||
type ActivityTimes struct {
|
||||
// Timestamp of the activity.
|
||||
RecordedTime time.Time `json:"recordedTime"`
|
||||
}
|
||||
|
||||
type _ActivityTimes ActivityTimes
|
||||
|
||||
// NewActivityTimes instantiates a new ActivityTimes object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewActivityTimes(recordedTime time.Time) *ActivityTimes {
|
||||
this := ActivityTimes{}
|
||||
this.RecordedTime = recordedTime
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewActivityTimesWithDefaults instantiates a new ActivityTimes object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewActivityTimesWithDefaults() *ActivityTimes {
|
||||
this := ActivityTimes{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetRecordedTime returns the RecordedTime field value
|
||||
func (o *ActivityTimes) GetRecordedTime() time.Time {
|
||||
if o == nil {
|
||||
var ret time.Time
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.RecordedTime
|
||||
}
|
||||
|
||||
// GetRecordedTimeOk returns a tuple with the RecordedTime field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *ActivityTimes) GetRecordedTimeOk() (*time.Time, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.RecordedTime, true
|
||||
}
|
||||
|
||||
// SetRecordedTime sets field value
|
||||
func (o *ActivityTimes) SetRecordedTime(v time.Time) {
|
||||
o.RecordedTime = v
|
||||
}
|
||||
|
||||
func (o ActivityTimes) MarshalJSON() ([]byte, error) {
|
||||
toSerialize, err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o ActivityTimes) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["recordedTime"] = o.RecordedTime
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *ActivityTimes) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"recordedTime",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, requiredProperty := range requiredProperties {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varActivityTimes := _ActivityTimes{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varActivityTimes)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = ActivityTimes(varActivityTimes)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableActivityTimes struct {
|
||||
value *ActivityTimes
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableActivityTimes) Get() *ActivityTimes {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableActivityTimes) Set(val *ActivityTimes) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableActivityTimes) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableActivityTimes) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableActivityTimes(val *ActivityTimes) *NullableActivityTimes {
|
||||
return &NullableActivityTimes{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableActivityTimes) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableActivityTimes) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
124
vendor/github.com/owncloud/libre-graph-api-go/model_collection_of_activities.go
generated
vendored
Normal file
124
vendor/github.com/owncloud/libre-graph-api-go/model_collection_of_activities.go
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
Libre Graph API
|
||||
|
||||
Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
|
||||
|
||||
API version: v1.0.4
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package libregraph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// checks if the CollectionOfActivities type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &CollectionOfActivities{}
|
||||
|
||||
// CollectionOfActivities struct for CollectionOfActivities
|
||||
type CollectionOfActivities struct {
|
||||
Value []Activity `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// NewCollectionOfActivities instantiates a new CollectionOfActivities object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewCollectionOfActivities() *CollectionOfActivities {
|
||||
this := CollectionOfActivities{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewCollectionOfActivitiesWithDefaults instantiates a new CollectionOfActivities object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewCollectionOfActivitiesWithDefaults() *CollectionOfActivities {
|
||||
this := CollectionOfActivities{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetValue returns the Value field value if set, zero value otherwise.
|
||||
func (o *CollectionOfActivities) GetValue() []Activity {
|
||||
if o == nil || IsNil(o.Value) {
|
||||
var ret []Activity
|
||||
return ret
|
||||
}
|
||||
return o.Value
|
||||
}
|
||||
|
||||
// GetValueOk returns a tuple with the Value field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *CollectionOfActivities) GetValueOk() ([]Activity, bool) {
|
||||
if o == nil || IsNil(o.Value) {
|
||||
return nil, false
|
||||
}
|
||||
return o.Value, true
|
||||
}
|
||||
|
||||
// HasValue returns a boolean if a field has been set.
|
||||
func (o *CollectionOfActivities) HasValue() bool {
|
||||
if o != nil && !IsNil(o.Value) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetValue gets a reference to the given []Activity and assigns it to the Value field.
|
||||
func (o *CollectionOfActivities) SetValue(v []Activity) {
|
||||
o.Value = v
|
||||
}
|
||||
|
||||
func (o CollectionOfActivities) MarshalJSON() ([]byte, error) {
|
||||
toSerialize, err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o CollectionOfActivities) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
if !IsNil(o.Value) {
|
||||
toSerialize["value"] = o.Value
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
type NullableCollectionOfActivities struct {
|
||||
value *CollectionOfActivities
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableCollectionOfActivities) Get() *CollectionOfActivities {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableCollectionOfActivities) Set(val *CollectionOfActivities) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableCollectionOfActivities) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableCollectionOfActivities) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableCollectionOfActivities(val *CollectionOfActivities) *NullableCollectionOfActivities {
|
||||
return &NullableCollectionOfActivities{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableCollectionOfActivities) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableCollectionOfActivities) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -1623,7 +1623,7 @@ github.com/opentracing/opentracing-go/log
|
||||
# github.com/orcaman/concurrent-map v1.0.0
|
||||
## explicit
|
||||
github.com/orcaman/concurrent-map
|
||||
# github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3
|
||||
# github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1
|
||||
## explicit; go 1.18
|
||||
github.com/owncloud/libre-graph-api-go
|
||||
# github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
||||
|
||||
Reference in New Issue
Block a user