~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
127 lines · 2.6 KB Raw History
package marketpresence

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"strings"
	"sync"
	"time"

	"github.com/bluesky-social/indigo/atproto/identity"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/ingress"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/market"
)

const (
	MaxPresenceName = 253

	ReasonFresh = "HeartbeatFresh"

	ReasonNoDID = "NoDID"

	ReasonNoHeartbeat = "NoHeartbeat"

	ReasonInvalid = "HeartbeatInvalid"

	ReasonMismatch = "HeartbeatMismatch"

	ReasonInFuture = "HeartbeatInFuture"
)

func PresenceName(bidder string) string {
	name := "bidder-" + bidder
	if len(name) <= MaxPresenceName {
		return name
	}
	sum := sha256.Sum256([]byte(bidder))
	return "bidder-" + hex.EncodeToString(sum[:])[:32]
}

type Config struct {
	Reader market.RecordGetter

	Directory identity.Directory

	HTTPClient market.HTTPDoer
}

type Query struct {
	DID string

	OfferingURI string

	PLCDirectoryURL string

	Now time.Time
}

type Observation struct {
	At time.Time

	Reason string
}

type Heartbeats struct {
	reader market.RecordGetter

	directory identity.Directory

	http market.HTTPDoer

	mu sync.Mutex

	readers map[string]market.RecordGetter
}

func New(cfg Config) *Heartbeats {
	return &Heartbeats{reader: cfg.Reader, directory: cfg.Directory, http: cfg.HTTPClient, readers: map[string]market.RecordGetter{}}
}

func (h *Heartbeats) Observe(ctx context.Context, q Query) Observation {
	did := strings.TrimSpace(q.DID)
	if did == "" {
		return Observation{Reason: ReasonNoDID}
	}
	rec, err := h.readerFor(strings.TrimSpace(q.PLCDirectoryURL)).GetRecord(ctx, did, market.HeartbeatNSID, market.HeartbeatRKey)
	if err != nil {
		return Observation{Reason: ReasonNoHeartbeat}
	}
	heartbeat, err := market.ParseHeartbeat(rec.Value)
	if err != nil {
		return Observation{Reason: ReasonInvalid}
	}
	if offering := strings.TrimSpace(q.OfferingURI); offering != "" && heartbeat.Offering != offering {
		return Observation{Reason: ReasonMismatch}
	}
	at, err := market.ParseTime(heartbeat.CreatedAt)
	if err != nil {
		return Observation{Reason: ReasonInvalid}
	}
	if at.After(q.Now.Add(market.HeartbeatMaxSkew)) {
		return Observation{Reason: ReasonInFuture}
	}
	return Observation{At: at, Reason: ReasonFresh}
}

func (h *Heartbeats) readerFor(plc string) market.RecordGetter {
	h.mu.Lock()
	defer h.mu.Unlock()
	if h.reader != nil {
		return h.reader
	}
	if reader, ok := h.readers[plc]; ok {
		return reader
	}
	dir := h.directory
	if dir == nil {
		dir = ingress.NewDirectory(plc)
	}
	reader := market.NewHTTPReader(dir)
	if h.http != nil {
		reader.Client = h.http
	}
	h.readers[plc] = reader
	return reader
}