2021-03-23 18:48:50 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-03-24 09:57:50 +00:00
|
|
|
"context"
|
2021-05-18 08:12:51 +00:00
|
|
|
"fmt"
|
2021-03-23 18:48:50 +00:00
|
|
|
|
2021-03-24 09:57:50 +00:00
|
|
|
v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation"
|
|
|
|
v2reputationgrpc "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc"
|
2021-04-29 06:43:15 +00:00
|
|
|
"github.com/nspcc-dev/neofs-api-go/v2/session"
|
2021-03-23 18:54:00 +00:00
|
|
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
2021-04-23 15:02:55 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/cmd/neofs-node/reputation/common"
|
2021-04-21 04:56:38 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/cmd/neofs-node/reputation/intermediate"
|
2021-04-23 15:02:55 +00:00
|
|
|
intermediatereputation "github.com/nspcc-dev/neofs-node/cmd/neofs-node/reputation/intermediate"
|
2021-04-18 08:51:49 +00:00
|
|
|
localreputation "github.com/nspcc-dev/neofs-node/cmd/neofs-node/reputation/local"
|
2021-04-29 06:43:15 +00:00
|
|
|
rtpwrapper "github.com/nspcc-dev/neofs-node/pkg/morph/client/reputation/wrapper"
|
2021-03-23 18:54:00 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/morph/event"
|
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/morph/event/netmap"
|
2021-04-03 08:07:11 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/network/cache"
|
2021-03-24 09:57:50 +00:00
|
|
|
grpcreputation "github.com/nspcc-dev/neofs-node/pkg/network/transport/reputation/grpc"
|
2021-03-23 18:48:50 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/services/reputation"
|
2021-04-17 17:13:29 +00:00
|
|
|
reputationcommon "github.com/nspcc-dev/neofs-node/pkg/services/reputation/common"
|
2021-04-17 19:17:30 +00:00
|
|
|
reputationrouter "github.com/nspcc-dev/neofs-node/pkg/services/reputation/common/router"
|
2021-04-29 06:43:15 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/services/reputation/eigentrust"
|
|
|
|
eigentrustcalc "github.com/nspcc-dev/neofs-node/pkg/services/reputation/eigentrust/calculator"
|
|
|
|
eigentrustctrl "github.com/nspcc-dev/neofs-node/pkg/services/reputation/eigentrust/controller"
|
2021-04-23 15:02:55 +00:00
|
|
|
intermediateroutes "github.com/nspcc-dev/neofs-node/pkg/services/reputation/eigentrust/routes"
|
|
|
|
consumerstorage "github.com/nspcc-dev/neofs-node/pkg/services/reputation/eigentrust/storage/consumers"
|
2021-04-21 04:56:38 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/services/reputation/eigentrust/storage/daughters"
|
2021-04-29 06:43:15 +00:00
|
|
|
localtrustcontroller "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/controller"
|
2021-04-23 15:02:55 +00:00
|
|
|
localroutes "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/routes"
|
2021-03-23 18:48:50 +00:00
|
|
|
truststorage "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/storage"
|
2021-03-24 09:57:50 +00:00
|
|
|
reputationrpc "github.com/nspcc-dev/neofs-node/pkg/services/reputation/rpc"
|
2021-03-23 18:48:50 +00:00
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
|
2021-03-23 18:51:31 +00:00
|
|
|
"go.uber.org/zap"
|
2021-03-23 18:48:50 +00:00
|
|
|
)
|
|
|
|
|
2021-03-23 18:54:00 +00:00
|
|
|
func initReputationService(c *cfg) {
|
2021-05-21 11:02:46 +00:00
|
|
|
wrap, err := rtpwrapper.NewFromMorph(c.cfgMorph.client, c.cfgReputation.scriptHash, 0)
|
2021-04-29 06:43:15 +00:00
|
|
|
fatalOnErr(err)
|
|
|
|
|
|
|
|
localKey := crypto.MarshalPublicKey(&c.key.PublicKey)
|
|
|
|
|
2021-03-23 18:54:00 +00:00
|
|
|
// consider sharing this between application components
|
|
|
|
nmSrc := newCachedNetmapStorage(c.cfgNetmap.state, c.cfgNetmap.wrapper)
|
|
|
|
|
2021-04-21 04:56:38 +00:00
|
|
|
// storing calculated trusts as a daughter
|
2021-04-29 06:43:15 +00:00
|
|
|
c.cfgReputation.localTrustStorage = truststorage.New(
|
2021-05-04 06:16:51 +00:00
|
|
|
truststorage.Prm{},
|
2021-04-29 06:43:15 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
daughterStorage := daughters.New(daughters.Prm{})
|
|
|
|
consumerStorage := consumerstorage.New(consumerstorage.Prm{})
|
2021-03-23 18:54:00 +00:00
|
|
|
|
2021-04-23 15:02:55 +00:00
|
|
|
// storing received daughter(of current node) trusts as a manager
|
|
|
|
daughterStorageWriterProvider := &intermediate.DaughterStorageWriterProvider{
|
2021-04-21 04:56:38 +00:00
|
|
|
Log: c.log,
|
2021-04-29 06:43:15 +00:00
|
|
|
Storage: daughterStorage,
|
2021-04-21 04:56:38 +00:00
|
|
|
}
|
|
|
|
|
2021-04-23 15:02:55 +00:00
|
|
|
consumerStorageWriterProvider := &intermediate.ConsumerStorageWriterProvider{
|
|
|
|
Log: c.log,
|
2021-04-29 06:43:15 +00:00
|
|
|
Storage: consumerStorage,
|
2021-04-23 15:02:55 +00:00
|
|
|
}
|
|
|
|
|
2021-04-29 06:43:15 +00:00
|
|
|
localTrustStorage := &localreputation.TrustStorage{
|
2021-04-18 08:51:49 +00:00
|
|
|
Log: c.log,
|
|
|
|
Storage: c.cfgReputation.localTrustStorage,
|
|
|
|
NmSrc: nmSrc,
|
2021-04-29 06:43:15 +00:00
|
|
|
LocalKey: localKey,
|
2021-03-23 18:54:00 +00:00
|
|
|
}
|
|
|
|
|
2021-04-23 15:02:55 +00:00
|
|
|
managerBuilder := common.NewManagerBuilder(
|
|
|
|
common.ManagersPrm{
|
2021-04-18 08:51:49 +00:00
|
|
|
NetMapSource: nmSrc,
|
|
|
|
},
|
2021-04-23 15:02:55 +00:00
|
|
|
common.WithLogger(c.log),
|
2021-04-18 08:51:49 +00:00
|
|
|
)
|
2021-04-03 08:07:11 +00:00
|
|
|
|
2021-04-23 15:02:55 +00:00
|
|
|
localRouteBuilder := localroutes.New(
|
|
|
|
localroutes.Prm{
|
|
|
|
ManagerBuilder: managerBuilder,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
intermediateRouteBuilder := intermediateroutes.New(
|
|
|
|
intermediateroutes.Prm{
|
|
|
|
ManagerBuilder: managerBuilder,
|
|
|
|
},
|
|
|
|
)
|
2021-04-03 08:07:11 +00:00
|
|
|
|
2021-04-23 15:02:55 +00:00
|
|
|
apiClientCache := cache.NewSDKClientCache()
|
|
|
|
|
|
|
|
remoteLocalTrustProvider := common.NewRemoteTrustProvider(
|
|
|
|
common.RemoteProviderPrm{
|
2021-04-18 08:51:49 +00:00
|
|
|
LocalAddrSrc: c,
|
2021-04-23 15:02:55 +00:00
|
|
|
DeadEndProvider: daughterStorageWriterProvider,
|
|
|
|
ClientCache: apiClientCache,
|
|
|
|
WriterProvider: localreputation.NewRemoteProvider(
|
|
|
|
localreputation.RemoteProviderPrm{
|
|
|
|
Key: c.key,
|
|
|
|
},
|
|
|
|
),
|
2021-04-18 08:51:49 +00:00
|
|
|
},
|
|
|
|
)
|
2021-04-03 08:07:11 +00:00
|
|
|
|
2021-04-23 15:02:55 +00:00
|
|
|
remoteIntermediateTrustProvider := common.NewRemoteTrustProvider(
|
|
|
|
common.RemoteProviderPrm{
|
|
|
|
LocalAddrSrc: c,
|
|
|
|
DeadEndProvider: consumerStorageWriterProvider,
|
|
|
|
ClientCache: apiClientCache,
|
|
|
|
WriterProvider: intermediatereputation.NewRemoteProvider(
|
|
|
|
intermediatereputation.RemoteProviderPrm{
|
|
|
|
Key: c.key,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
localTrustRouter := reputationrouter.New(
|
2021-04-17 19:17:30 +00:00
|
|
|
reputationrouter.Prm{
|
2021-04-03 08:07:11 +00:00
|
|
|
LocalServerInfo: c,
|
|
|
|
RemoteWriterProvider: remoteLocalTrustProvider,
|
2021-04-23 15:02:55 +00:00
|
|
|
Builder: localRouteBuilder,
|
|
|
|
},
|
|
|
|
)
|
2021-04-03 08:07:11 +00:00
|
|
|
|
2021-04-29 06:43:15 +00:00
|
|
|
intermediateTrustRouter := reputationrouter.New(
|
2021-04-23 15:02:55 +00:00
|
|
|
reputationrouter.Prm{
|
|
|
|
LocalServerInfo: c,
|
|
|
|
RemoteWriterProvider: remoteIntermediateTrustProvider,
|
|
|
|
Builder: intermediateRouteBuilder,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2021-04-29 06:43:15 +00:00
|
|
|
eigenTrustCalculator := eigentrustcalc.New(
|
|
|
|
eigentrustcalc.Prm{
|
2021-05-05 21:03:07 +00:00
|
|
|
AlphaProvider: c.cfgNetmap.wrapper,
|
2021-05-04 06:16:51 +00:00
|
|
|
InitialTrustSource: intermediatereputation.InitialTrustSource{
|
2021-05-04 10:10:41 +00:00
|
|
|
NetMap: nmSrc,
|
2021-05-04 06:16:51 +00:00
|
|
|
},
|
2021-04-29 06:43:15 +00:00
|
|
|
IntermediateValueTarget: intermediateTrustRouter,
|
|
|
|
WorkerPool: c.cfgReputation.workerPool,
|
|
|
|
FinalResultTarget: intermediate.NewFinalWriterProvider(
|
|
|
|
intermediate.FinalWriterProviderPrm{
|
|
|
|
PrivatKey: c.key,
|
|
|
|
PubKey: localKey,
|
|
|
|
Client: wrap,
|
|
|
|
},
|
|
|
|
intermediate.FinalWriterWithLogger(c.log),
|
|
|
|
),
|
|
|
|
DaughterTrustSource: &intermediate.DaughterTrustIteratorProvider{
|
|
|
|
DaughterStorage: daughterStorage,
|
|
|
|
ConsumerStorage: consumerStorage,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
eigentrustcalc.WithLogger(c.log),
|
|
|
|
)
|
|
|
|
|
|
|
|
eigenTrustController := eigentrustctrl.New(
|
|
|
|
eigentrustctrl.Prm{
|
|
|
|
DaughtersTrustCalculator: &intermediate.DaughtersTrustCalculator{
|
|
|
|
Calculator: eigenTrustCalculator,
|
|
|
|
},
|
|
|
|
IterationsProvider: c.cfgNetmap.wrapper,
|
|
|
|
WorkerPool: c.cfgReputation.workerPool,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
c.cfgReputation.localTrustCtrl = localtrustcontroller.New(
|
|
|
|
localtrustcontroller.Prm{
|
|
|
|
LocalTrustSource: localTrustStorage,
|
2021-04-23 15:02:55 +00:00
|
|
|
LocalTrustTarget: localTrustRouter,
|
|
|
|
},
|
|
|
|
)
|
2021-03-23 18:54:00 +00:00
|
|
|
|
2021-04-13 12:53:58 +00:00
|
|
|
addNewEpochAsyncNotificationHandler(
|
|
|
|
c,
|
|
|
|
func(ev event.Event) {
|
2021-04-29 06:43:15 +00:00
|
|
|
var reportPrm localtrustcontroller.ReportPrm
|
2021-03-23 18:54:00 +00:00
|
|
|
|
2021-04-13 12:53:58 +00:00
|
|
|
// report collected values from previous epoch
|
|
|
|
reportPrm.SetEpoch(ev.(netmap.NewEpoch).EpochNumber() - 1)
|
2021-03-23 18:54:00 +00:00
|
|
|
|
2021-04-13 12:53:58 +00:00
|
|
|
c.cfgReputation.localTrustCtrl.Report(reportPrm)
|
|
|
|
},
|
|
|
|
)
|
2021-03-24 09:57:50 +00:00
|
|
|
|
|
|
|
v2reputationgrpc.RegisterReputationServiceServer(c.cfgGRPC.server,
|
|
|
|
grpcreputation.New(
|
|
|
|
reputationrpc.NewSignService(
|
|
|
|
c.key,
|
|
|
|
reputationrpc.NewResponseService(
|
2021-04-06 17:15:09 +00:00
|
|
|
&reputationServer{
|
2021-04-29 06:43:15 +00:00
|
|
|
cfg: c,
|
|
|
|
log: c.log,
|
|
|
|
localRouter: localTrustRouter,
|
|
|
|
intermediateRouter: intermediateTrustRouter,
|
|
|
|
routeBuilder: localRouteBuilder,
|
2021-03-24 09:57:50 +00:00
|
|
|
},
|
|
|
|
c.respSvc,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
2021-04-15 14:57:29 +00:00
|
|
|
|
|
|
|
// initialize eigen trust block timer
|
|
|
|
durationMeter := NewEigenTrustDuration(c.cfgNetmap.wrapper)
|
|
|
|
|
|
|
|
newEigenTrustIterTimer(c, durationMeter, func() {
|
2021-04-29 06:43:15 +00:00
|
|
|
epoch, err := c.cfgNetmap.wrapper.Epoch()
|
|
|
|
if err != nil {
|
|
|
|
c.log.Debug(
|
|
|
|
"could not get current epoch",
|
|
|
|
zap.String("error", err.Error()),
|
|
|
|
)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
eigenTrustController.Continue(
|
|
|
|
eigentrustctrl.ContinuePrm{
|
|
|
|
Epoch: epoch - 1,
|
|
|
|
},
|
|
|
|
)
|
2021-04-15 14:57:29 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
addNewEpochAsyncNotificationHandler(
|
|
|
|
c,
|
|
|
|
func(e event.Event) {
|
|
|
|
durationMeter.Update() // recalculate duration of one iteration round
|
|
|
|
|
|
|
|
err := c.cfgMorph.eigenTrustTimer.Reset() // start iteration rounds again
|
|
|
|
if err != nil {
|
|
|
|
c.log.Warn("can't reset block timer to start eigen trust calculations again",
|
|
|
|
zap.String("error", err.Error()))
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
2021-03-24 09:57:50 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 17:15:09 +00:00
|
|
|
type reputationServer struct {
|
|
|
|
*cfg
|
2021-04-29 06:43:15 +00:00
|
|
|
log *logger.Logger
|
|
|
|
localRouter reputationcommon.WriterProvider
|
|
|
|
intermediateRouter reputationcommon.WriterProvider
|
|
|
|
routeBuilder reputationrouter.Builder
|
2021-04-06 17:15:09 +00:00
|
|
|
}
|
|
|
|
|
2021-05-07 11:13:11 +00:00
|
|
|
func (s *reputationServer) AnnounceLocalTrust(ctx context.Context, req *v2reputation.AnnounceLocalTrustRequest) (*v2reputation.AnnounceLocalTrustResponse, error) {
|
2021-04-29 06:43:15 +00:00
|
|
|
passedRoute := reverseRoute(req.GetVerificationHeader())
|
2021-04-06 17:15:09 +00:00
|
|
|
passedRoute = append(passedRoute, s)
|
|
|
|
|
2021-03-24 09:57:50 +00:00
|
|
|
body := req.GetBody()
|
|
|
|
|
2021-04-23 15:02:55 +00:00
|
|
|
eCtx := &common.EpochContext{
|
2021-04-06 17:15:09 +00:00
|
|
|
Context: ctx,
|
2021-04-18 08:51:49 +00:00
|
|
|
E: body.GetEpoch(),
|
2021-04-06 17:15:09 +00:00
|
|
|
}
|
|
|
|
|
2021-04-29 06:43:15 +00:00
|
|
|
w, err := s.localRouter.InitWriter(reputationrouter.NewRouteContext(eCtx, passedRoute))
|
2021-04-06 17:15:09 +00:00
|
|
|
if err != nil {
|
2021-05-18 08:12:51 +00:00
|
|
|
return nil, fmt.Errorf("could not initialize local trust writer: %w", err)
|
2021-04-06 17:15:09 +00:00
|
|
|
}
|
2021-03-24 09:57:50 +00:00
|
|
|
|
2021-04-06 17:15:09 +00:00
|
|
|
for _, trust := range body.GetTrusts() {
|
2021-04-29 06:43:15 +00:00
|
|
|
err = s.processLocalTrust(body.GetEpoch(), apiToLocalTrust(trust, passedRoute[0].PublicKey()), passedRoute, w)
|
2021-04-17 16:15:38 +00:00
|
|
|
if err != nil {
|
2021-05-18 08:12:51 +00:00
|
|
|
return nil, fmt.Errorf("could not write one of local trusts: %w", err)
|
2021-04-06 17:15:09 +00:00
|
|
|
}
|
2021-03-24 09:57:50 +00:00
|
|
|
}
|
|
|
|
|
2021-05-07 11:13:11 +00:00
|
|
|
resp := new(v2reputation.AnnounceLocalTrustResponse)
|
|
|
|
resp.SetBody(new(v2reputation.AnnounceLocalTrustResponseBody))
|
2021-03-24 09:57:50 +00:00
|
|
|
|
|
|
|
return resp, nil
|
2021-03-23 18:54:00 +00:00
|
|
|
}
|
2021-04-05 09:27:58 +00:00
|
|
|
|
2021-05-07 11:13:11 +00:00
|
|
|
func (s *reputationServer) AnnounceIntermediateResult(ctx context.Context, req *v2reputation.AnnounceIntermediateResultRequest) (*v2reputation.AnnounceIntermediateResultResponse, error) {
|
2021-04-29 06:43:15 +00:00
|
|
|
passedRoute := reverseRoute(req.GetVerificationHeader())
|
|
|
|
passedRoute = append(passedRoute, s)
|
|
|
|
|
|
|
|
body := req.GetBody()
|
2021-04-05 09:27:58 +00:00
|
|
|
|
2021-05-04 06:16:51 +00:00
|
|
|
eiCtx := eigentrust.NewIterContext(ctx, body.GetEpoch(), body.GetIteration())
|
2021-04-29 06:43:15 +00:00
|
|
|
|
2021-05-04 06:16:51 +00:00
|
|
|
w, err := s.intermediateRouter.InitWriter(reputationrouter.NewRouteContext(eiCtx, passedRoute))
|
2021-04-29 06:43:15 +00:00
|
|
|
if err != nil {
|
2021-05-18 08:12:51 +00:00
|
|
|
return nil, fmt.Errorf("could not initialize intermediate trust writer: %w", err)
|
2021-04-29 06:43:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
v2Trust := body.GetTrust()
|
|
|
|
|
2021-05-07 11:13:11 +00:00
|
|
|
trust := apiToLocalTrust(v2Trust.GetTrust(), v2Trust.GetTrustingPeer().GetPublicKey())
|
2021-04-29 06:43:15 +00:00
|
|
|
|
2021-05-04 06:16:51 +00:00
|
|
|
err = w.Write(trust)
|
2021-04-29 06:43:15 +00:00
|
|
|
if err != nil {
|
2021-05-18 08:12:51 +00:00
|
|
|
return nil, fmt.Errorf("could not write intermediate trust: %w", err)
|
2021-04-29 06:43:15 +00:00
|
|
|
}
|
|
|
|
|
2021-05-07 11:13:11 +00:00
|
|
|
resp := new(v2reputation.AnnounceIntermediateResultResponse)
|
|
|
|
resp.SetBody(new(v2reputation.AnnounceIntermediateResultResponseBody))
|
2021-04-05 09:27:58 +00:00
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
}
|
2021-04-06 17:15:09 +00:00
|
|
|
|
2021-04-29 06:43:15 +00:00
|
|
|
func (s *reputationServer) processLocalTrust(epoch uint64, t reputation.Trust,
|
|
|
|
passedRoute []reputationcommon.ServerInfo, w reputationcommon.Writer) error {
|
|
|
|
err := reputationrouter.CheckRoute(s.routeBuilder, epoch, t, passedRoute)
|
|
|
|
if err != nil {
|
2021-05-18 08:12:51 +00:00
|
|
|
return fmt.Errorf("wrong route of reputation trust value: %w", err)
|
2021-04-29 06:43:15 +00:00
|
|
|
}
|
|
|
|
|
2021-05-04 06:16:51 +00:00
|
|
|
return w.Write(t)
|
2021-04-29 06:43:15 +00:00
|
|
|
}
|
|
|
|
|
2021-04-17 16:15:38 +00:00
|
|
|
// apiToLocalTrust converts v2 Trust to local reputation.Trust, adding trustingPeer.
|
|
|
|
func apiToLocalTrust(t *v2reputation.Trust, trustingPeer []byte) reputation.Trust {
|
2021-04-06 17:15:09 +00:00
|
|
|
localTrust := reputation.Trust{}
|
|
|
|
|
|
|
|
localTrust.SetValue(reputation.TrustValueFromFloat64(t.GetValue()))
|
2021-05-07 11:13:11 +00:00
|
|
|
localTrust.SetPeer(reputation.PeerIDFromBytes(t.GetPeer().GetPublicKey()))
|
2021-04-17 16:15:38 +00:00
|
|
|
localTrust.SetTrustingPeer(reputation.PeerIDFromBytes(trustingPeer))
|
2021-04-06 17:15:09 +00:00
|
|
|
|
|
|
|
return localTrust
|
|
|
|
}
|
|
|
|
|
2021-04-29 06:43:15 +00:00
|
|
|
func reverseRoute(hdr *session.RequestVerificationHeader) (passedRoute []reputationcommon.ServerInfo) {
|
|
|
|
for hdr != nil {
|
|
|
|
passedRoute = append(passedRoute, &common.OnlyKeyRemoteServerInfo{
|
|
|
|
Key: hdr.GetBodySignature().GetKey(),
|
|
|
|
})
|
|
|
|
|
|
|
|
hdr = hdr.GetOrigin()
|
2021-04-06 17:15:09 +00:00
|
|
|
}
|
|
|
|
|
2021-04-29 06:43:15 +00:00
|
|
|
return
|
2021-04-06 17:15:09 +00:00
|
|
|
}
|