From 02ed77972d732ca2b6b0d09d4136305d0f25acb8 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 26 Apr 2023 14:37:15 +0200 Subject: [PATCH 1/8] Add notifications service --- cmd/revad/runtime/loader.go | 2 + go.mod | 53 +-- go.sum | 125 +++--- internal/serverless/services/loader/loader.go | 1 + .../services/notifications/notifications.go | 403 ++++++++++++++++++ pkg/notification/db_changes.sql | 46 ++ pkg/notification/db_sqlite.sql | 51 +++ .../handler/emailhandler/emailhandler.go | 116 +++++ pkg/notification/handler/handler.go | 24 ++ pkg/notification/handler/loader/loader.go | 25 ++ pkg/notification/handler/registry/registry.go | 60 +++ pkg/notification/manager/loader/loader.go | 25 ++ pkg/notification/manager/registry/registry.go | 36 ++ pkg/notification/manager/sql/sql.go | 199 +++++++++ .../manager/sql/sql_suite_test.go | 31 ++ pkg/notification/manager/sql/sql_test.go | 252 +++++++++++ pkg/notification/manager/sql/test.sqlite | Bin 0 -> 32768 bytes pkg/notification/notification.go | 114 +++++ .../notificationhelper/notificationhelper.go | 250 +++++++++++ .../template/registry/registry.go | 69 +++ pkg/notification/template/template.go | 170 ++++++++ pkg/notification/trigger/trigger.go | 41 ++ pkg/notification/utils/nats.go | 63 +++ pkg/utils/accumulator/accumulator.go | 126 ++++++ 24 files changed, 2194 insertions(+), 88 deletions(-) create mode 100644 internal/serverless/services/notifications/notifications.go create mode 100644 pkg/notification/db_changes.sql create mode 100644 pkg/notification/db_sqlite.sql create mode 100644 pkg/notification/handler/emailhandler/emailhandler.go create mode 100644 pkg/notification/handler/handler.go create mode 100644 pkg/notification/handler/loader/loader.go create mode 100644 pkg/notification/handler/registry/registry.go create mode 100644 pkg/notification/manager/loader/loader.go create mode 100644 pkg/notification/manager/registry/registry.go create mode 100644 pkg/notification/manager/sql/sql.go create mode 100644 pkg/notification/manager/sql/sql_suite_test.go create mode 100644 pkg/notification/manager/sql/sql_test.go create mode 100644 pkg/notification/manager/sql/test.sqlite create mode 100644 pkg/notification/notification.go create mode 100644 pkg/notification/notificationhelper/notificationhelper.go create mode 100644 pkg/notification/template/registry/registry.go create mode 100644 pkg/notification/template/template.go create mode 100644 pkg/notification/trigger/trigger.go create mode 100644 pkg/notification/utils/nats.go create mode 100644 pkg/utils/accumulator/accumulator.go diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go index 52488936d3..ad174b9130 100644 --- a/cmd/revad/runtime/loader.go +++ b/cmd/revad/runtime/loader.go @@ -37,6 +37,8 @@ import ( _ "github.com/cs3org/reva/pkg/datatx/manager/loader" _ "github.com/cs3org/reva/pkg/group/manager/loader" _ "github.com/cs3org/reva/pkg/metrics/driver/loader" + _ "github.com/cs3org/reva/pkg/notification/handler/loader" + _ "github.com/cs3org/reva/pkg/notification/manager/loader" _ "github.com/cs3org/reva/pkg/ocm/invite/repository/loader" _ "github.com/cs3org/reva/pkg/ocm/provider/authorizer/loader" _ "github.com/cs3org/reva/pkg/ocm/share/repository/loader" diff --git a/go.mod b/go.mod index 203457cdb5..f5515bb3af 100644 --- a/go.mod +++ b/go.mod @@ -24,14 +24,14 @@ require ( github.com/go-chi/chi/v5 v5.0.8 github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-playground/validator/v10 v10.11.2 - github.com/go-sql-driver/mysql v1.6.0 + github.com/go-sql-driver/mysql v1.7.0 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/golang/protobuf v1.5.2 + github.com/golang/protobuf v1.5.3 github.com/gomodule/redigo v1.8.9 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 - github.com/hashicorp/go-hclog v1.4.0 + github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-plugin v1.4.9 github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/juliangruber/go-intersect v1.1.0 @@ -40,10 +40,11 @@ require ( github.com/mileusna/useragent v1.2.1 github.com/minio/minio-go/v7 v7.0.45 github.com/mitchellh/mapstructure v1.5.0 - github.com/nats-io/nats-server/v2 v2.9.11 - github.com/nats-io/nats-streaming-server v0.25.2 + github.com/nats-io/nats-server/v2 v2.9.16 + github.com/nats-io/nats-streaming-server v0.25.4 + github.com/nats-io/nats.go v1.25.0 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.27.2 + github.com/onsi/gomega v1.27.6 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.5 github.com/prometheus/alertmanager v0.24.0 @@ -51,7 +52,7 @@ require ( github.com/rs/zerolog v1.28.0 github.com/sciencemesh/meshdirectory-web v1.0.4 github.com/sethvargo/go-password v0.2.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 github.com/thanhpk/randstr v1.0.4 github.com/tus/tusd v1.10.0 @@ -64,25 +65,28 @@ require ( go.opentelemetry.io/otel/sdk v1.11.2 go.opentelemetry.io/otel/trace v1.11.2 go.step.sm/crypto v0.23.2 - golang.org/x/crypto v0.5.0 + golang.org/x/crypto v0.8.0 golang.org/x/oauth2 v0.3.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.5.0 - golang.org/x/term v0.5.0 - golang.org/x/text v0.7.0 + golang.org/x/sys v0.7.0 + golang.org/x/term v0.7.0 + golang.org/x/text v0.9.0 google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef google.golang.org/grpc v1.52.0 google.golang.org/protobuf v1.28.1 gotest.tools v2.2.0+incompatible ) -require github.com/go-jose/go-jose/v3 v3.0.0 // indirect +require ( + github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/hashicorp/go-msgpack/v2 v2.1.0 // indirect +) require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 // indirect + github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect @@ -113,13 +117,13 @@ require ( github.com/hashicorp/go-immutable-radix v1.0.0 // indirect github.com/hashicorp/go-msgpack v1.1.5 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hashicorp/raft v1.3.11 // indirect + github.com/hashicorp/raft v1.4.0 // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/compress v1.16.4 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lestrrat-go/strftime v1.0.4 // indirect @@ -138,11 +142,10 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nats-io/jwt/v2 v2.3.0 // indirect - github.com/nats-io/nats.go v1.19.0 // indirect - github.com/nats-io/nkeys v0.3.0 // indirect + github.com/nats-io/jwt/v2 v2.4.1 // indirect + github.com/nats-io/nkeys v0.4.4 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/nats-io/stan.go v0.10.3 // indirect + github.com/nats-io/stan.go v0.10.4 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect @@ -154,7 +157,7 @@ require ( github.com/prometheus/client_golang v1.13.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/rs/xid v1.4.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect @@ -163,13 +166,13 @@ require ( github.com/sirupsen/logrus v1.9.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect - go.etcd.io/bbolt v1.3.6 // indirect + go.etcd.io/bbolt v1.3.7 // indirect go.mongodb.org/mongo-driver v1.8.3 // indirect go.opentelemetry.io/otel/metric v0.34.0 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect diff --git a/go.sum b/go.sum index a18fef0bf3..1ff0369c1c 100644 --- a/go.sum +++ b/go.sum @@ -156,7 +156,7 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -201,8 +201,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= -github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= @@ -376,8 +376,6 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/glpatcern/go-mime v0.0.0-20221026162842-2a8d71ad17a9 h1:3um08ooi0/lyRmK2eE1XTKmRQHDzPu0IvpCPMljyMZ8= github.com/glpatcern/go-mime v0.0.0-20221026162842-2a8d71ad17a9/go.mod h1:EJaddanP+JfU3UkVvn0rYYF3b/gD7eZRejbTHqiQExA= -github.com/gmgigi96/go-cs3apis v0.0.0-20230508122407-26b2c32caabc h1:/KUUgL9AkNP8TEOtALru0VzDlZh7IH6DBAwQrl5xw8Y= -github.com/gmgigi96/go-cs3apis v0.0.0-20230508122407-26b2c32caabc/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/go-acme/lego/v4 v4.4.0/go.mod h1:l3+tFUFZb590dWcqhWZegynUthtaHJbG2fevUpoOOE0= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -449,13 +447,14 @@ github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVL github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= @@ -540,8 +539,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -639,15 +639,16 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= -github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= +github.com/hashicorp/go-msgpack/v2 v2.1.0 h1:J2g2hMyjSefUPTnkLRU2MnsLLsPRB1n4Z/wJRN07GuA= +github.com/hashicorp/go-msgpack/v2 v2.1.0/go.mod h1:Tv81cKI2JmHZDjmzEmc1n+8h1DO5k+3pG6BPlNMQds0= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU= github.com/hashicorp/go-plugin v1.4.9/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= @@ -672,8 +673,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/raft v1.3.11 h1:p3v6gf6l3S797NnK5av3HcczOC1T5CLoaRvg0g9ys4A= -github.com/hashicorp/raft v1.3.11/go.mod h1:J8naEwc6XaaCfts7+28whSeRvCqTd6e20BlCU3LtEO4= +github.com/hashicorp/raft v1.4.0 h1:tn28S/AWv0BtRQgwZv/1NELu8sCvI0FixqL8C8MYKeY= +github.com/hashicorp/raft v1.4.0/go.mod h1:nz64BIjXphDLATfKGG5RzHtNUPioLeKFsXEm88yTVew= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -711,6 +712,7 @@ github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -732,8 +734,8 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= -github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU= +github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= @@ -766,8 +768,7 @@ github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9B github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.8 h1:3fdt97i/cwSU83+E0hZTC/Xpc9mTZxc6UWSCRcSbxiE= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linode/linodego v0.25.3/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE= @@ -876,27 +877,26 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= -github.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= +github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4= +github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats-server/v2 v2.9.3/go.mod h1:4sq8wvrpbvSzL1n3ZfEYnH4qeUuIl5W990j3kw13rRk= -github.com/nats-io/nats-server/v2 v2.9.11 h1:4y5SwWvWI59V5mcqtuoqKq6L9NDUydOP3Ekwuwl8cZI= -github.com/nats-io/nats-server/v2 v2.9.11/go.mod h1:b0oVuxSlkvS3ZjMkncFeACGyZohbO4XhSqW1Lt7iRRY= -github.com/nats-io/nats-streaming-server v0.25.2 h1:cWjytvYksYPgnXnSocqnRWVrSgLclusnPGBNHQR4SqI= -github.com/nats-io/nats-streaming-server v0.25.2/go.mod h1:bRbgx+iCG6EZEXpqVMroRDuCGwR1iW+ta84aEGBaMhI= +github.com/nats-io/nats-server/v2 v2.9.16 h1:SuNe6AyCcVy0g5326wtyU8TdqYmcPqzTjhkHojAjprc= +github.com/nats-io/nats-server/v2 v2.9.16/go.mod h1:z1cc5Q+kqJkz9mLUdlcSsdYnId4pyImHjNgoh6zxSC0= +github.com/nats-io/nats-streaming-server v0.25.4 h1:aaMmKcEMXLvviM9y73BmPquTgQ/fg+0EmFLzFSbXUqQ= +github.com/nats-io/nats-streaming-server v0.25.4/go.mod h1:zrOyvkrVEz/y72m1nAVb149k/CAumm3H9ze682Lg9uE= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nats.go v1.16.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nats.go v1.17.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nats.go v1.19.0 h1:H6j8aBnTQFoVrTGB6Xjd903UMdE7jz6DS4YkmAqgZ9Q= -github.com/nats-io/nats.go v1.19.0/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= +github.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= +github.com/nats-io/nats.go v1.25.0 h1:t5/wCPGciR7X3Mu8QOi4jiJaXaWM8qtkLu4lzGZvYHE= +github.com/nats-io/nats.go v1.25.0/go.mod h1:D2WALIhz7V8M0pH8Scx8JZXlg6Oqz5VG+nQkK8nJdvg= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA= +github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nats-io/stan.go v0.10.3 h1:8DOyQJ0+nza3zSVJZ19/cpikkrWA4rSKB3YvckIGOTI= -github.com/nats-io/stan.go v0.10.3/go.mod h1:Cgf5zk6kKpOCqqUIJeuBz6ZDz9osT791VhS6m28sSQQ= +github.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw= +github.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI= @@ -923,12 +923,12 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.8.4 h1:gf5mIQ8cLFieruNLAdgijHF1PYfLphKm2dxxcUtcqK0= +github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY= -github.com/onsi/gomega v1.27.2/go.mod h1:5mR3phAHpkAVIDkHEUBY6HGVsU+cpcEscrGPB4oPlZI= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -976,16 +976,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/alertmanager v0.24.0 h1:HBWR3lk4uy3ys+naDZthDdV7yEsxpaNeZuUS+hJgrOw= github.com/prometheus/alertmanager v0.24.0/go.mod h1:r6fy/D7FRuZh5YbnX6J3MBY0eI4Pb5yPYS7/bPSXXqI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= @@ -1000,12 +999,12 @@ github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= @@ -1017,7 +1016,6 @@ github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57J github.com/prometheus/exporter-toolkit v0.7.1/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -1026,8 +1024,9 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= @@ -1119,8 +1118,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 h1:VsBj3UD2xyAOu7kJw6O/2jjG2UXLFoBzihqDU9Ofg9M= github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= @@ -1176,8 +1176,8 @@ go-micro.dev/v4 v4.3.1-0.20211108085239-0c2041e43908 h1:4ori3xawGl2unFIOQPEgUuHd go-micro.dev/v4 v4.3.1-0.20211108085239-0c2041e43908/go.mod h1:tw47Xfg2YywfPUnglZgXQsSf7p0ST6mQL3v0JooGmSY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.m3o.com v0.1.0/go.mod h1:p8FdLqZH3R9a0y04qiMNT+clw69d3SxyQPFzCNbDRtk= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= @@ -1214,7 +1214,6 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1252,11 +1251,9 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1299,8 +1296,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1368,8 +1365,9 @@ golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1478,7 +1476,6 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1531,20 +1528,21 @@ golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1556,8 +1554,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1565,8 +1564,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1643,8 +1642,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/serverless/services/loader/loader.go b/internal/serverless/services/loader/loader.go index 1b466a144c..71ab9afb1d 100644 --- a/internal/serverless/services/loader/loader.go +++ b/internal/serverless/services/loader/loader.go @@ -21,5 +21,6 @@ package loader import ( // Load core serverless services. _ "github.com/cs3org/reva/internal/serverless/services/helloworld" + _ "github.com/cs3org/reva/internal/serverless/services/notifications" // Add your own service here. ) diff --git a/internal/serverless/services/notifications/notifications.go b/internal/serverless/services/notifications/notifications.go new file mode 100644 index 0000000000..b51b163fd4 --- /dev/null +++ b/internal/serverless/services/notifications/notifications.go @@ -0,0 +1,403 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package notifications + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/notification" + "github.com/cs3org/reva/pkg/notification/handler" + handlerRegistry "github.com/cs3org/reva/pkg/notification/handler/registry" + notificationManagerRegistry "github.com/cs3org/reva/pkg/notification/manager/registry" + "github.com/cs3org/reva/pkg/notification/template" + templateRegistry "github.com/cs3org/reva/pkg/notification/template/registry" + "github.com/cs3org/reva/pkg/notification/trigger" + "github.com/cs3org/reva/pkg/notification/utils" + "github.com/cs3org/reva/pkg/rserverless" + "github.com/cs3org/reva/pkg/utils/accumulator" + "github.com/mitchellh/mapstructure" + "github.com/nats-io/nats.go" + "github.com/rs/zerolog" +) + +type config struct { + NatsAddress string `mapstructure:"nats_address" docs:";The NATS server address."` + NatsToken string `mapstructure:"nats_token" docs:"The token to authenticate against the NATS server"` + NatsPrefix string `mapstructure:"nats_prefix" docs:"reva-notifications;The notifications NATS stream."` + HandlerConf map[string]interface{} `mapstructure:"handlers" docs:";Settings for the different notification handlers."` + GroupingInterval int `mapstructure:"grouping_interval" docs:"60;Time in seconds to group incoming notification triggers"` + GroupingMaxSize int `mapstructure:"grouping_max_size" docs:"100;Maximum number of notifications to group"` + StorageDriver string `mapstructure:"storage_driver" docs:"mysql;The driver used to store notifications"` + StorageDrivers map[string]map[string]interface{} `mapstructure:"storage_drivers"` +} + +func defaultConfig() *config { + return &config{ + NatsPrefix: "reva-notifications", + GroupingInterval: 60, + GroupingMaxSize: 100, + StorageDriver: "sql", + } +} + +type svc struct { + nc *nats.Conn + js nats.JetStreamContext + kv nats.KeyValue + conf *config + log *zerolog.Logger + handlers map[string]handler.Handler + templates templateRegistry.Registry + nm notification.Manager + accumulators map[string]*accumulator.Accumulator[trigger.Trigger] +} + +func init() { + rserverless.Register("notifications", New) +} + +func getNotificationManager(c *config, l *zerolog.Logger) (notification.Manager, error) { + if f, ok := notificationManagerRegistry.NewFuncs[c.StorageDriver]; ok { + return f(c.StorageDrivers[c.StorageDriver]) + } + return nil, errtypes.NotFound(fmt.Sprintf("storage driver %s not found", c.StorageDriver)) +} + +// New returns a new Notifications service. +func New(m map[string]interface{}, log *zerolog.Logger) (rserverless.Service, error) { + conf := defaultConfig() + + if err := mapstructure.Decode(m, conf); err != nil { + return nil, err + } + + nm, err := getNotificationManager(conf, log) + if err != nil { + return nil, err + } + log.Info().Msgf("notification storage %s initialized", conf.StorageDriver) + + s := &svc{ + conf: conf, + log: log, + nm: nm, + } + + return s, nil +} + +// Start starts the Notifications service. +func (s *svc) Start() { + s.templates = *templateRegistry.New() + s.handlers = handlerRegistry.InitHandlers(s.conf.HandlerConf, s.log) + s.accumulators = make(map[string]*accumulator.Accumulator[trigger.Trigger]) + + s.log.Debug().Msgf("connecting to nats server at %s", s.conf.NatsAddress) + err := s.connect() + if err != nil { + s.log.Error().Err(err).Msg("connecting to nats failed") + } + s.log.Info().Msg("notifications service ready") +} + +// Close performs cleanup. +func (s *svc) Close(ctx context.Context) error { + return s.nc.Drain() +} + +func (s *svc) connect() error { + nc, err := utils.ConnectToNats(s.conf.NatsAddress, s.conf.NatsToken, *s.log) + if err != nil { + return err + } + s.nc = nc + + js, err := nc.JetStream(nats.PublishAsyncMaxPending(256)) + if err != nil { + return errors.Wrap(err, "jetstream initialization failed") + } + + s.js = js + + if err := s.initNatsKV("template", s.handleMsgTemplate); err != nil { + return err + } + if err := s.initNatsStream("notification-register", s.handleMsgRegisterNotification); err != nil { + return err + } + if err := s.initNatsStream("notification-unregister", s.handleMsgUnregisterNotification); err != nil { + return err + } + return s.initNatsStream("trigger", s.handleMsgTrigger) +} + +func (s *svc) initNatsKV(name string, handler func(msg []byte)) error { + bucketName := fmt.Sprintf("%s-%s", s.conf.NatsPrefix, name) + kv, err := s.js.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: bucketName, + }) + if err != nil { + return errors.Wrap(err, "template store creation failed, probably because nats server is unreachable") + } + + s.kv = kv + + w, _ := kv.WatchAll() + + go func() { + for { + msg := <-w.Updates() + + if msg != nil { + handler(msg.Value()) + } + } + }() + + return nil +} + +func (s *svc) initNatsStream(name string, handler func(msg *nats.Msg)) error { + streamName := fmt.Sprintf("%s-%s", s.conf.NatsPrefix, name) + consumerName := fmt.Sprintf("%s-consumer-%s", s.conf.NatsPrefix, name) + subjectName := fmt.Sprintf("%s.%s", s.conf.NatsPrefix, name) + deliverySubjectName := fmt.Sprintf("%s-delivery.%s", s.conf.NatsPrefix, name) + + // Creates a NATS stream with given name if it does not exist already + if _, err := s.js.AddStream(&nats.StreamConfig{ + Name: streamName, + Subjects: []string{subjectName}, + }); err != nil { + return errors.Wrapf(err, "nats %s stream creation failed", name) + } + + // Adds a consumer with the given name to the JetStream context + if _, err := s.js.AddConsumer(streamName, &nats.ConsumerConfig{ + Durable: consumerName, + DeliverSubject: deliverySubjectName, + }); err != nil { + return errors.Wrapf(err, "nats %s consumer creation failed", name) + } + + // Subscribes the JetStream context to the consumer we just created + _, err := s.js.Subscribe("", func(msg *nats.Msg) { handler(msg) }, nats.Bind(streamName, consumerName)) + if err != nil { + return errors.Wrapf(err, "nats subscription to consumer %s failed", consumerName) + } + + return nil +} + +func (s *svc) handleMsgTemplate(msg []byte) { + if len(msg) == 0 { + return + } + + name, err := s.templates.Put(msg, s.handlers) + if err != nil { + s.log.Error().Err(err).Msgf("template registration failed %v", err) + + // If a template file was not found, delete that template from the registry altogether, + // this way we ensure templates that are deleted from the config are deleted from the + // store too. + wrappedErr := errors.Unwrap(errors.Unwrap(err)) + _, isFileNotFoundError := wrappedErr.(*template.FileNotFoundError) + if isFileNotFoundError && name != "" { + err := s.kv.Purge(name) + if err != nil { + s.log.Error().Err(err).Msgf("deletion of template %s from store failed", name) + } + s.log.Info().Msgf("template %s unregistered", name) + } + } else { + s.log.Info().Msgf("template %s registered", name) + } +} + +func (s *svc) handleMsgRegisterNotification(msg *nats.Msg) { + var data map[string]interface{} + err := json.Unmarshal(msg.Data, &data) + if err != nil { + s.log.Error().Err(err).Msg("notification registration unmarshall failed") + return + } + + n := ¬ification.Notification{} + if err := mapstructure.Decode(data, n); err != nil { + s.log.Error().Err(err).Msg("notification registration decoding failed") + return + } + + templ, err := s.templates.Get(n.TemplateName) + if err != nil { + s.log.Error().Err(err).Msg("notification template get failed") + return + } + + n.Template = *templ + err = s.nm.UpsertNotification(*n) + if err != nil { + s.log.Error().Err(err).Msgf("registering notification %s failed", n.Ref) + } else { + s.log.Info().Msgf("notification %s registered", n.Ref) + } +} + +func (s *svc) handleMsgUnregisterNotification(msg *nats.Msg) { + ref := string(msg.Data) + + err := s.nm.DeleteNotification(ref) + if err != nil { + _, isNotFoundError := err.(*notification.NotFoundError) + if isNotFoundError { + s.log.Debug().Msgf("a notification with ref %s does not exist", ref) + } else { + s.log.Error().Err(err).Msgf("notification unregister failed") + } + } else { + s.log.Debug().Msgf("notification %s unregistered", ref) + } +} + +func (s *svc) getAccumulatorForTrigger(tr trigger.Trigger) *accumulator.Accumulator[trigger.Trigger] { + a, ok := s.accumulators[tr.Ref] + + if !ok || a == nil { + timeout := time.Duration(s.conf.GroupingInterval) * time.Second + maxSize := s.conf.GroupingMaxSize + + a = accumulator.New[trigger.Trigger](timeout, maxSize, s.log) + _ = a.Start(s.notificationSendCallback) + s.accumulators[tr.Ref] = a + + s.log.Debug().Msgf("created new accumulator for trigger %s", tr.Ref) + } + + return a +} + +func (s *svc) handleMsgTrigger(msg *nats.Msg) { + var data map[string]interface{} + err := json.Unmarshal(msg.Data, &data) + if err != nil { + s.log.Error().Err(err).Msg("notification trigger unmarshall failed") + return + } + + tr := &trigger.Trigger{} + if err := mapstructure.Decode(data, tr); err != nil { + s.log.Error().Err(err).Msg("trigger creation failed") + return + } + + s.log.Info().Msgf("notification trigger %s received", tr.Ref) + + notif := tr.Notification + if notif == nil { + notif, err = s.nm.GetNotification(tr.Ref) + if err != nil { + _, isNotFoundError := err.(*notification.NotFoundError) + if isNotFoundError { + s.log.Debug().Msgf("trigger %s does not have a notification attached", tr.Ref) + return + } + s.log.Error().Err(err).Msgf("notification retrieval from store failed") + return + } + } + + templ, err := s.templates.Get(notif.TemplateName) + if err != nil { + s.log.Error().Err(err).Msgf("template %s for trigger %s not found", notif.TemplateName, tr.Ref) + return + } + + notif.Template = *templ + tr.Notification = notif + a := s.getAccumulatorForTrigger(*tr) + a.Input <- *tr +} + +func (s *svc) notificationSendCallback(ts []trigger.Trigger) { + const itemCount = 10 + var tr trigger.Trigger + + if len(ts) == 1 { + tr = ts[0] + s.log.Info().Msgf("sending single notification for trigger %s", tr.Ref) + } else { + moreCount := len(ts) - itemCount + if moreCount < 0 { + moreCount = 0 + } + + // create a new trigger + tr = trigger.Trigger{ + Ref: ts[0].Ref, + Sender: ts[0].Sender, + TemplateData: map[string]interface{}{ + "_count": len(ts), + "_items": []map[string]interface{}{}, + "_moreCount": moreCount, + }, + } + + // add template data of the first ten elements, ignore the rest + l := itemCount + templateData := []map[string]interface{}{} + if l > len(ts) { + l = len(ts) + } + for _, t := range ts[:l] { + templateData = append(templateData, t.TemplateData) + } + tr.TemplateData["_items"] = templateData + + // initialize the new trigger + notif, err := s.nm.GetNotification(tr.Ref) + if err != nil { + s.log.Error().Msgf("notification retrieval from store failed") + return + } + + templ, err := s.templates.Get(notif.TemplateName) + if err != nil { + s.log.Error().Err(err).Msgf("template %s for trigger %s not found", notif.TemplateName, tr.Ref) + return + } + + notif.Template = *templ + tr.Notification = notif + + s.log.Info().Msgf("sending multi notification for %d triggers %s", tr.TemplateData["_count"], tr.Ref) + } + + // destroy old accumulator + s.accumulators[tr.Ref] = nil + + if err := tr.Send(); err != nil { + s.log.Error().Err(err).Msgf("notification send failed") + } +} diff --git a/pkg/notification/db_changes.sql b/pkg/notification/db_changes.sql new file mode 100644 index 0000000000..5a4b92666b --- /dev/null +++ b/pkg/notification/db_changes.sql @@ -0,0 +1,46 @@ +-- Copyright 2018-2023 CERN +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- In applying this license, CERN does not waive the privileges and immunities +-- granted to it by virtue of its status as an Intergovernmental Organization +-- or submit itself to any jurisdiction. + +-- This file can be used to make the required changes to the MySQL DB. This is +-- not a proper migration but it should work on most situations. + +USE cernboxngcopy; + +CREATE TABLE `cbox_notifications` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `ref` VARCHAR(3072) UNIQUE NOT NULL, + `template_name` VARCHAR(320) NOT NULL +); + +COMMIT; + +CREATE TABLE `cbox_notification_recipients` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `notification_id` INT NOT NULL, + `recipient` VARCHAR(320) NOT NULL, + FOREIGN KEY (notification_id) + REFERENCES cbox_notifications (id) + ON DELETE CASCADE +); + +COMMIT; + +CREATE INDEX `cbox_notifications_ix0` ON `cbox_notifications` (`ref`); + +CREATE INDEX `cbox_notification_recipients_ix0` ON `cbox_notification_recipients` (`notification_id`); +CREATE INDEX `cbox_notification_recipients_ix1` ON `cbox_notification_recipients` (`user_name`); diff --git a/pkg/notification/db_sqlite.sql b/pkg/notification/db_sqlite.sql new file mode 100644 index 0000000000..8e110fe153 --- /dev/null +++ b/pkg/notification/db_sqlite.sql @@ -0,0 +1,51 @@ +-- Copyright 2018-2023 CERN +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- In applying this license, CERN does not waive the privileges and immunities +-- granted to it by virtue of its status as an Intergovernmental Organization +-- or submit itself to any jurisdiction. + +-- This file can be used to quickstart a SQLite DB for running the tests in +-- ./manager/sql/sql_test.go + +CREATE TABLE `cbox_notifications` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `ref` VARCHAR(3072) UNIQUE NOT NULL, + `template_name` VARCHAR(320) NOT NULL +); + +COMMIT; + +CREATE TABLE `cbox_notification_recipients` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `notification_id` INTEGER NOT NULL, + `recipient` VARCHAR(320) NOT NULL, + FOREIGN KEY (notification_id) + REFERENCES cbox_notifications (id) + ON DELETE CASCADE +); + +COMMIT; + +CREATE INDEX `cbox_notifications_ix0` ON `cbox_notifications` (`ref`); + +CREATE INDEX `cbox_notification_recipients_ix0` ON `cbox_notification_recipients` (`notification_id`); +CREATE INDEX `cbox_notification_recipients_ix1` ON `cbox_notification_recipients` (`recipient`); + +COMMIT; + +INSERT INTO `cbox_notifications` (`id`, `ref`, `template_name`) VALUES (1, "notification-test", "notification-template-test"); +INSERT INTO `cbox_notification_recipients` (`id`, `notification_id`, `recipient`) VALUES (1, 1, "jdoe"), (2, 1, "testuser"); + +COMMIT; diff --git a/pkg/notification/handler/emailhandler/emailhandler.go b/pkg/notification/handler/emailhandler/emailhandler.go new file mode 100644 index 0000000000..47152f51c8 --- /dev/null +++ b/pkg/notification/handler/emailhandler/emailhandler.go @@ -0,0 +1,116 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package emailhandler + +import ( + "fmt" + "net/smtp" + "regexp" + "strings" + + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/cs3org/reva/pkg/notification/handler/registry" + "github.com/mitchellh/mapstructure" + "github.com/rs/zerolog" +) + +func init() { + registry.Register("email", New) +} + +// EmailHandler is the notification handler for emails. +type EmailHandler struct { + conf *config + Log *zerolog.Logger +} + +type config struct { + SMTPAddress string `mapstructure:"smtp_server" docs:";The hostname and port of the SMTP server."` + SenderLogin string `mapstructure:"sender_login" docs:";The email to be used to send mails."` + SenderPassword string `mapstructure:"sender_password" docs:";The sender's password."` + DisableAuth bool `mapstructure:"disable_auth" docs:"false;Whether to disable SMTP auth."` + DefaultSender string `mapstructure:"default_sender" docs:"no-reply@cernbox.cern.ch;Default sender when not specified in the trigger."` +} + +func defaultConfig() *config { + return &config{ + DefaultSender: "no-reply@cernbox.cern.ch", + } +} + +// New returns a new email handler. +func New(log *zerolog.Logger, conf interface{}) (handler.Handler, error) { + c := defaultConfig() + if err := mapstructure.Decode(conf, c); err != nil { + return nil, err + } + + return &EmailHandler{ + conf: c, + Log: log, + }, nil +} + +// Send is the method run when a notification is triggered for this handler. +func (e *EmailHandler) Send(sender, recipient, subject, body string) error { + if sender == "" { + sender = e.conf.DefaultSender + } + + msg := e.generateMsg(sender, recipient, subject, body) + err := smtp.SendMail(e.conf.SMTPAddress, e.getAuth(), sender, []string{recipient}, msg) + if err != nil { + return err + } + + e.Log.Debug().Msgf("mail sent to recipient %s", recipient) + + return nil +} + +func (e *EmailHandler) getAuth() smtp.Auth { + if e.conf.DisableAuth { + return nil + } + + return smtp.PlainAuth("", e.conf.SenderLogin, e.conf.SenderPassword, strings.SplitN(e.conf.SMTPAddress, ":", 2)[0]) +} + +func (e *EmailHandler) generateMsg(from, to, subject, body string) []byte { + re := regexp.MustCompile(`\r?\n`) + cleanSubject := re.ReplaceAllString(strings.TrimSpace(subject), " ") + headers := []string{ + fmt.Sprintf("From: %s", from), + fmt.Sprintf("To: %s", to), + fmt.Sprintf("Subject: %s", cleanSubject), + "MIME-version: 1.0;", + "Content-Type: text/html; charset=\"UTF-8\";", + } + + var sb strings.Builder + + for _, h := range headers { + sb.WriteString(h) + sb.WriteString("\r\n") + } + sb.WriteString("\r\n") + sb.WriteString(body) + + return []byte(sb.String()) +} diff --git a/pkg/notification/handler/handler.go b/pkg/notification/handler/handler.go new file mode 100644 index 0000000000..a42e5cd047 --- /dev/null +++ b/pkg/notification/handler/handler.go @@ -0,0 +1,24 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package handler + +// Handler is the interface notification handlers have to implement. +type Handler interface { + Send(sender, recipient, subject, body string) error +} diff --git a/pkg/notification/handler/loader/loader.go b/pkg/notification/handler/loader/loader.go new file mode 100644 index 0000000000..3aa73e6077 --- /dev/null +++ b/pkg/notification/handler/loader/loader.go @@ -0,0 +1,25 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package loader + +import ( + // Load notification handlers. + _ "github.com/cs3org/reva/pkg/notification/handler/emailhandler" + // Add your own here. +) diff --git a/pkg/notification/handler/registry/registry.go b/pkg/notification/handler/registry/registry.go new file mode 100644 index 0000000000..e4a9271eab --- /dev/null +++ b/pkg/notification/handler/registry/registry.go @@ -0,0 +1,60 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package registry + +import ( + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/rs/zerolog" +) + +// NewHandlerFunc is the function that notification handlers should register to +// at init time. +type NewHandlerFunc func(Log *zerolog.Logger, conf interface{}) (handler.Handler, error) + +// NewHandlerFuncs is a map containing all the registered notification handlers. +var NewHandlerFuncs = map[string]NewHandlerFunc{} + +// Register registers a new notification handler new function. Not safe for +// concurrent use. Safe for use from package init. +func Register(name string, f NewHandlerFunc) { + NewHandlerFuncs[name] = f +} + +// InitHandlers initializes the notification handlers with the configuration +// and the log from a service. +func InitHandlers(handlerConf map[string]interface{}, log *zerolog.Logger) map[string]handler.Handler { + handlers := make(map[string]handler.Handler) + hCount := 0 + + for n, f := range NewHandlerFuncs { + if c, ok := handlerConf[n]; ok { + nh, err := f(log, c) + if err != nil { + log.Err(err).Msgf("error initializing notification handler %s", n) + } + handlers[n] = nh + hCount++ + } else { + log.Warn().Msgf("missing config for notification handler %s", n) + } + } + log.Info().Msgf("%d handlers initialized", hCount) + + return handlers +} diff --git a/pkg/notification/manager/loader/loader.go b/pkg/notification/manager/loader/loader.go new file mode 100644 index 0000000000..af580197b0 --- /dev/null +++ b/pkg/notification/manager/loader/loader.go @@ -0,0 +1,25 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package loader + +import ( + // Load core notification manager drivers. + _ "github.com/cs3org/reva/pkg/notification/manager/sql" + // Add your own here. +) diff --git a/pkg/notification/manager/registry/registry.go b/pkg/notification/manager/registry/registry.go new file mode 100644 index 0000000000..4ae551145a --- /dev/null +++ b/pkg/notification/manager/registry/registry.go @@ -0,0 +1,36 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package registry + +import "github.com/cs3org/reva/pkg/notification" + +// import "github.com/cs3org/reva/pkg/share" + +// NewFunc is the function that notification managers +// should register at init time. +type NewFunc func(map[string]interface{}) (notification.Manager, error) + +// NewFuncs is a map containing all the registered notification managers. +var NewFuncs = map[string]NewFunc{} + +// Register registers a new notification manager new function. +// Not safe for concurrent use. Safe for use from package init. +func Register(name string, f NewFunc) { + NewFuncs[name] = f +} diff --git a/pkg/notification/manager/sql/sql.go b/pkg/notification/manager/sql/sql.go new file mode 100644 index 0000000000..1e71dad7ab --- /dev/null +++ b/pkg/notification/manager/sql/sql.go @@ -0,0 +1,199 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sql + +import ( + "database/sql" + "fmt" + + "github.com/cs3org/reva/pkg/notification" + "github.com/cs3org/reva/pkg/notification/manager/registry" + "github.com/mitchellh/mapstructure" +) + +func init() { + registry.Register("sql", NewMysql) +} + +type config struct { + DBUsername string `mapstructure:"db_username"` + DBPassword string `mapstructure:"db_password"` + DBHost string `mapstructure:"db_host"` + DBPort int `mapstructure:"db_port"` + DBName string `mapstructure:"db_name"` + GatewaySvc string `mapstructure:"gatewaysvc"` +} + +type mgr struct { + driver string + db *sql.DB +} + +// NewMysql returns an instance of the sql notifications manager. +func NewMysql(m map[string]interface{}) (notification.Manager, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + return nil, err + } + + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DBUsername, c.DBPassword, c.DBHost, c.DBPort, c.DBName)) + if err != nil { + return nil, err + } + + return New("mysql", db) +} + +// New returns a new Notifications driver connecting to the given sql.DB. +func New(driver string, db *sql.DB) (notification.Manager, error) { + return &mgr{ + driver: driver, + db: db, + }, nil +} + +// UpsertNotification creates or updates a notification. +func (m *mgr) UpsertNotification(n notification.Notification) error { + if err := n.CheckNotification(); err != nil { + return err + } + + tx, err := m.db.Begin() + if err != nil { + return err + } + + // Create/update notification + stmt, err := m.db.Prepare("REPLACE INTO cbox_notifications (ref, template_name) VALUES (?, ?)") + if err != nil { + return err + } + defer stmt.Close() + + result, err := stmt.Exec(n.Ref, n.TemplateName) + if err != nil { + _ = tx.Rollback() + return err + } + + // Create/update recipients for the notification + notificationID, err := result.LastInsertId() + if err != nil { + _ = tx.Rollback() + return err + } + + stmt, err = tx.Prepare("REPLACE INTO cbox_notification_recipients (notification_id, recipient) VALUES (?, ?)") + if err != nil { + _ = tx.Rollback() + return err + } + defer stmt.Close() + + for _, recipient := range n.Recipients { + _, err := stmt.Exec(notificationID, recipient) + if err != nil { + _ = tx.Rollback() + return err + } + } + + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + +// GetNotification reads a notification. +func (m *mgr) GetNotification(ref string) (*notification.Notification, error) { + query := ` + SELECT n.id, n.ref, n.template_name, nr.recipient + FROM cbox_notifications AS n + JOIN cbox_notification_recipients AS nr ON n.id = nr.notification_id + WHERE n.ref = ? + ` + + rows, err := m.db.Query(query, ref) + if err != nil { + return nil, err + } + defer rows.Close() + + var n notification.Notification + count := 0 + n.Recipients = make([]string, 0) + + for rows.Next() { + var id string + var recipient string + err := rows.Scan(&id, &n.Ref, &n.TemplateName, &recipient) + if err != nil { + return nil, err + } + n.Recipients = append(n.Recipients, recipient) + count++ + } + if err = rows.Err(); err != nil { + return nil, err + } + if count == 0 { + return nil, ¬ification.NotFoundError{ + Ref: n.Ref, + } + } + + return &n, nil +} + +// DeleteNotification deletes a notification. +func (m *mgr) DeleteNotification(ref string) error { + tx, err := m.db.Begin() + if err != nil { + return err + } + + // Delete notification + stmt, err := m.db.Prepare("DELETE FROM cbox_notifications WHERE ref = ?") + if err != nil { + return err + } + defer stmt.Close() + + result, err := stmt.Exec(ref) + if err != nil { + _ = tx.Rollback() + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + _ = tx.Rollback() + return err + } + + if rowsAffected == 0 { + return ¬ification.NotFoundError{ + Ref: ref, + } + } + + return nil +} diff --git a/pkg/notification/manager/sql/sql_suite_test.go b/pkg/notification/manager/sql/sql_suite_test.go new file mode 100644 index 0000000000..e3c6fbd8e9 --- /dev/null +++ b/pkg/notification/manager/sql/sql_suite_test.go @@ -0,0 +1,31 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sql_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestSql(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Sql Suite") +} diff --git a/pkg/notification/manager/sql/sql_test.go b/pkg/notification/manager/sql/sql_test.go new file mode 100644 index 0000000000..60080ffab0 --- /dev/null +++ b/pkg/notification/manager/sql/sql_test.go @@ -0,0 +1,252 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sql_test + +import ( + "database/sql" + "fmt" + "os" + + "github.com/cs3org/reva/pkg/notification" + sqlmanager "github.com/cs3org/reva/pkg/notification/manager/sql" + _ "github.com/mattn/go-sqlite3" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SQL manager for notifications", func() { + var ( + db *sql.DB + testDBFile *os.File + mgr notification.Manager + n1 = ¬ification.Notification{ + Ref: "notification-test", + TemplateName: "notification-template-test", + Recipients: []string{"jdoe", "testuser"}, + } + n2 = ¬ification.Notification{ + Ref: "new-notification", + TemplateName: "new-template", + Recipients: []string{"newuser1", "newuser2"}, + } + nn *notification.Notification + ref string + err error + selectNotificationsSQL = "SELECT ref, template_name FROM cbox_notifications WHERE ref = ?" + selectNotificationRecipientsSQL = "SELECT COUNT(*) FROM cbox_notification_recipients WHERE notification_id = ?" + ) + + AfterEach(func() { + os.Remove(testDBFile.Name()) + }) + + BeforeEach(func() { + var err error + ref = "notification-test" + + testDBFile, err = os.CreateTemp("", "testdbfile") + Expect(err).ToNot(HaveOccurred()) + + dbData, err := os.ReadFile("test.sqlite") + Expect(err).ToNot(HaveOccurred()) + + _, err = testDBFile.Write(dbData) + Expect(err).ToNot(HaveOccurred()) + + err = testDBFile.Close() + Expect(err).ToNot(HaveOccurred()) + + db, err = sql.Open("sqlite3", fmt.Sprintf("%v?_foreign_keys=on", testDBFile.Name())) + Expect(err).ToNot(HaveOccurred()) + Expect(db).ToNot(BeNil()) + + mgr, err = sqlmanager.New("sqlite3", db) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + os.Remove(testDBFile.Name()) + }) + + Context("Creating notifications", func() { + When("creating a non-existing notification", func() { + JustBeforeEach(func() { + err = mgr.UpsertNotification(*n2) + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should create a notification entry", func() { + var newRef, newTemplateName string + err = db.QueryRow(selectNotificationsSQL, n2.Ref).Scan(&newRef, &newTemplateName) + Expect(newRef).To(Equal(n2.Ref)) + Expect(newTemplateName).To(Equal(n2.TemplateName)) + }) + + It("should create notification recipients entries", func() { + var notificationID int + err = db.QueryRow("SELECT id FROM cbox_notifications WHERE ref = ?", n2.Ref).Scan(¬ificationID) + Expect(err).ToNot(HaveOccurred()) + var newRecipientCount int + err = db.QueryRow(selectNotificationRecipientsSQL, notificationID).Scan(&newRecipientCount) + Expect(err).ToNot(HaveOccurred()) + Expect(newRecipientCount).To(Equal(len(n2.Recipients))) + }) + }) + + When("updating an existing notification", func() { + var m = ¬ification.Notification{ + Ref: "notification-test", + TemplateName: "new-notification-template-test", + Recipients: []string{"jdoe", "testuser2", "thirduser"}, + } + + JustBeforeEach(func() { + err = mgr.UpsertNotification(*m) + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should not increase the number of entries in the notification table", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notifications").Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + + It("should update the existing notification data", func() { + var newRef, newTemplateName string + err = db.QueryRow(selectNotificationsSQL, m.Ref).Scan(&newRef, &newTemplateName) + Expect(newRef).To(Equal(m.Ref)) + Expect(newTemplateName).To(Equal(m.TemplateName)) + }) + + It("should delete old entries in notification recipients", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notification_recipients WHERE recipient = 'testuser'").Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(BeZero()) + }) + + It("should create new entries in notification recipients", func() { + var notificationID int + err = db.QueryRow("SELECT id FROM cbox_notifications WHERE ref = ?", m.Ref).Scan(¬ificationID) + Expect(err).ToNot(HaveOccurred()) + var newRecipientCount int + err = db.QueryRow(selectNotificationRecipientsSQL, notificationID).Scan(&newRecipientCount) + Expect(err).ToNot(HaveOccurred()) + Expect(newRecipientCount).To(Equal(len(m.Recipients))) + }) + }) + + When("creating an invalid notification", func() { + o := ¬ification.Notification{} + + JustBeforeEach(func() { + err = mgr.UpsertNotification(*o) + }) + + It("should return an InvalidNotificationError", func() { + _, isInvalidNotificationError := err.(*notification.InvalidNotificationError) + Expect(err).To(HaveOccurred()) + Expect(isInvalidNotificationError).To(BeTrue()) + }) + }) + }) + + Context("Getting notifications", func() { + When("getting an existing notification", func() { + JustBeforeEach(func() { + nn, err = mgr.GetNotification(ref) + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a notification", func() { + Expect(nn.Ref).To(Equal(n1.Ref)) + Expect(nn.TemplateName).To(Equal(n1.TemplateName)) + Expect(nn.Recipients).To(Equal(n1.Recipients)) + }) + }) + + When("getting a non-existing notification", func() { + JustBeforeEach(func() { + nn, err = mgr.GetNotification("non-existent-ref") + }) + + It("should return a NotFoundError", func() { + _, isNotFoundError := err.(*notification.NotFoundError) + Expect(err).To(HaveOccurred()) + Expect(isNotFoundError).To(BeTrue()) + }) + }) + }) + + Context("Deleting notifications", func() { + When("deleting an existing notification", func() { + JustBeforeEach(func() { + err = mgr.DeleteNotification(ref) + + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should delete the notification from the database", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notifications WHERE ref = ?", ref).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(BeZero()) + }) + + It("should cascade the deletions to notification_recipients table", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notification_recipients WHERE notification_id = ?", 1).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(BeZero()) + }) + }) + + When("deleting a non-existing notification", func() { + JustBeforeEach(func() { + err = mgr.DeleteNotification("non-existent-ref") + + }) + + It("should not change the db and return a NotFoundError error", func() { + Expect(err).To(HaveOccurred()) + isNotFoundError, _ := err.(*notification.NotFoundError) + Expect(isNotFoundError).ToNot(BeNil()) + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notifications").Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + }) + + }) + +}) diff --git a/pkg/notification/manager/sql/test.sqlite b/pkg/notification/manager/sql/test.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..7bf9289cdda218362bae3993b3e552879705b4f0 GIT binary patch literal 32768 zcmeI*Pfyxl90%~HEmDeqx@0q)Or9f+PC*Z5mt`G3IwBUWFlTn42qozvptL4tmj##X zh3pk9*^Ah5*S!R{-8T}f?rs_0CXsrx>m*wW|e^Lsvh+TmJ!pL_V=}$KXx3O@FQ_#rtG_9W=8eI*ESj{s)mvJ7I(8iD##KU14XR)Mqfk>< z3-t_}Q?;~%$5A=KnDF+ESKivT2L&`}-B2gkg8nqeE)|O_7O$J{gaTV)CS5hNE4sN! zm)R!Oi-mmF;9FZ^Mq!F)?2oux%E%X}QOxE1Os9K*KG)66Gu=$i&89TklV>q+vh0!( zRFdr5s1{cglPxln85y=tLk)Fl(y98HcyCx>ImSCsMqkh93oNd+%Mm$|P74S1^8wxP z*2GJ&T;8eTw3DtX)y?0sYu2ekYJOIuMI*abWIY<)%${A}*)Dr_sZp-mJ;i$Q;@YYd zO-xP-n?VcR-EBT3ExGpYp53U}eT$JZZR=Z^^lO>oB{lVklmfMve?{%JEGHhNg?8fn zqtHK1gpWNp+KK(5lS5GKg5%8JXS^HMEG;t>O>CqE5>(v*DDQdAV7V0T4W;l2%a!O!x~40uX=z1Rwwb2tWV=5P$##AOL~eAuuCE$c)~5_35E!yI$W1$CqS+ zHF4f9LH$nn3kd=cfB*y_009U<00Izz00bZaf!ir!L!$mr4{t~V z5e5Vx009U<00Izz00bZa0SG_<0@oB65hFyI@Sg?PbM01)KPI4zzp6HE-2Z<~P!xjz z1Rwwb2tWV=5P$##AOHaf+)ROU@BjD5|DyVnsK59d5(FRs0SG_<0uX=z1Rwwb2tWV= zcS#^3DS{|TigG;u=fD4V6#N2UlsD3!{QnOi-lYbkEf9bJ1Rwwb2tWV=5P$##AOL~? UL|{ak5KbNd5M^mx@$dit4QC@5C;$Ke literal 0 HcmV?d00001 diff --git a/pkg/notification/notification.go b/pkg/notification/notification.go new file mode 100644 index 0000000000..fdf615f369 --- /dev/null +++ b/pkg/notification/notification.go @@ -0,0 +1,114 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package notification + +import ( + "fmt" + + "github.com/cs3org/reva/pkg/notification/template" +) + +// Notification is the representation of a notification. +type Notification struct { + TemplateName string + Template template.Template + Ref string + Recipients []string +} + +// Manager is the interface notification storage managers have to implement. +type Manager interface { + // UpsertNotification insert or updates a notification. + UpsertNotification(n Notification) error + // GetNotification reads a notification. + GetNotification(ref string) (*Notification, error) + // DeleteNotification deletes a notifcation. + DeleteNotification(ref string) error +} + +// NotFoundError is the error returned when a notification does not exist. +type NotFoundError struct { + Ref string +} + +// InvalidNotificationError is the error returned when a notification has invalid data. +type InvalidNotificationError struct { + Ref string + Msg string + Err error +} + +// Error returns the string error msg for NotFoundError. +func (n *NotFoundError) Error() string { + return fmt.Sprintf("notification %s not found", n.Ref) +} + +// Error returns the string error msg for InvalidNotificationError. +func (i *InvalidNotificationError) Error() string { + return i.Msg +} + +// Send is the method run when a notification is triggered. +func (n *Notification) Send(sender string, templateData map[string]interface{}) error { + subject, err := n.Template.RenderSubject(templateData) + if err != nil { + return err + } + + body, err := n.Template.RenderBody(templateData) + if err != nil { + return err + } + + for _, recipient := range n.Recipients { + err := n.Template.Handler.Send(sender, recipient, subject, body) + if err != nil { + return err + } + } + + return nil +} + +// CheckNotification checks if a notification has correct data. +func (n *Notification) CheckNotification() error { + if len(n.Ref) == 0 { + return &InvalidNotificationError{ + Ref: n.Ref, + Msg: "empty ref", + } + } + + if err := template.CheckTemplateName(n.TemplateName); err != nil { + return &InvalidNotificationError{ + Ref: n.Ref, + Msg: fmt.Sprintf("invalid template name %s", n.TemplateName), + Err: err, + } + } + + if len(n.Recipients) == 0 { + return &InvalidNotificationError{ + Ref: n.Ref, + Msg: "empty recipient list", + } + } + + return nil +} diff --git a/pkg/notification/notificationhelper/notificationhelper.go b/pkg/notification/notificationhelper/notificationhelper.go new file mode 100644 index 0000000000..af92e2f4d1 --- /dev/null +++ b/pkg/notification/notificationhelper/notificationhelper.go @@ -0,0 +1,250 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package notificationhelper + +import ( + "encoding/json" + "fmt" + + "github.com/cs3org/reva/pkg/notification" + "github.com/cs3org/reva/pkg/notification/template" + "github.com/cs3org/reva/pkg/notification/trigger" + "github.com/cs3org/reva/pkg/notification/utils" + "github.com/mitchellh/mapstructure" + "github.com/nats-io/nats.go" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// NotificationHelper is the type used in services to work with notifications. +type NotificationHelper struct { + Name string + Conf *Config + Log *zerolog.Logger + nc *nats.Conn + js nats.JetStreamContext + kv nats.KeyValue +} + +// Config contains the configuration for the Notification Helper. +type Config struct { + NatsAddress string `mapstructure:"nats_address" docs:";The NATS server address."` + NatsToken string `mapstructure:"nats_token" docs:";The token to authenticate against the NATS server"` + NatsStream string `mapstructure:"nats_stream" docs:"reva-notifications;The notifications NATS stream."` + Templates map[string]interface{} `mapstructure:"templates" docs:";Notification templates for the service."` +} + +func defaultConfig() *Config { + return &Config{ + NatsStream: "reva-notifications", + } +} + +// New creates a new Notification Helper. +func New(name string, m map[string]interface{}, log *zerolog.Logger) *NotificationHelper { + conf := defaultConfig() + nh := &NotificationHelper{ + Name: name, + Conf: conf, + Log: log, + } + + if err := mapstructure.Decode(m, conf); err != nil { + log.Error().Err(err).Msgf("decoding config failed, notifications will be disabled") + return nh + } + + annotatedLogger := log.With().Str("service", nh.Name).Str("scope", "notifications").Logger() + nh.Log = &annotatedLogger + + if err := nh.connect(); err != nil { + log.Error().Err(err).Msgf("connecting to nats failed, notifications will be disabled") + return nh + } + + nh.registerTemplates(nh.Conf.Templates) + + return nh +} + +func (nh *NotificationHelper) connect() error { + nc, err := utils.ConnectToNats(nh.Conf.NatsAddress, nh.Conf.NatsToken, *nh.Log) + if err != nil { + return err + } + nh.nc = nc + + js, err := nh.nc.JetStream(nats.PublishAsyncMaxPending(256)) + if err != nil { + return errors.Wrap(err, "jetstream initialization failed") + } + stream, _ := js.StreamInfo(nh.Conf.NatsStream) + if stream != nil { + if _, err := js.AddStream(&nats.StreamConfig{ + Name: nh.Conf.NatsStream, + Subjects: []string{ + fmt.Sprintf("%s.notification", nh.Conf.NatsStream), + fmt.Sprintf("%s.trigger", nh.Conf.NatsStream), + }, + }); err != nil { + return errors.Wrap(err, "nats stream creation failed") + } + } + nh.js = js + + bucketName := fmt.Sprintf("%s-template", nh.Conf.NatsStream) + kv, err := nh.js.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: bucketName, + }) + if err != nil { + return errors.Wrap(err, "template store creation failed, probably because nats server is unreachable") + } + nh.kv = kv + return nil +} + +// Stop stops the notification helper. +func (nh *NotificationHelper) Stop() { + if err := nh.nc.Drain(); err != nil { + nh.Log.Error().Err(err) + } +} + +func (nh *NotificationHelper) registerTemplates(ts map[string]interface{}) { + if len(ts) == 0 { + nh.Log.Info().Msg("no templates to register") + return + } + + tCount := 0 + for tn, tm := range ts { + var tc template.RegistrationRequest + if err := mapstructure.Decode(tm, &tc); err != nil { + nh.Log.Error().Err(err).Msgf("template '%s' definition decoding failed", tn) + continue + } + if err := template.CheckTemplateName(tc.Name); err != nil { + nh.Log.Error().Err(err).Msgf("template name '%s' is incorrect", tc.Name) + continue + } + if tc.Handler == "" { + nh.Log.Error().Msgf("template definition '%s' is missing handler field", tn) + continue + } + if tc.BodyTmplPath == "" { + nh.Log.Error().Msgf("template definition '%s' is missing body_template_path field", tn) + continue + } + + nh.registerTemplate(&tc) + tCount++ + } + + nh.Log.Info().Msgf("%d templates to register", tCount) +} + +func (nh *NotificationHelper) registerTemplate(rr *template.RegistrationRequest) { + if nh.kv == nil { + nh.Log.Info().Msgf("template registration skipped, helper is misconfigured") + return + } + + tb, err := json.Marshal(rr) + if err != nil { + nh.Log.Error().Err(err).Msgf("template registration json marshalling failed") + } + + go func() { + _, err := nh.kv.Put(rr.Name, tb) + if err != nil { + nh.Log.Error().Err(err).Msgf("template registration publish failed") + return + } + nh.Log.Debug().Msgf("%s template registration published", rr.Name) + }() +} + +// RegisterNotification registers a notification in the notification service. +func (nh *NotificationHelper) RegisterNotification(n *notification.Notification) { + if nh.js == nil { + nh.Log.Info().Msgf("notification registration skipped, helper is misconfigured") + return + } + + nb, err := json.Marshal(n) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification registration json marshalling failed") + return + } + + notificationSubject := fmt.Sprintf("%s.notification-register", nh.Conf.NatsStream) + + go func() { + _, err := nh.js.Publish(notificationSubject, nb) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification registration publish failed") + return + } + nh.Log.Debug().Msgf("%s notification registration published", n.Ref) + }() +} + +// UnregisterNotification unregisters a notification in the notification service. +func (nh *NotificationHelper) UnregisterNotification(ref string) { + if nh.js == nil { + nh.Log.Info().Msgf("notification unregistration skipped, notification helper is misconfigured") + return + } + + notificationSubject := fmt.Sprintf("%s.notification-unregister", nh.Conf.NatsStream) + + go func() { + _, err := nh.js.Publish(notificationSubject, []byte(ref)) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification unregistration publish failed") + return + } + nh.Log.Debug().Msgf("%s notification unregistration published", ref) + }() +} + +// TriggerNotification sends a notification trigger to the notifications service. +func (nh *NotificationHelper) TriggerNotification(tr *trigger.Trigger) { + if nh.js == nil { + nh.Log.Info().Msgf("notification trigger skipped, notification helper is misconfigured") + return + } + + trb, err := json.Marshal(tr) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification trigger json marshalling failed") + return + } + + triggerSubject := fmt.Sprintf("%s.trigger", nh.Conf.NatsStream) + + go func() { + _, err := nh.js.Publish(triggerSubject, trb) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification trigger publish failed") + return + } + nh.Log.Debug().Msgf("%s notification trigger published", tr.Ref) + }() +} diff --git a/pkg/notification/template/registry/registry.go b/pkg/notification/template/registry/registry.go new file mode 100644 index 0000000000..bb13fe6a8f --- /dev/null +++ b/pkg/notification/template/registry/registry.go @@ -0,0 +1,69 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package registry + +import ( + "encoding/json" + "fmt" + + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/cs3org/reva/pkg/notification/template" + "github.com/pkg/errors" +) + +// Registry provides with means for dynamically registering notification templates. +type Registry struct { + store map[string]template.Template +} + +// New returns a new Template Registry. +func New() *Registry { + r := &Registry{ + store: make(map[string]template.Template), + } + + return r +} + +// Put registers a handler in the registry. +func (r *Registry) Put(tb []byte, hs map[string]handler.Handler) (string, error) { + var data map[string]interface{} + + err := json.Unmarshal(tb, &data) + if err != nil { + return "", errors.Wrapf(err, "template registration unmarshall failed") + } + + t, name, err := template.New(data, hs) + if err != nil { + return name, errors.Wrapf(err, "template %s registration failed", name) + } + + r.store[t.Name] = *t + return t.Name, nil +} + +// Get retrieves a handler from the registry. +func (r *Registry) Get(n string) (*template.Template, error) { + if t, ok := r.store[n]; ok { + return &t, nil + } + + return nil, fmt.Errorf("template %s not found", n) +} diff --git a/pkg/notification/template/template.go b/pkg/notification/template/template.go new file mode 100644 index 0000000000..39d5fc0c18 --- /dev/null +++ b/pkg/notification/template/template.go @@ -0,0 +1,170 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package template + +import ( + "bytes" + "errors" + "fmt" + htmlTemplate "html/template" + "io" + "os" + "path/filepath" + "regexp" + textTemplate "text/template" + + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/mitchellh/mapstructure" +) + +const validTemplateNameRegex = "[a-zA-Z0-9-]" + +// RegistrationRequest represents a Template registration request. +type RegistrationRequest struct { + Name string `mapstructure:"name" json:"name"` + Handler string `mapstructure:"handler" json:"handler"` + BodyTmplPath string `mapstructure:"body_template_path" json:"body_template_path"` + SubjectTmplPath string `mapstructure:"subject_template_path" json:"subject_template_path"` + Persistent bool `mapstructure:"persistent" json:"persistent"` +} + +// Template represents a notification template. +type Template struct { + Name string + Handler handler.Handler + Persistent bool + tmplSubject *textTemplate.Template + tmplBody *htmlTemplate.Template +} + +// FileNotFoundError is the error returned when a template file is missing. +type FileNotFoundError struct { + TemplateFileName string + Err error +} + +// Error returns the string error msg for FileNotFoundError. +func (t *FileNotFoundError) Error() string { + return fmt.Sprintf("template file %s not found", t.TemplateFileName) +} + +// New creates a new Template from a RegistrationRequest. +func New(m map[string]interface{}, hs map[string]handler.Handler) (*Template, string, error) { + rr := &RegistrationRequest{} + if err := mapstructure.Decode(m, rr); err != nil { + return nil, rr.Name, err + } + + h, ok := hs[rr.Handler] + if !ok { + return nil, rr.Name, fmt.Errorf("unknown handler %s", rr.Handler) + } + + tmplSubject, err := parseTmplFile(rr.SubjectTmplPath, "subject") + if err != nil { + return nil, rr.Name, err + } + + tmplBody, err := parseTmplFile(rr.BodyTmplPath, "body") + if err != nil { + return nil, rr.Name, err + } + + t := &Template{ + Name: rr.Name, + Handler: h, + tmplSubject: tmplSubject.(*textTemplate.Template), + tmplBody: tmplBody.(*htmlTemplate.Template), + } + + if err := CheckTemplateName(t.Name); err != nil { + return nil, rr.Name, err + } + + return t, rr.Name, nil +} + +// RenderSubject renders the subject template. +func (t *Template) RenderSubject(arguments map[string]interface{}) (string, error) { + var buf bytes.Buffer + err := t.tmplSubject.Execute(&buf, arguments) + return buf.String(), err +} + +// RenderBody renders the body template. +func (t *Template) RenderBody(arguments map[string]interface{}) (string, error) { + var buf bytes.Buffer + err := t.tmplBody.Execute(&buf, arguments) + return buf.String(), err +} + +// CheckTemplateName validates the name of the template. +func CheckTemplateName(name string) error { + if name == "" { + return errors.New("template name cannot be empty") + } + + re := regexp.MustCompile(validTemplateNameRegex) + invalidChars := re.ReplaceAllString(name, "") + if len(invalidChars) > 0 { + return fmt.Errorf("template name %s must contain only %s", name, validTemplateNameRegex) + } + + return nil +} + +func parseTmplFile(path, name string) (interface{}, error) { + if path == "" { + return textTemplate.New(name).Parse("") + } + + ext := filepath.Ext(path) + f, err := os.Open(path) + if err != nil { + return nil, &FileNotFoundError{ + TemplateFileName: path, + Err: err, + } + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + switch ext { + case ".txt": + tmpl, err := textTemplate.New(name).Parse(string(data)) + if err != nil { + return nil, err + } + + return tmpl, nil + case ".html": + tmpl, err := htmlTemplate.New(name).Parse(string(data)) + if err != nil { + return nil, err + } + + return tmpl, nil + default: + return nil, errors.New("unknown template type") + } +} diff --git a/pkg/notification/trigger/trigger.go b/pkg/notification/trigger/trigger.go new file mode 100644 index 0000000000..0fc60885c1 --- /dev/null +++ b/pkg/notification/trigger/trigger.go @@ -0,0 +1,41 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package trigger + +import ( + "github.com/cs3org/reva/pkg/notification" +) + +// Trigger represents a notification Trigger. +type Trigger struct { + Notification *notification.Notification + Ref string + Sender string + TemplateData map[string]interface{} +} + +// Send is the method run when a notification is triggered. +func (t *Trigger) Send() error { + err := t.Notification.Send(t.Sender, t.TemplateData) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/notification/utils/nats.go b/pkg/notification/utils/nats.go new file mode 100644 index 0000000000..cf206f46e8 --- /dev/null +++ b/pkg/notification/utils/nats.go @@ -0,0 +1,63 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Package utils contains utilities related to the notifications service and helper. +package utils + +import ( + "time" + + "github.com/nats-io/nats.go" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// ConnectToNats returns a resilient connection to the specified NATS server. +func ConnectToNats(natsAddress, natsToken string, log zerolog.Logger) (*nats.Conn, error) { + nc, err := nats.Connect( + natsAddress, + nats.DrainTimeout(9*time.Second), // reva timeout on graceful shutdown is 10 seconds + nats.MaxReconnects(-1), + nats.Token(natsToken), + nats.ErrorHandler(func(c *nats.Conn, s *nats.Subscription, err error) { + log.Error().Err(err).Msgf("nats error") + }), + nats.ClosedHandler(func(c *nats.Conn) { + log.Error().Err(c.LastError()).Msgf("connection to nats server closed") + }), + nats.DisconnectErrHandler(func(_ *nats.Conn, err error) { + log.Error().Err(err).Msgf("connection to nats server disconnected") + }), + nats.CustomReconnectDelay(func(attempts int) time.Duration { + if attempts%3 == 0 { + log.Info().Msg("connection to nats server failed 3 times, backing off") + return 5 * time.Minute + } + + return 2 * time.Second + }), + nats.ReconnectHandler(func(_ *nats.Conn) { + log.Info().Msgf("connection to nats server reconnected") + }), + ) + if err != nil { + return nil, errors.Wrapf(err, "connection to nats server at '%s' failed", natsAddress) + } + + return nc, nil +} diff --git a/pkg/utils/accumulator/accumulator.go b/pkg/utils/accumulator/accumulator.go new file mode 100644 index 0000000000..710812f172 --- /dev/null +++ b/pkg/utils/accumulator/accumulator.go @@ -0,0 +1,126 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package accumulator + +import ( + "errors" + "time" + + "github.com/rs/zerolog" +) + +// Accumulator gathers items arriving spaced in time and groups them. +type Accumulator[T any] struct { + started bool + timeout time.Duration + timeoutChan chan bool + timeoutResetChan chan bool + maxSize int + Input chan T + pool []T + log *zerolog.Logger +} + +// New creates a new accumulator. An Accumulator gathers items arriving spaced +// in time and groups them. +// +// The main parameters are timeout and maxSize, determining the limits for the +// accumulator. +// +// An accumulator is started with the start method, which takes fn, a func([]T) +// argument that will be run every time the limit parameters are reached. After +// running fn, the accumulator pool is emptied. +// +// Items are put into the accumulator using the <-input channel, making it +// thread-safe. +func New[T any](timeout time.Duration, maxSize int, log *zerolog.Logger) *Accumulator[T] { + if timeout == 0 { + timeout = time.Duration(60) * time.Second + log.Warn().Msgf("timeout must be a positive duration greater than zero, using default (%d)", timeout) + } + + if maxSize == 0 { + maxSize = 100 + log.Warn().Msgf("maxSize must be a positive integer greater than zero, using default (%d)", maxSize) + } + + input := make(chan T) + accumulator := &Accumulator[T]{ + timeout: timeout, + timeoutResetChan: make(chan bool, 1), + maxSize: maxSize, + Input: input, + log: log, + } + + return accumulator +} + +func (a *Accumulator[T]) startTimeout() { + if !a.started { + a.started = true + a.timeoutChan = make(chan bool) + go func() { + select { + case <-a.timeoutResetChan: + a.timeoutChan = nil + case <-time.After(a.timeout): + a.timeoutChan <- true + a.timeoutChan = nil + } + a.started = false + }() + } +} + +// Start starts the accumulator. +// +// This does not mean the timer will start running. That happens once the first +// item arrives through the <-input channel. Once the time reaches the timeout +// or the max size of the accumulator is reached, fn will be run with the slice +// of items currently in the accumulator. +func (a *Accumulator[T]) Start(fn func([]T)) error { + if fn == nil { + return errors.New("fn must be a callback function") + } + + go func() { + for { + select { + case i := <-a.Input: + a.startTimeout() + a.pool = append(a.pool, i) + + if len(a.pool) >= a.maxSize { + fn(a.pool) + a.pool = nil + a.timeoutResetChan <- true + a.timeoutChan = nil + } + case <-a.timeoutChan: + if len(a.pool) > 0 { + fn(a.pool) + a.pool = nil + } + } + } + }() + + return nil +} From 82fb53c072a7cc4cb907af6f48b53570d13aca78 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 26 Apr 2023 14:45:34 +0200 Subject: [PATCH 2/8] Add notification helper to ocdav --- internal/http/services/owncloud/ocdav/ocdav.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 4c029c6560..7f6d7ca366 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -33,6 +33,7 @@ import ( "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/notification/notificationhelper" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/rhttp/global" @@ -111,6 +112,7 @@ type Config struct { PublicURL string `mapstructure:"public_url"` FavoriteStorageDriver string `mapstructure:"favorite_storage_driver"` FavoriteStorageDrivers map[string]map[string]interface{} `mapstructure:"favorite_storage_drivers"` + Notifications map[string]interface{} `mapstructure:"notifications" docs:"Settingsg for the Notification Helper"` } func (c *Config) init() { @@ -127,11 +129,12 @@ func (c *Config) init() { } type svc struct { - c *Config - webDavHandler *WebDavHandler - davHandler *DavHandler - favoritesManager favorite.Manager - client *http.Client + c *Config + webDavHandler *WebDavHandler + davHandler *DavHandler + favoritesManager favorite.Manager + client *http.Client + notificationHelper *notificationhelper.NotificationHelper } func getFavoritesManager(c *Config) (favorite.Manager, error) { @@ -163,8 +166,10 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))), rhttp.Insecure(conf.Insecure), ), - favoritesManager: fm, + favoritesManager: fm, + notificationHelper: notificationhelper.New("ocdav", conf.Notifications, log), } + // initialize handlers and set default configs if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil { return nil, err @@ -180,6 +185,7 @@ func (s *svc) Prefix() string { } func (s *svc) Close() error { + s.notificationHelper.Stop() return nil } From ed69fb6e0b29ce7e486be3add20087df2a08ea4a Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 26 Apr 2023 15:01:21 +0200 Subject: [PATCH 3/8] Add notification helper to ocs --- internal/http/services/owncloud/ocs/config/config.go | 1 + .../owncloud/ocs/handlers/apps/sharing/shares/shares.go | 8 ++++++-- internal/http/services/owncloud/ocs/ocs.go | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/http/services/owncloud/ocs/config/config.go b/internal/http/services/owncloud/ocs/config/config.go index 31b1850eb1..123cdd47be 100644 --- a/internal/http/services/owncloud/ocs/config/config.go +++ b/internal/http/services/owncloud/ocs/config/config.go @@ -45,6 +45,7 @@ type Config struct { AllowedLanguages []string `mapstructure:"allowed_languages"` OCMMountPoint string `mapstructure:"ocm_mount_point"` ListOCMShares bool `mapstructure:"list_ocm_shares"` + Notifications map[string]interface{} `mapstructure:"notifications"` } // Init sets sane defaults. diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 1553dbb25f..b2390d0273 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -44,6 +44,7 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/notification/notificationhelper" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/share" @@ -74,6 +75,8 @@ type Handler struct { resourceInfoCache cache.ResourceInfoCache resourceInfoCacheTTL time.Duration listOCMShares bool + notificationHelper *notificationhelper.NotificationHelper + Log *zerolog.Logger } // we only cache the minimal set of data instead of the full user metadata. @@ -98,7 +101,7 @@ func getCacheManager(c *config.Config) (cache.ResourceInfoCache, error) { } // Init initializes this and any contained handlers. -func (h *Handler) Init(c *config.Config) { +func (h *Handler) Init(c *config.Config, l *zerolog.Logger) { h.gatewayAddr = c.GatewaySvc h.storageRegistryAddr = c.StorageregistrySvc h.publicURL = c.Config.Host @@ -106,7 +109,8 @@ func (h *Handler) Init(c *config.Config) { h.homeNamespace = c.HomeNamespace h.ocmMountPoint = c.OCMMountPoint h.listOCMShares = c.ListOCMShares - + h.Log = l + h.notificationHelper = notificationhelper.New("ocs", c.Notifications, l) h.additionalInfoTemplate, _ = template.New("additionalInfo").Parse(c.AdditionalInfoAttribute) h.resourceInfoCacheTTL = time.Second * time.Duration(c.ResourceInfoCacheTTL) diff --git a/internal/http/services/owncloud/ocs/ocs.go b/internal/http/services/owncloud/ocs/ocs.go index c579fa04f9..a55795e5cc 100644 --- a/internal/http/services/owncloud/ocs/ocs.go +++ b/internal/http/services/owncloud/ocs/ocs.go @@ -62,7 +62,7 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) router: r, } - if err := s.routerInit(); err != nil { + if err := s.routerInit(log); err != nil { return nil, err } @@ -86,7 +86,7 @@ func (s *svc) Unprotected() []string { return []string{"/v1.php/cloud/capabilities", "/v2.php/cloud/capabilities"} } -func (s *svc) routerInit() error { +func (s *svc) routerInit(l *zerolog.Logger) error { capabilitiesHandler := new(capabilities.Handler) userHandler := new(user.Handler) usersHandler := new(users.Handler) @@ -97,7 +97,7 @@ func (s *svc) routerInit() error { usersHandler.Init(s.c) userHandler.Init(s.c) configHandler.Init(s.c) - sharesHandler.Init(s.c) + sharesHandler.Init(s.c, l) shareesHandler.Init(s.c) s.router.Route("/v{version:(1|2)}.php", func(r chi.Router) { From 6f4712ec2db7717de41b421a48c864d33c3cbb64 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 26 Apr 2023 15:06:00 +0200 Subject: [PATCH 4/8] Add upload notification to public shares --- .../publicshareprovider.go | 2 +- internal/http/services/owncloud/ocdav/put.go | 51 +++++++++ .../services/owncloud/ocs/conversions/main.go | 24 ++-- .../handlers/apps/sharing/shares/public.go | 104 +++++++++++++++++- pkg/cbox/publicshare/sql/sql.go | 52 +++++---- pkg/cbox/utils/conversions.go | 60 +++++----- pkg/notification/db_changes.sql | 8 ++ pkg/publicshare/manager/json/json.go | 28 ++--- pkg/publicshare/manager/memory/memory.go | 28 ++--- pkg/publicshare/publicshare.go | 2 +- 10 files changed, 269 insertions(+), 90 deletions(-) diff --git a/internal/grpc/services/publicshareprovider/publicshareprovider.go b/internal/grpc/services/publicshareprovider/publicshareprovider.go index 09a28bc230..5829b050d8 100644 --- a/internal/grpc/services/publicshareprovider/publicshareprovider.go +++ b/internal/grpc/services/publicshareprovider/publicshareprovider.go @@ -145,7 +145,7 @@ func (s *service) CreatePublicShare(ctx context.Context, req *link.CreatePublicS log.Error().Msg("error getting user from context") } - share, err := s.sm.CreatePublicShare(ctx, u, req.ResourceInfo, req.Grant, req.Description, req.Internal) + share, err := s.sm.CreatePublicShare(ctx, u, req.ResourceInfo, req.Grant, req.Description, req.Internal, req.NotifyUploads, req.NotifyUploadsExtraRecipients) switch err.(type) { case nil: return &link.CreatePublicShareResponse{ diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index b91746e6ff..c2ee4fffc0 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -20,18 +20,22 @@ package ocdav import ( "context" + "encoding/json" "net/http" "path" + "path/filepath" "strconv" "strings" "time" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + linkv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/internal/http/services/datagateway" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/notification/trigger" "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/storage/utils/chunking" rtrace "github.com/cs3org/reva/pkg/trace" @@ -321,6 +325,53 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ lastModifiedString := t.Format(time.RFC1123Z) w.Header().Set(HeaderLastModified, lastModifiedString) + var m map[string]*typespb.OpaqueEntry + if sRes.Info.GetOpaque() != nil { + m = sRes.Info.GetOpaque().Map + } + + if ls, ok := m["link-share"]; ok { + l := &linkv1beta1.PublicShare{} + switch ls.Decoder { + case "json": + _ = json.Unmarshal(ls.Value, l) + default: + log.Error().Msgf("opaque entry decoder %s not recognized", ls.Decoder) + } + + path := "" + folder := "" + _, shareFileName := filepath.Split(ref.Path) + + if f, ok := m["eos"]; ok { + eosOpaque := make(map[string]interface{}) + switch f.Decoder { + case "json": + _ = json.Unmarshal(f.Value, &eosOpaque) + default: + log.Error().Msgf("opaque entry decoder %s not recognized", f.Decoder) + } + + if p, ok := eosOpaque["file"]; ok { + path, _ = filepath.Split(p.(string)) + } + } + + if path != "" { + folder = filepath.Base(path) + } + + trg := &trigger.Trigger{ + Ref: l.Id.OpaqueId, + TemplateData: map[string]interface{}{ + "path": path, + "folder": folder, + "fileName": shareFileName, + }, + } + s.notificationHelper.TriggerNotification(trg) + } + // file was new if info == nil { w.WriteHeader(http.StatusCreated) diff --git a/internal/http/services/owncloud/ocs/conversions/main.go b/internal/http/services/owncloud/ocs/conversions/main.go index 24a50b9246..1650ab3182 100644 --- a/internal/http/services/owncloud/ocs/conversions/main.go +++ b/internal/http/services/owncloud/ocs/conversions/main.go @@ -149,6 +149,10 @@ type ShareData struct { Quicklink bool `json:"quicklink,omitempty" xml:"quicklink,omitempty"` // Description of the public share Description string `json:"description" xml:"description"` + // Whether to notify owner of file uploads to the public share + NotifyUploads bool `json:"notify_uploads" xml:"notify_uploads"` + // Additional recipients for the file upload to public share notification + NotifyUploadsExtraRecipients string `json:"notify_uploads_extra_recipients" xml:"notify_uploads_extra_recipients"` } // ShareeData holds share recipient search results. @@ -214,15 +218,17 @@ func PublicShare2ShareData(share *link.PublicShare, r *http.Request, publicURL s sd := &ShareData{ // share.permissions are mapped below // Displaynames are added later - ShareType: ShareTypePublicLink, - Token: share.Token, - Name: share.DisplayName, - MailSend: 0, - URL: publicURL + path.Join("/", "s/"+share.Token), - UIDOwner: LocalUserIDToString(share.Creator), - UIDFileOwner: LocalUserIDToString(share.Owner), - Quicklink: share.Quicklink, - Description: share.Description, + ShareType: ShareTypePublicLink, + Token: share.Token, + Name: share.DisplayName, + MailSend: 0, + URL: publicURL + path.Join("/", "s/"+share.Token), + UIDOwner: LocalUserIDToString(share.Creator), + UIDFileOwner: LocalUserIDToString(share.Owner), + Quicklink: share.Quicklink, + Description: share.Description, + NotifyUploads: share.NotifyUploads, + NotifyUploadsExtraRecipients: share.NotifyUploadsExtraRecipients, } if share.Id != nil { sd.ID = share.Id.OpaqueId diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go index 78784a9d05..312b55f3fb 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go @@ -31,6 +31,8 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/notification" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/pkg/errors" @@ -109,6 +111,8 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, } internal, _ := strconv.ParseBool(r.FormValue("internal")) + notifyUploads, _ := strconv.ParseBool(r.FormValue("notifyUploads")) + notifyUploadsExtraRecipients := r.FormValue("notifyUploadsExtraRecipients") req := link.CreatePublicShareRequest{ ResourceInfo: statInfo, @@ -118,8 +122,10 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, }, Password: r.FormValue("password"), }, - Description: r.FormValue("description"), - Internal: internal, + Description: r.FormValue("description"), + Internal: internal, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, } expireTimeString, ok := r.Form["expireDate"] @@ -317,6 +323,25 @@ func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, shar }, }) } + + // remove notifications when a public link stops having 'uploader' permissions + if !isPermissionUploader(newPermissions) { + if before.Share.NotifyUploads { + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADS, + NotifyUploads: false, + }) + } + + if before.Share.NotifyUploadsExtraRecipients != "" { + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADSEXTRARECIPIENTS, + NotifyUploadsExtraRecipients: "", + }) + } + + h.notificationHelper.UnregisterNotification(shareID) + } } // ExpireDate @@ -371,6 +396,64 @@ func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, shar }) } + // NotifyUploads + newNotifyUploads, ok := r.Form["notifyUploads"] + + if ok { + ok2 := permissionsStayUploader(before, newPermissions) + u, ok3 := ctxpkg.ContextGetUser(r.Context()) + + if ok2 && ok3 { + notifyUploads, _ := strconv.ParseBool(newNotifyUploads[0]) + updatesFound = true + + logger.Info().Str("shares", "update").Msgf("notify uploads updated to '%v'", notifyUploads) + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADS, + NotifyUploads: notifyUploads, + }) + + if notifyUploads { + n := ¬ification.Notification{ + TemplateName: "sharedfolder-upload-mail", + Ref: shareID, + Recipients: []string{u.Mail}, + } + h.notificationHelper.RegisterNotification(n) + } else { + h.notificationHelper.UnregisterNotification(shareID) + } + } + } + + // NotifyUploadsExtraRecipients + newNotifyUploadsExtraRecipients, ok := r.Form["notifyUploadsExtraRecipients"] + + if ok { + ok2 := permissionsStayUploader(before, newPermissions) + u, ok3 := ctxpkg.ContextGetUser(r.Context()) + + if ok2 && ok3 { + notifyUploadsExtraRecipients := newNotifyUploadsExtraRecipients[0] + updatesFound = true + logger.Info().Str("shares", "update").Msgf("notify uploads extra recipients updated to '%v'", notifyUploadsExtraRecipients) + + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADSEXTRARECIPIENTS, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, + }) + + if len(notifyUploadsExtraRecipients) > 0 { + n := ¬ification.Notification{ + TemplateName: "sharedfolder-upload-mail", + Ref: shareID, + Recipients: []string{u.Mail, notifyUploadsExtraRecipients}, + } + h.notificationHelper.RegisterNotification(n) + } + } + } + publicShare := before.Share // Updates are atomical. See: https://github.com/cs3org/cs3apis/pull/67#issuecomment-617651428 so in order to get the latest updated version @@ -452,6 +535,8 @@ func (h *Handler) removePublicShare(w http.ResponseWriter, r *http.Request, shar return } + h.notificationHelper.UnregisterNotification(shareID) + response.WriteOCSSuccess(w, r, nil) } @@ -515,6 +600,21 @@ func permissionFromRequest(r *http.Request, h *Handler) (*provider.ResourcePermi return p, err } +func isPermissionUploader(permissions *provider.ResourcePermissions) bool { + if permissions == nil { + return false + } + + publicSharePermissions := &link.PublicSharePermissions{ + Permissions: permissions, + } + return conversions.RoleFromResourcePermissions(publicSharePermissions.Permissions).Name == conversions.RoleUploader +} + +func permissionsStayUploader(before *link.GetPublicShareResponse, newPermissions *provider.ResourcePermissions) bool { + return (newPermissions == nil && isPermissionUploader(before.Share.GetPermissions().Permissions)) || isPermissionUploader(newPermissions) +} + // TODO: add mapping for user share permissions to role // Maps oc10 public link permissions to roles. diff --git a/pkg/cbox/publicshare/sql/sql.go b/pkg/cbox/publicshare/sql/sql.go index d06c396903..aaefa85dac 100644 --- a/pkg/cbox/publicshare/sql/sql.go +++ b/pkg/cbox/publicshare/sql/sql.go @@ -135,7 +135,7 @@ func New(m map[string]interface{}) (publicshare.Manager, error) { return &mgr, nil } -func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) { +func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) { tkn := utils.RandString(15) now := time.Now().Unix() @@ -162,8 +162,8 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr fileSource = 0 } - query := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,token=?,share_name=?,quicklink=?,description=?,internal=?" - params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName, quicklink, description, internal} + query := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,token=?,share_name=?,quicklink=?,description=?,internal=?,notify_uploads=?,notify_uploads_extra_recipients=?" + params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName, quicklink, description, internal, notifyUploads, notifyUploadsExtraRecipients} var passwordProtected bool password := g.Password @@ -201,18 +201,20 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr Id: &link.PublicShareId{ OpaqueId: strconv.FormatInt(lastID, 10), }, - Owner: rInfo.GetOwner(), - Creator: u.Id, - ResourceId: rInfo.Id, - Token: tkn, - Permissions: g.Permissions, - Ctime: createdAt, - Mtime: createdAt, - PasswordProtected: passwordProtected, - Expiration: g.Expiration, - DisplayName: displayName, - Quicklink: quicklink, - Description: description, + Owner: rInfo.GetOwner(), + Creator: u.Id, + ResourceId: rInfo.Id, + Token: tkn, + Permissions: g.Permissions, + Ctime: createdAt, + Mtime: createdAt, + PasswordProtected: passwordProtected, + Expiration: g.Expiration, + DisplayName: displayName, + Quicklink: quicklink, + Description: description, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, }, nil } @@ -243,6 +245,10 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link } case link.UpdatePublicShareRequest_Update_TYPE_DESCRIPTION: paramsMap["description"] = req.Update.GetDescription() + case link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADS: + paramsMap["notify_uploads"] = req.Update.GetNotifyUploads() + case link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADSEXTRARECIPIENTS: + paramsMap["notify_uploads_extra_recipients"] = req.Update.GetNotifyUploadsExtraRecipients() default: return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType()) } @@ -276,8 +282,8 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, string, error) { s := conversions.DBShare{Token: token} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND token=?" - if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND token=?" + if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { if err == sql.ErrNoRows { return nil, "", errtypes.NotFound(token) } @@ -293,8 +299,8 @@ func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (* func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) { uid := conversions.FormatUserID(u.Id) s := conversions.DBShare{ID: id.OpaqueId} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions, quicklink, description FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)" - if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)" + if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { if err == sql.ErrNoRows { return nil, "", errtypes.NotFound(id.OpaqueId) } @@ -340,7 +346,7 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu } func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) { - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (share_type=?) AND internal=false" + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (share_type=?) AND internal=false" var resourceFilters, ownerFilters, creatorFilters string var resourceParams, ownerParams, creatorParams []interface{} params := []interface{}{publicShareType} @@ -398,7 +404,7 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters [] var s conversions.DBShare shares := []*link.PublicShare{} for rows.Next() { - if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { continue } cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s) @@ -460,8 +466,8 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) { s := conversions.DBShare{Token: token} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description FROM oc_share WHERE share_type=? AND token=?" - if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE share_type=? AND token=?" + if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { if err == sql.ErrNoRows { return nil, errtypes.NotFound(token) } diff --git a/pkg/cbox/utils/conversions.go b/pkg/cbox/utils/conversions.go index 0c3262b5a1..9a63109b3b 100644 --- a/pkg/cbox/utils/conversions.go +++ b/pkg/cbox/utils/conversions.go @@ -36,23 +36,25 @@ import ( // DBShare stores information about user and public shares. type DBShare struct { - ID string - UIDOwner string - UIDInitiator string - Prefix string - ItemSource string - ItemType string - ShareWith string - Token string - Expiration string - Permissions int - ShareType int - ShareName string - STime int - FileTarget string - State int - Quicklink bool - Description string + ID string + UIDOwner string + UIDInitiator string + Prefix string + ItemSource string + ItemType string + ShareWith string + Token string + Expiration string + Permissions int + ShareType int + ShareName string + STime int + FileTarget string + State int + Quicklink bool + Description string + NotifyUploads bool + NotifyUploadsExtraRecipients string } // FormatGrantee formats a CS3API grantee to a string. @@ -292,16 +294,18 @@ func ConvertToCS3PublicShare(ctx context.Context, gateway gatewayv1beta1.Gateway StorageId: s.Prefix, OpaqueId: s.ItemSource, }, - Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)}, - Owner: owner, - Creator: creator, - Token: s.Token, - DisplayName: s.ShareName, - PasswordProtected: pwd, - Expiration: expires, - Ctime: ts, - Mtime: ts, - Quicklink: s.Quicklink, - Description: s.Description, + Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)}, + Owner: owner, + Creator: creator, + Token: s.Token, + DisplayName: s.ShareName, + PasswordProtected: pwd, + Expiration: expires, + Ctime: ts, + Mtime: ts, + Quicklink: s.Quicklink, + Description: s.Description, + NotifyUploads: s.NotifyUploads, + NotifyUploadsExtraRecipients: s.NotifyUploadsExtraRecipients, }, nil } diff --git a/pkg/notification/db_changes.sql b/pkg/notification/db_changes.sql index 5a4b92666b..7b7be6ad03 100644 --- a/pkg/notification/db_changes.sql +++ b/pkg/notification/db_changes.sql @@ -44,3 +44,11 @@ CREATE INDEX `cbox_notifications_ix0` ON `cbox_notifications` (`ref`); CREATE INDEX `cbox_notification_recipients_ix0` ON `cbox_notification_recipients` (`notification_id`); CREATE INDEX `cbox_notification_recipients_ix1` ON `cbox_notification_recipients` (`user_name`); + +-- changes for added notifications on ocm shares + +ALTER TABLE cernboxngcopy.oc_share ADD notify_uploads BOOL DEFAULT false; + +UPDATE cernboxngcopy.oc_share SET notify_uploads = false; + +ALTER TABLE cernboxngcopy.oc_share MODIFY notify_uploads BOOL DEFAULT false NOT NULL; diff --git a/pkg/publicshare/manager/json/json.go b/pkg/publicshare/manager/json/json.go index f8c9f009c2..11419b474f 100644 --- a/pkg/publicshare/manager/json/json.go +++ b/pkg/publicshare/manager/json/json.go @@ -138,7 +138,7 @@ func (m *manager) startJanitorRun() { } // CreatePublicShare adds a new entry to manager.shares. -func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) { +func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) { id := &link.PublicShareId{ OpaqueId: utils.RandString(15), } @@ -168,18 +168,20 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr } s := link.PublicShare{ - Id: id, - Owner: rInfo.GetOwner(), - Creator: u.Id, - ResourceId: rInfo.Id, - Token: tkn, - Permissions: g.Permissions, - Ctime: createdAt, - Mtime: createdAt, - PasswordProtected: passwordProtected, - Expiration: g.Expiration, - DisplayName: displayName, - Description: description, + Id: id, + Owner: rInfo.GetOwner(), + Creator: u.Id, + ResourceId: rInfo.Id, + Token: tkn, + Permissions: g.Permissions, + Ctime: createdAt, + Mtime: createdAt, + PasswordProtected: passwordProtected, + Expiration: g.Expiration, + DisplayName: displayName, + Description: description, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, } ps := &publicShare{ diff --git a/pkg/publicshare/manager/memory/memory.go b/pkg/publicshare/manager/memory/memory.go index 557992974a..8af0b16bcd 100644 --- a/pkg/publicshare/manager/memory/memory.go +++ b/pkg/publicshare/manager/memory/memory.go @@ -58,7 +58,7 @@ var ( ) // CreatePublicShare adds a new entry to manager.shares. -func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) { +func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) { id := &link.PublicShareId{ OpaqueId: randString(15), } @@ -86,18 +86,20 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr } s := link.PublicShare{ - Id: id, - Owner: rInfo.GetOwner(), - Creator: u.Id, - ResourceId: rInfo.Id, - Token: tkn, - Permissions: g.Permissions, - Ctime: createdAt, - Mtime: modifiedAt, - PasswordProtected: passwordProtected, - Expiration: g.Expiration, - DisplayName: displayName, - Description: description, + Id: id, + Owner: rInfo.GetOwner(), + Creator: u.Id, + ResourceId: rInfo.Id, + Token: tkn, + Permissions: g.Permissions, + Ctime: createdAt, + Mtime: modifiedAt, + PasswordProtected: passwordProtected, + Expiration: g.Expiration, + DisplayName: displayName, + Description: description, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, } m.shares.Store(s.Token, &s) diff --git a/pkg/publicshare/publicshare.go b/pkg/publicshare/publicshare.go index 39d81cfbb9..9add4e398f 100644 --- a/pkg/publicshare/publicshare.go +++ b/pkg/publicshare/publicshare.go @@ -35,7 +35,7 @@ import ( // Manager manipulates public shares. type Manager interface { - CreatePublicShare(ctx context.Context, u *user.User, md *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) + CreatePublicShare(ctx context.Context, u *user.User, md *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest, g *link.Grant) (*link.PublicShare, error) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) From 1fd8fb59f4ea6a26ff574edbe379100075211831 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 26 Apr 2023 15:08:19 +0200 Subject: [PATCH 5/8] Add share creation notification --- .../ocs/handlers/apps/sharing/shares/group.go | 12 +- .../handlers/apps/sharing/shares/shares.go | 136 +++++++++++++++++- .../ocs/handlers/apps/sharing/shares/user.go | 12 +- internal/http/services/owncloud/ocs/ocs.go | 6 +- 4 files changed, 153 insertions(+), 13 deletions(-) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go index c1cc10bba3..8041595938 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go @@ -20,6 +20,7 @@ package shares import ( "net/http" + "strconv" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -28,6 +29,7 @@ import ( types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" ) @@ -80,5 +82,13 @@ func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statI }, } - h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) + if shareID, ok := h.createCs3Share(ctx, w, r, c, createShareReq, statInfo); ok { + notify, _ := strconv.ParseBool(r.FormValue("notify")) + if notify { + granter, ok := ctxpkg.ContextGetUser(ctx) + if ok { + h.SendShareNotification(shareID.OpaqueId, granter, groupRes.Group, statInfo) + } + } + } } diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index b2390d0273..106aa7c9af 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -26,6 +26,7 @@ import ( "mime" "net/http" "path" + "path/filepath" "strconv" "strings" "text/template" @@ -44,7 +45,10 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/notification" "github.com/cs3org/reva/pkg/notification/notificationhelper" + "github.com/cs3org/reva/pkg/notification/trigger" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/share" @@ -238,6 +242,125 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { } } +// NotifyShare handles GET requests on /apps/files_sharing/api/v1/shares/(shareid)/notify. +func (h *Handler) NotifyShare(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + opaqueID := chi.URLParam(r, "shareid") + + c, err := pool.GetGatewayServiceClient(pool.Endpoint(h.gatewayAddr)) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) + return + } + + shareRes, err := c.GetShare(ctx, &collaboration.GetShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: opaqueID, + }, + }, + }, + }) + if err != nil || shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + h.Log.Error().Err(err).Msg("error getting share") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting share", err) + return + } + + granter, ok := ctxpkg.ContextGetUser(ctx) + if !ok { + h.Log.Error().Err(err).Msgf("error getting granter data") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting granter data", err) + } + + resourceID := shareRes.Share.ResourceId + statInfo, status, err := h.getResourceInfoByID(ctx, c, resourceID) + if err != nil || status.Code != rpc.Code_CODE_OK { + h.Log.Error().Err(err).Msg("error mapping share data") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) + return + } + + var recipient string + + granteeType := shareRes.Share.Grantee.Type + if granteeType == provider.GranteeType_GRANTEE_TYPE_USER { + granteeID := shareRes.Share.Grantee.GetUserId().OpaqueId + granteeRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ + Claim: "username", + Value: granteeID, + SkipFetchingUserGroups: true, + }) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grantee data", err) + return + } + + recipient = h.SendShareNotification(opaqueID, granter, granteeRes.User, statInfo) + } else if granteeType == provider.GranteeType_GRANTEE_TYPE_GROUP { + granteeID := shareRes.Share.Grantee.GetGroupId().OpaqueId + granteeRes, err := c.GetGroupByClaim(ctx, &grouppb.GetGroupByClaimRequest{ + Claim: "group_name", + Value: granteeID, + SkipFetchingMembers: true, + }) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grantee data", err) + return + } + + recipient = h.SendShareNotification(opaqueID, granter, granteeRes.Group, statInfo) + } + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + rb, _ := json.Marshal(map[string]interface{}{"recipients": []string{recipient}}) + _, err = w.Write(rb) + if err != nil { + h.Log.Error().Err(err).Msg("error writing response") + } +} + +// SendShareNotification sends a notification with information from a Share. +func (h *Handler) SendShareNotification(opaqueID string, granter *userpb.User, grantee interface{}, statInfo *provider.ResourceInfo) string { + var granteeDisplayName, granteeName, recipient string + isGranteeGroup := false + + if u, ok := grantee.(*userpb.User); ok { + granteeDisplayName = u.DisplayName + granteeName = u.Username + recipient = u.Mail + } else if g, ok := grantee.(*grouppb.Group); ok { + granteeDisplayName = g.DisplayName + granteeName = g.GroupName + recipient = g.Mail + isGranteeGroup = true + } + + h.notificationHelper.TriggerNotification(&trigger.Trigger{ + Notification: ¬ification.Notification{ + TemplateName: "share-create-mail", + Ref: opaqueID, + Recipients: []string{recipient}, + }, + Ref: opaqueID, + TemplateData: map[string]interface{}{ + "granteeDisplayName": granteeDisplayName, + "granteeUserName": granteeName, + "granterDisplayName": granter.DisplayName, + "granterUserName": granter.Username, + "path": statInfo.Path, + "isFolder": statInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER, + "isGranteeGroup": isGranteeGroup, + "base": filepath.Base(statInfo.Path), + }, + }) + h.Log.Debug().Msgf("notification trigger %s created", opaqueID) + + return recipient +} + func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri *provider.ResourceInfo, defaultPermissions *conversions.Role) (*conversions.Role, []byte, error) { reqRole, reqPermissions := r.FormValue("role"), r.FormValue("permissions") var role *conversions.Role @@ -1107,33 +1230,34 @@ func (h *Handler) getResourceInfo(ctx context.Context, client gateway.GatewayAPI return pinfo, status, nil } -func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client gateway.GatewayAPIClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) { +func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client gateway.GatewayAPIClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) (*collaboration.ShareId, bool) { createShareResponse, err := client.CreateShare(ctx, req) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create share request", err) - return + return nil, false } if createShareResponse.Status.Code != rpc.Code_CODE_OK { if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) - return + return nil, false } response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create share request failed", err) - return + return nil, false } s, err := conversions.CS3Share2ShareData(ctx, createShareResponse.Share) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) - return + return nil, false } err = h.addFileInfo(ctx, s, info) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) - return + return nil, false } h.mapUserIds(ctx, client, s) response.WriteOCSSuccess(w, r, s) + return createShareResponse.Share.Id, true } func mapState(state collaboration.ShareState) int { diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index 620a7bdaf7..ba7acc601f 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go @@ -20,6 +20,7 @@ package shares import ( "net/http" + "strconv" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -29,6 +30,7 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" ) @@ -82,7 +84,15 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statIn }, } - h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) + if shareID, ok := h.createCs3Share(ctx, w, r, c, createShareReq, statInfo); ok { + notify, _ := strconv.ParseBool(r.FormValue("notify")) + if notify { + granter, ok := ctxpkg.ContextGetUser(ctx) + if ok { + h.SendShareNotification(shareID.OpaqueId, granter, userRes.User, statInfo) + } + } + } } func (h *Handler) isUserShare(r *http.Request, oid string) bool { diff --git a/internal/http/services/owncloud/ocs/ocs.go b/internal/http/services/owncloud/ocs/ocs.go index a55795e5cc..5aef6c9ffb 100644 --- a/internal/http/services/owncloud/ocs/ocs.go +++ b/internal/http/services/owncloud/ocs/ocs.go @@ -119,16 +119,12 @@ func (s *svc) routerInit(l *zerolog.Logger) error { }) r.Get("/{shareid}", sharesHandler.GetShare) r.Put("/{shareid}", sharesHandler.UpdateShare) + r.Get("/{shareid}/notify", sharesHandler.NotifyShare) r.Delete("/{shareid}", sharesHandler.RemoveShare) }) r.Get("/sharees", shareesHandler.FindSharees) }) - // placeholder for notifications - r.Get("/apps/notifications/api/v1/notifications", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - r.Get("/config", configHandler.GetConfig) r.Route("/cloud", func(r chi.Router) { From 78dfa83e1b87766ad7c862d508b565a00ec6bb03 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 26 Apr 2023 17:11:07 +0200 Subject: [PATCH 6/8] Remove old mailer service --- internal/http/services/loader/loader.go | 1 - internal/http/services/mailer/mailer.go | 378 ------------------------ 2 files changed, 379 deletions(-) delete mode 100644 internal/http/services/mailer/mailer.go diff --git a/internal/http/services/loader/loader.go b/internal/http/services/loader/loader.go index 693264c157..adf1ad02da 100644 --- a/internal/http/services/loader/loader.go +++ b/internal/http/services/loader/loader.go @@ -25,7 +25,6 @@ import ( _ "github.com/cs3org/reva/internal/http/services/datagateway" _ "github.com/cs3org/reva/internal/http/services/dataprovider" _ "github.com/cs3org/reva/internal/http/services/helloworld" - _ "github.com/cs3org/reva/internal/http/services/mailer" _ "github.com/cs3org/reva/internal/http/services/mentix" _ "github.com/cs3org/reva/internal/http/services/meshdirectory" _ "github.com/cs3org/reva/internal/http/services/metrics" diff --git a/internal/http/services/mailer/mailer.go b/internal/http/services/mailer/mailer.go deleted file mode 100644 index 6d9c23c9c5..0000000000 --- a/internal/http/services/mailer/mailer.go +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package mailer - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/smtp" - "os" - "path/filepath" - "strings" - "text/template" - - 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" - collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - ctxpkg "github.com/cs3org/reva/pkg/ctx" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/pkg/rhttp/global" - "github.com/cs3org/reva/pkg/sharedconf" - "github.com/mitchellh/mapstructure" - "github.com/rs/zerolog" -) - -func init() { - global.Register("mailer", New) -} - -type config struct { - SMTPAddress string `mapstructure:"smtp_server" docs:";The hostname and port of the SMTP server."` - SenderLogin string `mapstructure:"sender_login" docs:";The email to be used to send mails."` - SenderPassword string `mapstructure:"sender_password" docs:";The sender's password."` - DisableAuth bool `mapstructure:"disable_auth" docs:"false;Whether to disable SMTP auth."` - Prefix string `mapstructure:"prefix"` - BodyTemplatePath string `mapstructure:"body_template_path"` - SubjectTemplate string `mapstructure:"subject_template"` - GatewaySVC string `mapstructure:"gateway_svc"` -} - -type svc struct { - conf *config - client gateway.GatewayAPIClient - tplBody *template.Template - tplSubj *template.Template -} - -// New creates a new mailer service. -func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { - conf := &config{} - if err := mapstructure.Decode(m, conf); err != nil { - return nil, err - } - - conf.init() - - client, err := pool.GetGatewayServiceClient(pool.Endpoint(conf.GatewaySVC)) - if err != nil { - return nil, err - } - - s := &svc{ - conf: conf, - client: client, - } - - if err = s.initBodyTemplate(); err != nil { - return nil, err - } - if err = s.initSubjectTemplate(); err != nil { - return nil, err - } - - return s, nil -} - -func (s *svc) Close() error { - return nil -} - -func (s *svc) initBodyTemplate() error { - f, err := os.Open(s.conf.BodyTemplatePath) - if err != nil { - return err - } - defer f.Close() - - data, err := io.ReadAll(f) - if err != nil { - return err - } - - tpl, err := template.New("tpl_body").Parse(string(data)) - if err != nil { - return err - } - - s.tplBody = tpl - return nil -} - -func (s *svc) initSubjectTemplate() error { - tpl, err := template.New("tpl_subj").Parse(s.conf.SubjectTemplate) - if err != nil { - return err - } - s.tplSubj = tpl - return nil -} - -func (c *config) init() { - if c.Prefix == "" { - c.Prefix = "mailer" - } - - if c.SubjectTemplate == "" { - c.SubjectTemplate = "{{.OwnerName}} ({{.OwnerUsername}}) shared {{if .IsDir}}folder{{else}}file{{end}} '{{.Filename}}' with you" - } - - c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) -} - -func (s *svc) Prefix() string { - return s.conf.Prefix -} - -func (s *svc) Unprotected() []string { - return nil -} - -type out struct { - Recipients []string `json:"recipients"` -} - -func getIDsFromRequest(r *http.Request) ([]string, error) { - if err := r.ParseForm(); err != nil { - return nil, err - } - - idsSet := make(map[string]struct{}) - - for _, id := range r.Form["id"] { - if _, ok := idsSet[id]; ok { - continue - } - idsSet[id] = struct{}{} - } - - ids := make([]string, 0, len(idsSet)) - for id := range idsSet { - ids = append(ids, id) - } - - return ids, nil -} - -func (s *svc) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - ctx := r.Context() - - ids, err := getIDsFromRequest(r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - - if len(ids) == 0 { - http.Error(w, "share id not provided", http.StatusBadRequest) - return - } - - var recipients []string - for _, id := range ids { - recipient, err := s.sendMailForShare(ctx, id) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - recipients = append(recipients, recipient) - } - - w.WriteHeader(http.StatusOK) - w.Header().Add("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(out{Recipients: recipients}) - }) -} - -type shareInfo struct { - RecipientEmail string - RecipientUsername string - OwnerEmail string - OwnerName string - OwnerUsername string - ShareType string - Filename string - Path string - IsDir bool - ShareID string -} - -func (s *svc) getAuth() smtp.Auth { - if s.conf.DisableAuth { - return nil - } - return smtp.PlainAuth("", s.conf.SenderLogin, s.conf.SenderPassword, strings.SplitN(s.conf.SMTPAddress, ":", 2)[0]) -} - -func (s *svc) sendMailForShare(ctx context.Context, id string) (string, error) { - share, err := s.getShareInfoByID(ctx, id) - if err != nil { - return "", err - } - - msg, err := s.generateMsg(share.OwnerEmail, share.RecipientEmail, share) - if err != nil { - return "", err - } - - return share.RecipientEmail, smtp.SendMail(s.conf.SMTPAddress, s.getAuth(), share.OwnerEmail, []string{share.RecipientEmail}, msg) -} - -func (s *svc) generateMsg(from, to string, share *shareInfo) ([]byte, error) { - subj, err := s.generateEmailSubject(share) - if err != nil { - return nil, err - } - - body, err := s.generateEmailBody(share) - if err != nil { - return nil, err - } - - msg := fmt.Sprintf("From: %s\r\n"+ - "To: %s\r\n"+ - "Subject: %s\r\n\r\n%s\r\n", from, to, subj, body) - return []byte(msg), nil -} - -func (s *svc) getShareInfoByID(ctx context.Context, id string) (*shareInfo, error) { - user, ok := ctxpkg.ContextGetUser(ctx) - if !ok { - return nil, errtypes.UserRequired("user not in context") - } - - shareRes, err := s.client.GetShare(ctx, &collaboration.GetShareRequest{ - Ref: &collaboration.ShareReference{ - Spec: &collaboration.ShareReference_Id{ - Id: &collaboration.ShareId{ - OpaqueId: id, - }, - }, - }, - }) - - switch { - case err != nil: - return nil, err - case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND: - return nil, errtypes.NotFound(fmt.Sprintf("share %s not found", id)) - case shareRes.Status.Code != rpc.Code_CODE_OK: - return nil, errtypes.InternalError(shareRes.Status.Message) - } - - share := shareRes.Share - statRes, err := s.client.Stat(ctx, &provider.StatRequest{ - Ref: &provider.Reference{ - ResourceId: share.ResourceId, - }, - }) - - switch { - case err != nil: - return nil, err - case statRes.Status.Code == rpc.Code_CODE_NOT_FOUND: - return nil, errtypes.NotFound("reference not found") - case statRes.Status.Code != rpc.Code_CODE_OK: - return nil, errtypes.InternalError(statRes.Status.Message) - } - - file := statRes.Info - - info := &shareInfo{} - switch g := share.Grantee.Id.(type) { - case *provider.Grantee_UserId: - grantee, err := s.getUser(ctx, g.UserId) - if err != nil { - return nil, err - } - info.RecipientEmail = grantee.Mail - info.RecipientUsername = grantee.Username - info.ShareType = "user" - case *provider.Grantee_GroupId: - grantee, err := s.getGroup(ctx, g.GroupId) - if err != nil { - return nil, err - } - info.RecipientEmail = grantee.Mail - info.RecipientUsername = grantee.GroupName - info.ShareType = "group" - } - - info.OwnerEmail = user.Mail - info.OwnerName = user.DisplayName - info.OwnerUsername = user.Username - - info.Path = file.Path - info.Filename = filepath.Base(file.Path) - if file.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - info.IsDir = true - } else { - info.IsDir = false - } - - info.ShareID = id - - return info, nil -} - -func (s *svc) getUser(ctx context.Context, userID *user.UserId) (*user.User, error) { - res, err := s.client.GetUser(ctx, &user.GetUserRequest{ - UserId: userID, - }) - if err != nil { - return nil, err - } - - return res.User, nil -} - -func (s *svc) getGroup(ctx context.Context, groupID *group.GroupId) (*group.Group, error) { - res, err := s.client.GetGroup(ctx, &group.GetGroupRequest{ - GroupId: groupID, - }) - if err != nil { - return nil, err - } - - return res.Group, nil -} - -func (s *svc) generateEmailSubject(share *shareInfo) (string, error) { - var buf bytes.Buffer - err := s.tplSubj.Execute(&buf, share) - return buf.String(), err -} - -func (s *svc) generateEmailBody(share *shareInfo) (string, error) { - var buf bytes.Buffer - err := s.tplBody.Execute(&buf, share) - return buf.String(), err -} From 7c7293f7aff927bdf6d98c68abce7a160fe99446 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 27 Apr 2023 17:31:24 +0200 Subject: [PATCH 7/8] Remove old notifications stub --- internal/http/services/ocmd/notifications.go | 34 -------------------- internal/http/services/ocmd/ocm.go | 3 -- 2 files changed, 37 deletions(-) delete mode 100644 internal/http/services/ocmd/notifications.go diff --git a/internal/http/services/ocmd/notifications.go b/internal/http/services/ocmd/notifications.go deleted file mode 100644 index 1e3f591298..0000000000 --- a/internal/http/services/ocmd/notifications.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package ocmd - -import ( - "net/http" -) - -type notificationsHandler struct { -} - -func (h *notificationsHandler) init(c *config) { -} - -// SendNotification is used to let the provider know that a user has removed a share. -func (h *notificationsHandler) SendNotification(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) -} diff --git a/internal/http/services/ocmd/ocm.go b/internal/http/services/ocmd/ocm.go index ab178796f6..fa92fb4121 100644 --- a/internal/http/services/ocmd/ocm.go +++ b/internal/http/services/ocmd/ocm.go @@ -75,19 +75,16 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) func (s *svc) routerInit() error { sharesHandler := new(sharesHandler) - notificationsHandler := new(notificationsHandler) invitesHandler := new(invitesHandler) if err := sharesHandler.init(s.Conf); err != nil { return err } - notificationsHandler.init(s.Conf) if err := invitesHandler.init(s.Conf); err != nil { return err } s.router.Post("/shares", sharesHandler.CreateShare) - s.router.Post("/notifications", notificationsHandler.SendNotification) s.router.Post("/invite-accepted", invitesHandler.AcceptInvite) return nil } From e1eb680a4ec9647c461a7916ae75bd3c5e6db1bc Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Fri, 12 May 2023 10:04:31 +0200 Subject: [PATCH 8/8] Changelog --- changelog/unreleased/notifications-framework.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 changelog/unreleased/notifications-framework.md diff --git a/changelog/unreleased/notifications-framework.md b/changelog/unreleased/notifications-framework.md new file mode 100644 index 0000000000..83ccd26118 --- /dev/null +++ b/changelog/unreleased/notifications-framework.md @@ -0,0 +1,13 @@ +Enhancement: Notifications framework + +Adds a notifications framework to Reva. + +The new notifications service communicates with the rest of +reva using NATS. It provides helper functions to register new +notifications and to send them. + +Notification templates are provided in the configuration files +for each service, and they are registered into the notifications +service on initialization. + +https://github.com/cs3org/reva/pull/3825