// Package peer sends clipboard content to remote clip-sync peers via HTTP POST. package peer import ( "bytes" "encoding/json" "fmt" "log" "net" "net/http" "time" "git.dracodev.net/Projets/clip-sync/internal/config" "git.dracodev.net/Projets/clip-sync/internal/dedup" ) const clipPath = "/clip" // Broadcaster sends clip payloads to all configured peers concurrently. type Broadcaster struct { peers []config.PeerConfig origin string client *http.Client } // NewBroadcaster creates a Broadcaster for the configured peer list. func NewBroadcaster(peers []config.PeerConfig, origin string) *Broadcaster { return &Broadcaster{ peers: peers, origin: origin, client: &http.Client{ Timeout: 5 * time.Second, Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 3 * time.Second, }).DialContext, MaxIdleConnsPerHost: 2, }, }, } } // Broadcast sends the given text to all configured peers in parallel. // Errors are logged but not returned — a single unreachable peer should not // block other peers or the daemon loop. func (b *Broadcaster) Broadcast(text string) { payload := dedup.Payload{ Text: text, Ts: time.Now().UnixNano(), Origin: b.origin, } body, err := json.Marshal(payload) if err != nil { log.Printf("peer: marshal payload: %v", err) return } for i := range b.peers { go b.sendToPeer(b.peers[i], body) } } func (b *Broadcaster) sendToPeer(p config.PeerConfig, body []byte) { url := fmt.Sprintf("http://%s%s", p.Addr, clipPath) req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { log.Printf("peer [%s]: create request: %v", p.Name, err) return } req.Header.Set("Content-Type", "application/json") resp, err := b.client.Do(req) if err != nil { log.Printf("peer [%s] %s: %v", p.Name, p.Addr, err) return } resp.Body.Close() if resp.StatusCode != http.StatusNoContent { log.Printf("peer [%s] %s: unexpected status %d", p.Name, p.Addr, resp.StatusCode) } }