Debug Spread Get Peer
This commit is contained in:
58
Dockerfile
58
Dockerfile
@@ -1,45 +1,67 @@
|
|||||||
|
# ========================
|
||||||
|
# Global build arguments
|
||||||
|
# ========================
|
||||||
|
ARG CONF_NUM
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Dependencies stage
|
||||||
|
# ========================
|
||||||
FROM golang:alpine AS deps
|
FROM golang:alpine AS deps
|
||||||
|
ARG CONF_NUM
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN sed -i '/replace/d' go.mod
|
RUN sed -i '/replace/d' go.mod
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
#----------------------------------------------------------------------------------------------
|
# ========================
|
||||||
|
# Builder stage
|
||||||
|
# ========================
|
||||||
FROM golang:alpine AS builder
|
FROM golang:alpine AS builder
|
||||||
|
ARG CONF_NUM
|
||||||
|
|
||||||
WORKDIR /app
|
# Fail fast if CONF_NUM missing
|
||||||
|
RUN test -n "$CONF_NUM"
|
||||||
|
|
||||||
RUN apk add git
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
RUN go install github.com/beego/bee/v2@latest
|
|
||||||
|
|
||||||
WORKDIR /oc-discovery
|
WORKDIR /oc-discovery
|
||||||
|
|
||||||
|
# Reuse Go cache
|
||||||
COPY --from=deps /go/pkg /go/pkg
|
COPY --from=deps /go/pkg /go/pkg
|
||||||
COPY --from=deps /app/go.mod /app/go.sum ./
|
COPY --from=deps /app/go.mod /app/go.sum ./
|
||||||
|
|
||||||
RUN export CGO_ENABLED=0 && \
|
# App sources
|
||||||
export GOOS=linux && \
|
|
||||||
export GOARCH=amd64 && \
|
|
||||||
export BUILD_FLAGS="-ldflags='-w -s'"
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
# Clean replace directives again (safety)
|
||||||
RUN sed -i '/replace/d' go.mod
|
RUN sed -i '/replace/d' go.mod
|
||||||
|
|
||||||
|
# Build package
|
||||||
|
RUN go install github.com/beego/bee/v2@latest
|
||||||
RUN bee pack
|
RUN bee pack
|
||||||
RUN mkdir -p /app/extracted && tar -zxvf oc-discovery.tar.gz -C /app/extracted
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------------------------------
|
# Extract bundle
|
||||||
|
RUN mkdir -p /app/extracted \
|
||||||
|
&& tar -zxvf oc-discovery.tar.gz -C /app/extracted
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Runtime stage
|
||||||
|
# ========================
|
||||||
FROM golang:alpine
|
FROM golang:alpine
|
||||||
|
ARG CONF_NUM
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/extracted/oc-discovery /usr/bin/
|
|
||||||
COPY --from=builder /app/extracted/swagger /app/swagger
|
|
||||||
COPY --from=builder /app/extracted/docker_discovery.json /etc/oc/discovery.json
|
|
||||||
|
|
||||||
EXPOSE 8080
|
RUN mkdir ./pem
|
||||||
|
|
||||||
|
COPY --from=builder /app/extracted/pem/private${CONF_NUM}.pem ./pem/private.pem
|
||||||
|
COPY --from=builder /app/extracted/psk ./psk
|
||||||
|
COPY --from=builder /app/extracted/pem/public${CONF_NUM}.pem ./pem/public.pem
|
||||||
|
|
||||||
|
COPY --from=builder /app/extracted/oc-discovery /usr/bin/oc-discovery
|
||||||
|
COPY --from=builder /app/extracted/docker_discovery${CONF_NUM}.json /etc/oc/discovery.json
|
||||||
|
|
||||||
|
EXPOSE 400${CONF_NUM}
|
||||||
|
|
||||||
ENTRYPOINT ["oc-discovery"]
|
ENTRYPOINT ["oc-discovery"]
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -87,9 +88,11 @@ func (event *Event) Verify(p *peer.Peer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TopicNodeActivityPub struct {
|
type TopicNodeActivityPub struct {
|
||||||
DID string
|
|
||||||
PeerID string
|
|
||||||
NodeActivity peer.PeerState
|
NodeActivity peer.PeerState
|
||||||
|
Disposer pp.AddrInfo `json:"disposer_address"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DID string `json:"did"` // real PEER ID
|
||||||
|
PeerID string `json:"peer_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LongLivedPubSubService struct {
|
type LongLivedPubSubService struct {
|
||||||
@@ -119,7 +122,7 @@ func (s *LongLivedPubSubService) processEvent(
|
|||||||
const TopicPubSubNodeActivity = "oc-node-activity"
|
const TopicPubSubNodeActivity = "oc-node-activity"
|
||||||
const TopicPubSubSearch = "oc-node-search"
|
const TopicPubSubSearch = "oc-node-search"
|
||||||
|
|
||||||
func (s *LongLivedPubSubService) SubscribeToNodeActivity(ps *pubsub.PubSub) error {
|
func (s *LongLivedPubSubService) SubscribeToNodeActivity(ps *pubsub.PubSub, f *func(context.Context, TopicNodeActivityPub, string)) error {
|
||||||
ps.RegisterTopicValidator(TopicPubSubNodeActivity, func(ctx context.Context, p pp.ID, m *pubsub.Message) bool {
|
ps.RegisterTopicValidator(TopicPubSubNodeActivity, func(ctx context.Context, p pp.ID, m *pubsub.Message) bool {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -130,10 +133,13 @@ func (s *LongLivedPubSubService) SubscribeToNodeActivity(ps *pubsub.PubSub) erro
|
|||||||
defer s.PubsubMu.Unlock()
|
defer s.PubsubMu.Unlock()
|
||||||
s.LongLivedPubSubs[TopicPubSubNodeActivity] = topic
|
s.LongLivedPubSubs[TopicPubSubNodeActivity] = topic
|
||||||
}
|
}
|
||||||
|
if f != nil {
|
||||||
|
return SubscribeEvents(s, context.Background(), TopicPubSubNodeActivity, -1, *f)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LongLivedPubSubService) SubscribeToSearch(ps *pubsub.PubSub) error {
|
func (s *LongLivedPubSubService) SubscribeToSearch(ps *pubsub.PubSub, f *func(context.Context, Event, string)) error {
|
||||||
ps.RegisterTopicValidator(TopicPubSubSearch, func(ctx context.Context, p pp.ID, m *pubsub.Message) bool {
|
ps.RegisterTopicValidator(TopicPubSubSearch, func(ctx context.Context, p pp.ID, m *pubsub.Message) bool {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -144,5 +150,67 @@ func (s *LongLivedPubSubService) SubscribeToSearch(ps *pubsub.PubSub) error {
|
|||||||
defer s.PubsubMu.Unlock()
|
defer s.PubsubMu.Unlock()
|
||||||
s.LongLivedPubSubs[TopicPubSubSearch] = topic
|
s.LongLivedPubSubs[TopicPubSubSearch] = topic
|
||||||
}
|
}
|
||||||
|
if f != nil {
|
||||||
|
return SubscribeEvents(s, context.Background(), TopicPubSubSearch, -1, *f)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SubscribeEvents[T interface{}](s *LongLivedPubSubService,
|
||||||
|
ctx context.Context, proto string, timeout int, f func(context.Context, T, string),
|
||||||
|
) error {
|
||||||
|
s.PubsubMu.Lock()
|
||||||
|
if s.LongLivedPubSubs[proto] == nil {
|
||||||
|
s.PubsubMu.Unlock()
|
||||||
|
return errors.New("no protocol subscribed in pubsub")
|
||||||
|
}
|
||||||
|
topic := s.LongLivedPubSubs[proto]
|
||||||
|
s.PubsubMu.Unlock()
|
||||||
|
|
||||||
|
sub, err := topic.Subscribe() // then subscribe to it
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// launch loop waiting for results.
|
||||||
|
go waitResults[T](s, ctx, sub, proto, timeout, f)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitResults[T interface{}](s *LongLivedPubSubService, ctx context.Context, sub *pubsub.Subscription, proto string, timeout int, f func(context.Context, T, string)) {
|
||||||
|
defer ctx.Done()
|
||||||
|
for {
|
||||||
|
s.PubsubMu.Lock() // check safely if cache is actually notified subscribed to topic
|
||||||
|
if s.LongLivedPubSubs[proto] == nil { // if not kill the loop.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.PubsubMu.Unlock()
|
||||||
|
// if still subscribed -> wait for new message
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
if timeout != -1 {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
msg, err := sub.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
// timeout hit, no message before deadline kill subsciption.
|
||||||
|
s.PubsubMu.Lock()
|
||||||
|
delete(s.LongLivedPubSubs, proto)
|
||||||
|
s.PubsubMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var evt T
|
||||||
|
if err := json.Unmarshal(msg.Data, &evt); err != nil { // map to event
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f(ctx, evt, fmt.Sprintf("%v", proto))
|
||||||
|
/*if p, err := ps.Node.GetPeerRecord(ctx, evt.From); err == nil && len(p) > 0 {
|
||||||
|
if err := ps.processEvent(ctx, p[0], &evt, topicName); err != nil {
|
||||||
|
logger.Err(err)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ func (ix *LongLivedStreamRecordedService[T]) gc() {
|
|||||||
}
|
}
|
||||||
ix.PubsubMu.Lock()
|
ix.PubsubMu.Lock()
|
||||||
if ix.LongLivedPubSubs[TopicPubSubNodeActivity] != nil {
|
if ix.LongLivedPubSubs[TopicPubSubNodeActivity] != nil {
|
||||||
|
ad, err := pp.AddrInfoFromString("/ip4/" + conf.GetConfig().Hostname + " /tcp/" + fmt.Sprintf("%v", conf.GetConfig().NodeEndpointPort) + " /p2p/" + ix.Host.ID().String())
|
||||||
|
if err == nil {
|
||||||
if b, err := json.Marshal(TopicNodeActivityPub{
|
if b, err := json.Marshal(TopicNodeActivityPub{
|
||||||
|
Disposer: *ad,
|
||||||
|
Name: rec.HeartbeatStream.Name,
|
||||||
DID: rec.HeartbeatStream.DID,
|
DID: rec.HeartbeatStream.DID,
|
||||||
PeerID: pid.String(),
|
PeerID: pid.String(),
|
||||||
NodeActivity: peer.OFFLINE,
|
NodeActivity: peer.OFFLINE,
|
||||||
@@ -76,10 +80,10 @@ func (ix *LongLivedStreamRecordedService[T]) gc() {
|
|||||||
ix.LongLivedPubSubs[TopicPubSubNodeActivity].Publish(context.Background(), b)
|
ix.LongLivedPubSubs[TopicPubSubNodeActivity].Publish(context.Background(), b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ix.PubsubMu.Unlock()
|
ix.PubsubMu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ix *LongLivedStreamRecordedService[T]) Snapshot(interval time.Duration) {
|
func (ix *LongLivedStreamRecordedService[T]) Snapshot(interval time.Duration) {
|
||||||
@@ -150,6 +154,7 @@ func CheckHeartbeat(h host.Host, s network.Stream, maxNodes int) (*pp.ID, *Heart
|
|||||||
}
|
}
|
||||||
pid, err := pp.Decode(hb.PeerID)
|
pid, err := pp.Decode(hb.PeerID)
|
||||||
hb.Stream = &Stream{
|
hb.Stream = &Stream{
|
||||||
|
Name: hb.Name,
|
||||||
DID: hb.DID,
|
DID: hb.DID,
|
||||||
Stream: s,
|
Stream: s,
|
||||||
Expiry: time.Now().UTC().Add(2 * time.Minute),
|
Expiry: time.Now().UTC().Add(2 * time.Minute),
|
||||||
@@ -166,6 +171,7 @@ type StreamRecord[T interface{}] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
|
Name string `json:"name"`
|
||||||
DID string `json:"did"`
|
DID string `json:"did"`
|
||||||
Stream network.Stream
|
Stream network.Stream
|
||||||
Expiry time.Time `json:"expiry"`
|
Expiry time.Time `json:"expiry"`
|
||||||
@@ -261,7 +267,7 @@ func ConnectToIndexers(h host.Host, minIndexer int, maxIndexer int, myPID pp.ID)
|
|||||||
if len(StaticIndexers) < minIndexer {
|
if len(StaticIndexers) < minIndexer {
|
||||||
// TODO : ask for unknown indexer.
|
// TODO : ask for unknown indexer.
|
||||||
}
|
}
|
||||||
SendHeartbeat(ctx, ProtocolHeartbeat, h, StreamIndexers, StaticIndexers, 20*time.Second) // your indexer is just like a node for the next indexer.
|
SendHeartbeat(ctx, ProtocolHeartbeat, conf.GetConfig().Name, h, StreamIndexers, StaticIndexers, 20*time.Second) // your indexer is just like a node for the next indexer.
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddStreamProtocol(ctx *context.Context, protoS ProtocolStream, h host.Host, proto protocol.ID, id pp.ID, mypid pp.ID, force bool, onStreamCreated *func(network.Stream)) ProtocolStream {
|
func AddStreamProtocol(ctx *context.Context, protoS ProtocolStream, h host.Host, proto protocol.ID, id pp.ID, mypid pp.ID, force bool, onStreamCreated *func(network.Stream)) ProtocolStream {
|
||||||
@@ -299,6 +305,7 @@ func AddStreamProtocol(ctx *context.Context, protoS ProtocolStream, h host.Host,
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Heartbeat struct {
|
type Heartbeat struct {
|
||||||
|
Name string `json:"name"`
|
||||||
Stream *Stream `json:"stream"`
|
Stream *Stream `json:"stream"`
|
||||||
DID string `json:"did"`
|
DID string `json:"did"`
|
||||||
PeerID string `json:"peer_id"`
|
PeerID string `json:"peer_id"`
|
||||||
@@ -311,7 +318,7 @@ type HeartbeatInfo []struct {
|
|||||||
|
|
||||||
const ProtocolHeartbeat = "/opencloud/heartbeat/1.0"
|
const ProtocolHeartbeat = "/opencloud/heartbeat/1.0"
|
||||||
|
|
||||||
func SendHeartbeat(ctx context.Context, proto protocol.ID, h host.Host, ps ProtocolStream, peers []*pp.AddrInfo, interval time.Duration) {
|
func SendHeartbeat(ctx context.Context, proto protocol.ID, name string, h host.Host, ps ProtocolStream, peers []*pp.AddrInfo, interval time.Duration) {
|
||||||
peerID, err := oclib.GenerateNodeID()
|
peerID, err := oclib.GenerateNodeID()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
panic("can't heartbeat daemon failed to start")
|
panic("can't heartbeat daemon failed to start")
|
||||||
@@ -323,6 +330,7 @@ func SendHeartbeat(ctx context.Context, proto protocol.ID, h host.Host, ps Proto
|
|||||||
select {
|
select {
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
hb := Heartbeat{
|
hb := Heartbeat{
|
||||||
|
Name: name,
|
||||||
DID: peerID,
|
DID: peerID,
|
||||||
PeerID: h.ID().String(),
|
PeerID: h.ID().String(),
|
||||||
Timestamp: time.Now().UTC().Unix(),
|
Timestamp: time.Now().UTC().Unix(),
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DiscoveryPeer interface {
|
type DiscoveryPeer interface {
|
||||||
GetPeerRecord(ctx context.Context, key string) (*peer.Peer, error)
|
GetPeerRecord(ctx context.Context, key string) ([]*peer.Peer, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package indexer
|
package indexer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"oc-discovery/conf"
|
||||||
"oc-discovery/daemons/node/common"
|
"oc-discovery/daemons/node/common"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ type PeerRecord struct {
|
|||||||
ExpiryDate time.Time `json:"expiry_date"`
|
ExpiryDate time.Time `json:"expiry_date"`
|
||||||
|
|
||||||
TTL int `json:"ttl"` // max of hop diffusion
|
TTL int `json:"ttl"` // max of hop diffusion
|
||||||
|
NoPub bool `json:"no_pub"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PeerRecord) Sign() error {
|
func (p *PeerRecord) Sign() error {
|
||||||
@@ -121,7 +124,7 @@ type GetValue struct {
|
|||||||
|
|
||||||
type GetResponse struct {
|
type GetResponse struct {
|
||||||
Found bool `json:"found"`
|
Found bool `json:"found"`
|
||||||
Record PeerRecord `json:"record,omitempty"`
|
Records map[string]PeerRecord `json:"records,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ix *IndexerService) initNodeHandler() {
|
func (ix *IndexerService) initNodeHandler() {
|
||||||
@@ -178,7 +181,23 @@ func (ix *IndexerService) handleNodePublish(s network.Stream) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ix.LongLivedPubSubs[common.TopicPubSubNodeActivity] != nil && !rec.NoPub {
|
||||||
|
ad, err := peer.AddrInfoFromString("/ip4/" + conf.GetConfig().Hostname + " /tcp/" + fmt.Sprintf("%v", conf.GetConfig().NodeEndpointPort) + " /p2p/" + ix.Host.ID().String())
|
||||||
|
if err == nil {
|
||||||
|
if b, err := json.Marshal(common.TopicNodeActivityPub{
|
||||||
|
Disposer: *ad,
|
||||||
|
DID: rec.DID,
|
||||||
|
Name: rec.Name,
|
||||||
|
PeerID: pid.String(),
|
||||||
|
NodeActivity: pp.ONLINE,
|
||||||
|
}); err == nil {
|
||||||
|
ix.LongLivedPubSubs[common.TopicPubSubNodeActivity].Publish(context.Background(), b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if rec.TTL > 0 {
|
if rec.TTL > 0 {
|
||||||
|
rec.NoPub = true
|
||||||
for _, ad := range common.StaticIndexers {
|
for _, ad := range common.StaticIndexers {
|
||||||
if ad.ID == s.Conn().RemotePeer() {
|
if ad.ID == s.Conn().RemotePeer() {
|
||||||
continue
|
continue
|
||||||
@@ -211,56 +230,59 @@ func (ix *IndexerService) handleNodeGet(s network.Stream) {
|
|||||||
if ix.StreamRecords[common.ProtocolGet] == nil {
|
if ix.StreamRecords[common.ProtocolGet] == nil {
|
||||||
ix.StreamRecords[common.ProtocolGet] = map[peer.ID]*common.StreamRecord[PeerRecord]{}
|
ix.StreamRecords[common.ProtocolGet] = map[peer.ID]*common.StreamRecord[PeerRecord]{}
|
||||||
}
|
}
|
||||||
|
resp := GetResponse{
|
||||||
|
Found: false,
|
||||||
|
Records: map[string]PeerRecord{},
|
||||||
|
}
|
||||||
streams := ix.StreamRecords[common.ProtocolPublish]
|
streams := ix.StreamRecords[common.ProtocolPublish]
|
||||||
// simple lookup by PeerID (or DID)
|
// simple lookup by PeerID (or DID)
|
||||||
for _, rec := range streams {
|
for _, rec := range streams {
|
||||||
if rec.Record.DID == req.Key || rec.Record.PeerID == req.Key { // OK
|
if rec.Record.DID == req.Key || rec.Record.PeerID == req.Key || rec.Record.Name == req.Key { // OK
|
||||||
resp := GetResponse{
|
resp.Found = true
|
||||||
Found: true,
|
resp.Records[rec.Record.PeerID] = rec.Record
|
||||||
Record: rec.Record,
|
if rec.Record.DID == req.Key || rec.Record.PeerID == req.Key { // there unique... no need to proceed more...
|
||||||
}
|
|
||||||
_ = json.NewEncoder(s).Encode(resp)
|
_ = json.NewEncoder(s).Encode(resp)
|
||||||
break
|
ix.StreamMU.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if not found ask to my neighboor indexers
|
// if not found ask to my neighboor indexers
|
||||||
if common.StreamIndexers[common.ProtocolGet] == nil {
|
for pid, dsp := range ix.DisposedPeers {
|
||||||
_ = json.NewEncoder(s).Encode(GetResponse{Found: false})
|
if _, ok := resp.Records[dsp.PeerID]; !ok && (dsp.Name == req.Key || dsp.DID == req.Key || dsp.PeerID == req.Key) {
|
||||||
ix.StreamMU.Unlock()
|
ctxTTL, err := context.WithTimeout(context.Background(), 120*time.Second)
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, ad := range common.StaticIndexers {
|
if ix.Host.Network().Connectedness(pid) != network.Connected {
|
||||||
if ad.ID == s.Conn().RemotePeer() {
|
_ = ix.Host.Connect(ctxTTL, dsp.Disposer)
|
||||||
|
str, err := ix.Host.NewStream(ctxTTL, pid, common.ProtocolGet)
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.StreamIndexers[common.ProtocolGet][ad.ID] == nil {
|
for {
|
||||||
|
if ctxTTL.Err() == context.DeadlineExceeded {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var subResp GetResponse
|
||||||
|
if err := json.NewDecoder(str).Decode(&resp); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stream := common.StreamIndexers[common.ProtocolGet][ad.ID]
|
if subResp.Found {
|
||||||
if err := json.NewEncoder(stream.Stream).Encode(GetValue{Key: req.Key}); err != nil {
|
for k, v := range subResp.Records {
|
||||||
continue
|
if _, ok := resp.Records[k]; !ok {
|
||||||
|
resp.Records[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp GetResponse
|
|
||||||
if err := json.NewDecoder(stream.Stream).Decode(&resp); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if resp.Found {
|
|
||||||
for _, rec := range streams {
|
|
||||||
if rec.Record.DID == req.Key || rec.Record.PeerID == req.Key { // OK
|
|
||||||
resp := GetResponse{
|
|
||||||
Found: true,
|
|
||||||
Record: rec.Record,
|
|
||||||
}
|
|
||||||
_ = json.NewEncoder(s).Encode(resp)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not found
|
// Not found
|
||||||
_ = json.NewEncoder(s).Encode(GetResponse{Found: false})
|
_ = json.NewEncoder(s).Encode(resp)
|
||||||
ix.StreamMU.Unlock()
|
ix.StreamMU.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package indexer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"oc-discovery/daemons/node/common"
|
"oc-discovery/daemons/node/common"
|
||||||
|
"sync"
|
||||||
|
|
||||||
oclib "cloud.o-forge.io/core/oc-lib"
|
oclib "cloud.o-forge.io/core/oc-lib"
|
||||||
|
pp "cloud.o-forge.io/core/oc-lib/models/peer"
|
||||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||||
"github.com/libp2p/go-libp2p/core/host"
|
"github.com/libp2p/go-libp2p/core/host"
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Index Record is the model for the specialized registry of node connected to Indexer
|
// Index Record is the model for the specialized registry of node connected to Indexer
|
||||||
@@ -14,6 +17,8 @@ type IndexerService struct {
|
|||||||
*common.LongLivedStreamRecordedService[PeerRecord]
|
*common.LongLivedStreamRecordedService[PeerRecord]
|
||||||
PS *pubsub.PubSub
|
PS *pubsub.PubSub
|
||||||
isStrictIndexer bool
|
isStrictIndexer bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
DisposedPeers map[peer.ID]*common.TopicNodeActivityPub
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a pubsub is given... indexer is also an active oc-node. If not... your a strict indexer
|
// if a pubsub is given... indexer is also an active oc-node. If not... your a strict indexer
|
||||||
@@ -37,10 +42,21 @@ func NewIndexerService(h host.Host, ps *pubsub.PubSub, maxNode int) *IndexerServ
|
|||||||
logger.Info().Msg("connect to indexers as strict indexer...")
|
logger.Info().Msg("connect to indexers as strict indexer...")
|
||||||
common.ConnectToIndexers(h, 0, 5, ix.Host.ID()) // TODO : make var to change how many indexers are allowed.
|
common.ConnectToIndexers(h, 0, 5, ix.Host.ID()) // TODO : make var to change how many indexers are allowed.
|
||||||
logger.Info().Msg("subscribe to node activity as strict indexer...")
|
logger.Info().Msg("subscribe to node activity as strict indexer...")
|
||||||
ix.SubscribeToNodeActivity(ix.PS) // now we subscribe to a long run topic named node-activity, to relay message.
|
|
||||||
logger.Info().Msg("subscribe to decentralized search flow as strict indexer...")
|
logger.Info().Msg("subscribe to decentralized search flow as strict indexer...")
|
||||||
ix.SubscribeToSearch(ix.PS)
|
ix.SubscribeToSearch(ix.PS, nil)
|
||||||
}
|
}
|
||||||
|
f := func(ctx context.Context, evt common.TopicNodeActivityPub, _ string) {
|
||||||
|
ix.mu.Lock()
|
||||||
|
if evt.NodeActivity == pp.OFFLINE {
|
||||||
|
delete(ix.DisposedPeers, evt.Disposer.ID)
|
||||||
|
}
|
||||||
|
if evt.NodeActivity == pp.ONLINE {
|
||||||
|
ix.DisposedPeers[evt.Disposer.ID] = &evt
|
||||||
|
}
|
||||||
|
ix.mu.Unlock()
|
||||||
|
}
|
||||||
|
ix.SubscribeToNodeActivity(ix.PS, &f) // now we subscribe to a long run topic named node-activity, to relay message.
|
||||||
ix.initNodeHandler() // then listen up on every protocol expected
|
ix.initNodeHandler() // then listen up on every protocol expected
|
||||||
return ix
|
return ix
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ func InitNode(isNode bool, isIndexer bool) (*Node, error) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
logger.Info().Msg("subscribe to decentralized search flow...")
|
logger.Info().Msg("subscribe to decentralized search flow...")
|
||||||
node.SubscribeToSearch(node.PS)
|
|
||||||
logger.Info().Msg("run garbage collector...")
|
logger.Info().Msg("run garbage collector...")
|
||||||
node.StartGC(30 * time.Second)
|
node.StartGC(30 * time.Second)
|
||||||
|
|
||||||
@@ -90,6 +89,12 @@ func InitNode(isNode bool, isIndexer bool) (*Node, error) {
|
|||||||
if node.PubSubService, err = pubsub.InitPubSub(context.Background(), node.Host, node.PS, node, node.StreamService); err != nil {
|
if node.PubSubService, err = pubsub.InitPubSub(context.Background(), node.Host, node.PS, node, node.StreamService); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
f := func(ctx context.Context, evt common.Event, topic string) {
|
||||||
|
if p, err := node.GetPeerRecord(ctx, evt.From); err == nil && len(p) > 0 {
|
||||||
|
node.StreamService.SendResponse(p[0], &evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.SubscribeToSearch(node.PS, &f)
|
||||||
}
|
}
|
||||||
if isIndexer {
|
if isIndexer {
|
||||||
logger.Info().Msg("generate opencloud indexer...")
|
logger.Info().Msg("generate opencloud indexer...")
|
||||||
@@ -102,7 +107,7 @@ func InitNode(isNode bool, isIndexer bool) (*Node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Node) Close() {
|
func (d *Node) Close() {
|
||||||
if d.isIndexer {
|
if d.isIndexer && d.IndexerService != nil {
|
||||||
d.IndexerService.Close()
|
d.IndexerService.Close()
|
||||||
}
|
}
|
||||||
d.PubSubService.Close()
|
d.PubSubService.Close()
|
||||||
@@ -147,9 +152,9 @@ func (d *Node) publishPeerRecord(
|
|||||||
func (d *Node) GetPeerRecord(
|
func (d *Node) GetPeerRecord(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
key string,
|
key string,
|
||||||
) (*peer.Peer, error) {
|
) ([]*peer.Peer, error) {
|
||||||
var err error
|
var err error
|
||||||
var info *indexer.PeerRecord
|
var info map[string]indexer.PeerRecord
|
||||||
if common.StreamIndexers[common.ProtocolPublish] == nil {
|
if common.StreamIndexers[common.ProtocolPublish] == nil {
|
||||||
return nil, errors.New("no protocol Publish is set up on the node")
|
return nil, errors.New("no protocol Publish is set up on the node")
|
||||||
}
|
}
|
||||||
@@ -162,29 +167,32 @@ func (d *Node) GetPeerRecord(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
var resp indexer.GetResponse
|
var resp indexer.GetResponse
|
||||||
if err := json.NewDecoder(stream.Stream).Decode(&resp); err != nil {
|
if err := json.NewDecoder(stream.Stream).Decode(&resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Found {
|
if resp.Found {
|
||||||
info = &resp.Record
|
info = resp.Records
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var p *peer.Peer
|
}
|
||||||
if info != nil {
|
var ps []*peer.Peer
|
||||||
if pk, err := info.Verify(); err != nil {
|
for _, pr := range info {
|
||||||
|
if pk, err := pr.Verify(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if ok, p, err := info.ExtractPeer(d.PeerID.String(), key, pk); err != nil {
|
} else if ok, p, err := pr.ExtractPeer(d.PeerID.String(), key, pk); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
if ok {
|
if ok {
|
||||||
d.publishPeerRecord(info)
|
d.publishPeerRecord(&pr)
|
||||||
}
|
}
|
||||||
return p, nil
|
ps = append(ps, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p, err
|
|
||||||
|
return ps, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Node) claimInfo(
|
func (d *Node) claimInfo(
|
||||||
|
|||||||
@@ -23,14 +23,13 @@ func (ps *PubSubService) handleEventSearch( // only : on partner followings. 3 c
|
|||||||
if !(action == tools.PB_SEARCH_RESPONSE || action == tools.PB_SEARCH) {
|
if !(action == tools.PB_SEARCH_RESPONSE || action == tools.PB_SEARCH) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// TODO VERIFY: FROM SHOULD BE A PEER ID OR A DID
|
if p, err := ps.Node.GetPeerRecord(ctx, evt.From); err == nil && len(p) > 0 { // peerFrom is Unique
|
||||||
if p, err := ps.Node.GetPeerRecord(ctx, evt.From); err == nil {
|
if err := evt.Verify(p[0]); err != nil {
|
||||||
if err := evt.Verify(p); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch action {
|
switch action {
|
||||||
case tools.PB_SEARCH: // when someone ask for search.
|
case tools.PB_SEARCH: // when someone ask for search.
|
||||||
if err := ps.StreamService.SendResponse(p, evt); err != nil {
|
if err := ps.StreamService.SendResponse(p[0], evt); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PubSubService struct {
|
type PubSubService struct {
|
||||||
|
*common.LongLivedPubSubService
|
||||||
Node common.DiscoveryPeer
|
Node common.DiscoveryPeer
|
||||||
Host host.Host
|
Host host.Host
|
||||||
PS *pubsub.PubSub
|
PS *pubsub.PubSub
|
||||||
@@ -24,8 +25,8 @@ type PubSubService struct {
|
|||||||
|
|
||||||
func InitPubSub(ctx context.Context, h host.Host, ps *pubsub.PubSub, node common.DiscoveryPeer, streamService *stream.StreamService) (*PubSubService, error) {
|
func InitPubSub(ctx context.Context, h host.Host, ps *pubsub.PubSub, node common.DiscoveryPeer, streamService *stream.StreamService) (*PubSubService, error) {
|
||||||
service := &PubSubService{
|
service := &PubSubService{
|
||||||
|
LongLivedPubSubService: common.NewLongLivedPubSubService(h),
|
||||||
Node: node,
|
Node: node,
|
||||||
Host: h,
|
|
||||||
StreamService: streamService,
|
StreamService: streamService,
|
||||||
PS: ps,
|
PS: ps,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ package pubsub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"oc-discovery/daemons/node/common"
|
"oc-discovery/daemons/node/common"
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
oclib "cloud.o-forge.io/core/oc-lib"
|
oclib "cloud.o-forge.io/core/oc-lib"
|
||||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||||
"cloud.o-forge.io/core/oc-lib/tools"
|
"cloud.o-forge.io/core/oc-lib/tools"
|
||||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ps *PubSubService) initSubscribeEvents(ctx context.Context) error {
|
func (ps *PubSubService) initSubscribeEvents(ctx context.Context) error {
|
||||||
@@ -25,72 +20,20 @@ func (ps *PubSubService) initSubscribeEvents(ctx context.Context) error {
|
|||||||
func (ps *PubSubService) subscribeEvents(
|
func (ps *PubSubService) subscribeEvents(
|
||||||
ctx context.Context, dt *tools.DataType, action tools.PubSubAction, peerID string, timeout int,
|
ctx context.Context, dt *tools.DataType, action tools.PubSubAction, peerID string, timeout int,
|
||||||
) error {
|
) error {
|
||||||
|
logger := oclib.GetLogger()
|
||||||
// define a name app.action#peerID
|
// define a name app.action#peerID
|
||||||
name := action.String() + "#" + peerID
|
name := action.String() + "#" + peerID
|
||||||
if dt != nil { // if a datatype is precised then : app.action.datatype#peerID
|
if dt != nil { // if a datatype is precised then : app.action.datatype#peerID
|
||||||
name = action.String() + "." + (*dt).String() + "#" + peerID
|
name = action.String() + "." + (*dt).String() + "#" + peerID
|
||||||
}
|
}
|
||||||
topic, err := ps.PS.Join(name) // find out the topic
|
f := func(ctx context.Context, evt common.Event, topicName string) {
|
||||||
if err != nil {
|
if p, err := ps.Node.GetPeerRecord(ctx, evt.From); err == nil && len(p) > 0 {
|
||||||
return err
|
if err := ps.processEvent(ctx, p[0], &evt, topicName); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
sub, err := topic.Subscribe() // then subscribe to it
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ps.mutex.Lock() // add safely in cache your subscription.
|
|
||||||
ps.Subscription = append(ps.Subscription, name)
|
|
||||||
ps.mutex.Unlock()
|
|
||||||
|
|
||||||
// launch loop waiting for results.
|
|
||||||
go ps.waitResults(ctx, sub, name, timeout)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps *PubSubService) waitResults(ctx context.Context, sub *pubsub.Subscription, topicName string, timeout int) {
|
|
||||||
logger := oclib.GetLogger()
|
|
||||||
defer ctx.Done()
|
|
||||||
for {
|
|
||||||
ps.mutex.Lock() // check safely if cache is actually notified subscribed to topic
|
|
||||||
if !slices.Contains(ps.Subscription, topicName) { // if not kill the loop.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
ps.mutex.Unlock()
|
|
||||||
// if still subscribed -> wait for new message
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
if timeout != -1 {
|
|
||||||
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
msg, err := sub.Next(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
// timeout hit, no message before deadline kill subsciption.
|
|
||||||
ps.mutex.Lock()
|
|
||||||
subs := []string{}
|
|
||||||
for _, ss := range ps.Subscription {
|
|
||||||
if ss != topicName {
|
|
||||||
subs = append(subs, ss)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ps.Subscription = subs
|
|
||||||
ps.mutex.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var evt common.Event
|
|
||||||
if err := json.Unmarshal(msg.Data, &evt); err != nil { // map to event
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p, err := ps.Node.GetPeerRecord(ctx, evt.From); err == nil {
|
|
||||||
if err := ps.processEvent(ctx, p, &evt, topicName); err != nil {
|
|
||||||
logger.Err(err)
|
logger.Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return common.SubscribeEvents(ps.LongLivedPubSubService, ctx, name, -1, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *PubSubService) processEvent(
|
func (ps *PubSubService) processEvent(
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ func (ps *StreamService) handleEventFromPartner(evt *common.Event, action tools.
|
|||||||
p := peers.Data[0].(*peer.Peer)
|
p := peers.Data[0].(*peer.Peer)
|
||||||
// TODO : something if peer is missing in our side !
|
// TODO : something if peer is missing in our side !
|
||||||
ps.SendResponse(p, evt)
|
ps.SendResponse(p, evt)
|
||||||
} else if p, err := ps.Node.GetPeerRecord(context.Background(), evt.From); err == nil {
|
} else if p, err := ps.Node.GetPeerRecord(context.Background(), evt.From); err == nil && len(p) > 0 { // peer from is peerID
|
||||||
ps.SendResponse(p, evt)
|
ps.SendResponse(p[0], evt)
|
||||||
}
|
}
|
||||||
case tools.PB_CREATE:
|
case tools.PB_CREATE:
|
||||||
case tools.PB_UPDATE:
|
case tools.PB_UPDATE:
|
||||||
@@ -115,7 +115,7 @@ func (abs *StreamService) SendResponse(p *peer.Peer, event *common.Event) error
|
|||||||
abs.PublishResources(&ndt, event.User, peerID, j)
|
abs.PublishResources(&ndt, event.User, peerID, j)
|
||||||
} else {
|
} else {
|
||||||
abs.PublishResources(nil, event.User, peerID, j)
|
abs.PublishResources(nil, event.User, peerID, j)
|
||||||
} // TODO : TEMP STREAM !
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func (s *StreamService) write(
|
|||||||
|
|
||||||
if s.Streams[proto][peerID.ID] == nil {
|
if s.Streams[proto][peerID.ID] == nil {
|
||||||
// should create a very temp stream
|
// should create a very temp stream
|
||||||
ctxTTL, err := context.WithTimeout(context.Background(), 10*time.Second)
|
ctxTTL, err := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isAPartner {
|
if isAPartner {
|
||||||
ctxTTL = context.Background()
|
ctxTTL = context.Background()
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (s *StreamService) ConnectToPartner(pid pp.ID, ad *pp.AddrInfo) {
|
|||||||
}
|
}
|
||||||
s.Streams = common.AddStreamProtocol(nil, s.Streams, s.Host, proto, pid, s.Key, false, &f)
|
s.Streams = common.AddStreamProtocol(nil, s.Streams, s.Host, proto, pid, s.Key, false, &f)
|
||||||
}
|
}
|
||||||
common.SendHeartbeat(context.Background(), ProtocolHeartbeatPartner,
|
common.SendHeartbeat(context.Background(), ProtocolHeartbeatPartner, conf.GetConfig().Name,
|
||||||
s.Host, s.Streams, []*pp.AddrInfo{ad}, 20*time.Second)
|
s.Host, s.Streams, []*pp.AddrInfo{ad}, 20*time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -52,5 +52,4 @@ func main() {
|
|||||||
log.Println("shutting down")
|
log.Println("shutting down")
|
||||||
n.Close()
|
n.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
54
peers.json
54
peers.json
@@ -1,54 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"peer_id": "a50d3697-7ede-4fe5-a385-e9d01ebc1002",
|
|
||||||
"name": "ASF",
|
|
||||||
"keywords": [
|
|
||||||
"car",
|
|
||||||
"highway",
|
|
||||||
"images",
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"last_seen_online": "2023-03-07T11:57:13.378707853+01:00",
|
|
||||||
"api_version": "1",
|
|
||||||
"api_url": "http://127.0.0.1:49618/v1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"peer_id": "a50d3697-7ede-4fe5-a385-e9d01ebc1003",
|
|
||||||
"name": "IT",
|
|
||||||
"keywords": [
|
|
||||||
"car",
|
|
||||||
"highway",
|
|
||||||
"images",
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"last_seen_online": "2023-03-07T11:57:13.378707853+01:00",
|
|
||||||
"api_version": "1",
|
|
||||||
"api_url": "https://it.irtse.com/oc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"peer_id": "a50d3697-7ede-4fe5-a385-e9d01ebc1004",
|
|
||||||
"name": "Centre de traitement des amendes",
|
|
||||||
"keywords": [
|
|
||||||
"car",
|
|
||||||
"highway",
|
|
||||||
"images",
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"last_seen_online": "2023-03-07T11:57:13.378707853+01:00",
|
|
||||||
"api_version": "1",
|
|
||||||
"api_url": "https://impots.irtse.com/oc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"peer_id": "a50d3697-7ede-4fe5-a385-e9d01ebc1005",
|
|
||||||
"name": "Douanes",
|
|
||||||
"keywords": [
|
|
||||||
"car",
|
|
||||||
"highway",
|
|
||||||
"images",
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"last_seen_online": "2023-03-07T11:57:13.378707853+01:00",
|
|
||||||
"api_version": "1",
|
|
||||||
"api_url": "https://douanes.irtse.com/oc"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user