go-nostr/relay.go

185 lines
3.5 KiB
Go
Raw Permalink Normal View History

package nostr
import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"math/rand"
"time"
s "github.com/SaveTheRbtz/generic-sync-map-go"
"github.com/gorilla/websocket"
)
type Status int
const (
PublishStatusSent Status = 0
PublishStatusFailed Status = -1
PublishStatusSucceeded Status = 1
)
func (s Status) String() string {
switch s {
case PublishStatusSent:
return "sent"
case PublishStatusFailed:
return "failed"
case PublishStatusSucceeded:
return "success"
}
return "unknown"
}
type Relay struct {
URL string
Connection *Connection
subscriptions s.MapOf[string, *Subscription]
Notices chan string
}
func NewRelay(url string) *Relay {
return &Relay{
URL: NormalizeURL(url),
subscriptions: s.MapOf[string, *Subscription]{},
}
}
func (r *Relay) Connect() error {
if r.URL == "" {
return fmt.Errorf("invalid relay URL '%s'", r.URL)
}
socket, _, err := websocket.DefaultDialer.Dial(r.URL, nil)
if err != nil {
return fmt.Errorf("error opening websocket to '%s': %w", r.URL, err)
}
conn := NewConnection(socket)
for {
typ, message, err := conn.socket.ReadMessage()
if err != nil {
return fmt.Errorf("read error: %w", err)
}
if typ == websocket.PingMessage {
conn.WriteMessage(websocket.PongMessage, nil)
continue
}
if typ != websocket.TextMessage || len(message) == 0 || message[0] != '[' {
continue
}
var jsonMessage []json.RawMessage
err = json.Unmarshal(message, &jsonMessage)
if err != nil {
continue
}
if len(jsonMessage) < 2 {
continue
}
var label string
json.Unmarshal(jsonMessage[0], &label)
switch label {
case "NOTICE":
var content string
json.Unmarshal(jsonMessage[1], &content)
r.Notices <- content
case "EVENT":
if len(jsonMessage) < 3 {
continue
}
var channel string
json.Unmarshal(jsonMessage[1], &channel)
if subscription, ok := r.subscriptions.Load(channel); ok {
var event Event
json.Unmarshal(jsonMessage[2], &event)
// check signature of all received events, ignore invalid
ok, err := event.CheckSignature()
if !ok {
errmsg := ""
if err != nil {
errmsg = err.Error()
}
log.Printf("bad signature: %s", errmsg)
continue
}
// check if the event matches the desired filter, ignore otherwise
if !subscription.filters.Match(&event) {
continue
}
if !subscription.stopped {
subscription.Events <- event
}
}
}
}
}
func (r Relay) Publish(event Event) chan Status {
statusChan := make(chan Status)
go func() {
err := r.Connection.WriteJSON([]interface{}{"EVENT", event})
if err != nil {
statusChan <- PublishStatusFailed
close(statusChan)
}
statusChan <- PublishStatusSent
sub := r.Subscribe(Filters{Filter{IDs: []string{event.ID}}})
for {
select {
case receivedEvent := <-sub.Events:
if receivedEvent.ID == event.ID {
statusChan <- PublishStatusSucceeded
close(statusChan)
break
} else {
continue
}
case <-time.After(5 * time.Second):
close(statusChan)
break
}
break
}
}()
return statusChan
}
func (r *Relay) Subscribe(filters Filters) *Subscription {
random := make([]byte, 7)
rand.Read(random)
id := hex.EncodeToString(random)
return r.subscribe(id, filters)
}
func (r *Relay) subscribe(id string, filters Filters) *Subscription {
sub := Subscription{}
sub.id = id
sub.Events = make(chan Event)
r.subscriptions.Store(sub.id, &sub)
sub.Sub(filters)
return &sub
}
func (r *Relay) Close() error {
return r.Connection.Close()
}