2022-11-06 21:15:42 -03:00
|
|
|
package nostr
|
|
|
|
|
|
|
|
import (
|
2022-12-17 19:39:10 +01:00
|
|
|
"context"
|
2022-11-06 21:15:42 -03:00
|
|
|
"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]
|
|
|
|
|
2022-11-15 07:53:50 -03:00
|
|
|
Notices chan string
|
|
|
|
ConnectionError chan error
|
|
|
|
|
2022-11-12 21:49:57 -03:00
|
|
|
statusChans s.MapOf[string, chan Status]
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
|
|
|
|
2022-12-17 19:39:10 +01:00
|
|
|
// RelayConnect forwards calls to RelayConnectContext with a background context.
|
2022-11-15 07:53:50 -03:00
|
|
|
func RelayConnect(url string) (*Relay, error) {
|
2022-12-17 19:39:10 +01:00
|
|
|
return RelayConnectContext(context.Background(), url)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RelayConnectContext creates a new relay client and connects to a canonical
|
|
|
|
// URL using Relay.ConnectContext, passing ctx as is.
|
|
|
|
func RelayConnectContext(ctx context.Context, url string) (*Relay, error) {
|
2022-11-15 07:53:50 -03:00
|
|
|
r := &Relay{URL: NormalizeURL(url)}
|
2022-12-17 19:39:10 +01:00
|
|
|
err := r.ConnectContext(ctx)
|
2022-11-15 07:53:50 -03:00
|
|
|
return r, err
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
|
|
|
|
2022-11-26 09:25:31 -03:00
|
|
|
func (r *Relay) String() string {
|
|
|
|
return r.URL
|
|
|
|
}
|
|
|
|
|
2022-12-17 19:39:10 +01:00
|
|
|
// Connect calls ConnectContext with a background context.
|
2022-11-06 21:15:42 -03:00
|
|
|
func (r *Relay) Connect() error {
|
2022-12-17 19:39:10 +01:00
|
|
|
return r.ConnectContext(context.Background())
|
|
|
|
}
|
|
|
|
|
|
|
|
// ConnectContext tries to establish a websocket connection to r.URL.
|
|
|
|
// If the context expires before the connection is complete, an error is returned.
|
|
|
|
// Once successfully connected, context expiration has no effect: call r.Close
|
|
|
|
// to close the connection.
|
|
|
|
func (r *Relay) ConnectContext(ctx context.Context) error {
|
2022-11-06 21:15:42 -03:00
|
|
|
if r.URL == "" {
|
|
|
|
return fmt.Errorf("invalid relay URL '%s'", r.URL)
|
|
|
|
}
|
|
|
|
|
2022-12-17 19:39:10 +01:00
|
|
|
socket, _, err := websocket.DefaultDialer.DialContext(ctx, r.URL, nil)
|
2022-11-06 21:15:42 -03:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error opening websocket to '%s': %w", r.URL, err)
|
|
|
|
}
|
|
|
|
|
2022-11-15 07:53:50 -03:00
|
|
|
r.Notices = make(chan string)
|
|
|
|
r.ConnectionError = make(chan error)
|
|
|
|
|
2022-11-06 21:15:42 -03:00
|
|
|
conn := NewConnection(socket)
|
2022-11-14 19:48:02 -03:00
|
|
|
r.Connection = conn
|
2022-11-06 21:15:42 -03:00
|
|
|
|
2022-11-14 19:48:02 -03:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
typ, message, err := conn.socket.ReadMessage()
|
|
|
|
if err != nil {
|
2022-11-15 07:53:50 -03:00
|
|
|
r.ConnectionError <- err
|
|
|
|
break
|
2022-11-14 19:48:02 -03:00
|
|
|
}
|
2022-11-15 07:53:50 -03:00
|
|
|
|
2022-11-14 19:48:02 -03:00
|
|
|
if typ == websocket.PingMessage {
|
|
|
|
conn.WriteMessage(websocket.PongMessage, nil)
|
|
|
|
continue
|
|
|
|
}
|
2022-11-06 21:15:42 -03:00
|
|
|
|
2022-11-14 19:48:02 -03:00
|
|
|
if typ != websocket.TextMessage || len(message) == 0 || message[0] != '[' {
|
|
|
|
continue
|
|
|
|
}
|
2022-11-06 21:15:42 -03:00
|
|
|
|
2022-11-14 19:48:02 -03:00
|
|
|
var jsonMessage []json.RawMessage
|
|
|
|
err = json.Unmarshal(message, &jsonMessage)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2022-11-06 21:15:42 -03:00
|
|
|
|
2022-11-14 19:48:02 -03:00
|
|
|
if len(jsonMessage) < 2 {
|
2022-11-06 21:15:42 -03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-11-14 19:48:02 -03:00
|
|
|
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 {
|
2022-11-06 21:15:42 -03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-11-14 19:48:02 -03:00
|
|
|
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
|
2022-11-19 14:00:29 -03:00
|
|
|
if !subscription.Filters.Match(&event) {
|
2022-11-14 19:48:02 -03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if !subscription.stopped {
|
|
|
|
subscription.Events <- event
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case "EOSE":
|
|
|
|
if len(jsonMessage) < 2 {
|
2022-11-06 21:15:42 -03:00
|
|
|
continue
|
|
|
|
}
|
2022-11-14 19:48:02 -03:00
|
|
|
var channel string
|
|
|
|
json.Unmarshal(jsonMessage[1], &channel)
|
|
|
|
if subscription, ok := r.subscriptions.Load(channel); ok {
|
2022-11-16 10:07:15 -03:00
|
|
|
subscription.emitEose.Do(func() {
|
|
|
|
subscription.EndOfStoredEvents <- struct{}{}
|
|
|
|
})
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
2022-11-14 19:48:02 -03:00
|
|
|
case "OK":
|
|
|
|
if len(jsonMessage) < 3 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var (
|
|
|
|
eventId string
|
|
|
|
ok bool
|
|
|
|
)
|
|
|
|
json.Unmarshal(jsonMessage[1], &eventId)
|
|
|
|
json.Unmarshal(jsonMessage[2], &ok)
|
|
|
|
|
|
|
|
if statusChan, ok := r.statusChans.Load(eventId); ok {
|
|
|
|
if ok {
|
|
|
|
statusChan <- PublishStatusSucceeded
|
|
|
|
} else {
|
|
|
|
statusChan <- PublishStatusFailed
|
|
|
|
}
|
2022-11-12 21:49:57 -03:00
|
|
|
}
|
|
|
|
}
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
2022-11-14 19:48:02 -03:00
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r Relay) Publish(event Event) chan Status {
|
2022-11-17 09:28:45 -03:00
|
|
|
statusChan := make(chan Status, 4)
|
2022-11-06 21:15:42 -03:00
|
|
|
|
|
|
|
go func() {
|
2022-11-12 21:49:57 -03:00
|
|
|
// we keep track of this so the OK message can be used to close it
|
|
|
|
r.statusChans.Store(event.ID, statusChan)
|
|
|
|
defer r.statusChans.Delete(event.ID)
|
|
|
|
|
2022-11-06 21:15:42 -03:00
|
|
|
err := r.Connection.WriteJSON([]interface{}{"EVENT", event})
|
|
|
|
if err != nil {
|
|
|
|
statusChan <- PublishStatusFailed
|
|
|
|
close(statusChan)
|
2022-11-17 09:28:45 -03:00
|
|
|
return
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
|
|
|
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 {
|
2022-11-14 19:48:02 -03:00
|
|
|
if r.Connection == nil {
|
|
|
|
panic(fmt.Errorf("must call .Connect() first before calling .Subscribe()"))
|
|
|
|
}
|
|
|
|
|
2022-11-19 14:00:29 -03:00
|
|
|
sub := r.PrepareSubscription()
|
|
|
|
sub.Filters = filters
|
|
|
|
sub.Fire()
|
|
|
|
return sub
|
|
|
|
}
|
|
|
|
|
2022-11-26 19:32:03 -03:00
|
|
|
func (r *Relay) QuerySync(filter Filter, timeout time.Duration) []Event {
|
|
|
|
sub := r.Subscribe(Filters{filter})
|
|
|
|
var events []Event
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case evt := <-sub.Events:
|
|
|
|
events = append(events, evt)
|
|
|
|
case <-sub.EndOfStoredEvents:
|
|
|
|
return events
|
|
|
|
case <-time.After(timeout):
|
|
|
|
return events
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-19 14:00:29 -03:00
|
|
|
func (r *Relay) PrepareSubscription() *Subscription {
|
2022-11-06 21:15:42 -03:00
|
|
|
random := make([]byte, 7)
|
|
|
|
rand.Read(random)
|
|
|
|
id := hex.EncodeToString(random)
|
2022-11-19 14:00:29 -03:00
|
|
|
|
|
|
|
return r.prepareSubscription(id)
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
|
|
|
|
2022-11-19 14:00:29 -03:00
|
|
|
func (r *Relay) prepareSubscription(id string) *Subscription {
|
|
|
|
sub := &Subscription{
|
2022-11-26 09:25:51 -03:00
|
|
|
Relay: r,
|
2022-11-14 19:48:02 -03:00
|
|
|
conn: r.Connection,
|
|
|
|
id: id,
|
|
|
|
Events: make(chan Event),
|
2022-11-16 10:05:28 -03:00
|
|
|
EndOfStoredEvents: make(chan struct{}, 1),
|
2022-11-14 19:48:02 -03:00
|
|
|
}
|
2022-11-06 21:15:42 -03:00
|
|
|
|
2022-11-19 14:00:29 -03:00
|
|
|
r.subscriptions.Store(sub.id, sub)
|
|
|
|
return sub
|
2022-11-06 21:15:42 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Relay) Close() error {
|
|
|
|
return r.Connection.Close()
|
|
|
|
}
|