mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-05-05 08:10:14 +02:00
Added some NIP-42 functionality to the client (relay.go) (#38)
This commit is contained in:
parent
9775016bf1
commit
87b6280299
40
README.md
40
README.md
@ -87,6 +87,46 @@ for _, url := range []string{"wss://nostr.zebedee.cloud", "wss://nostr-pub.wello
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Authenticating with NIP-42
|
||||||
|
|
||||||
|
For this section, the user needs access to a relay implementing NIP-42.
|
||||||
|
E.g., https://github.com/fiatjaf/relayer with a relay implementing the relayer.Auther interface.
|
||||||
|
|
||||||
|
``` go
|
||||||
|
func main() {
|
||||||
|
url := "ws://localhost:7447"
|
||||||
|
|
||||||
|
// Once the connection is initiated the server will send "AUTH" with the challenge string.
|
||||||
|
relay, err := nostr.RelayConnect(context.Background(), url)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize test user.
|
||||||
|
sk := nostr.GeneratePrivateKey()
|
||||||
|
pub, _ := nostr.GetPublicKey(sk)
|
||||||
|
npub, _ := nip19.EncodePublicKey(pub)
|
||||||
|
|
||||||
|
// Relay.Challenges channel will receive the "AUTH" command.
|
||||||
|
challenge := <-relay.Challenges
|
||||||
|
|
||||||
|
// Create the auth event to send back.
|
||||||
|
// The user will be authenticated as pub.
|
||||||
|
event := nip42.CreateUnsignedAuthEvent(challenge, pub, url)
|
||||||
|
event.Sign(sk)
|
||||||
|
|
||||||
|
// Set-up context with 3 second time out.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Send the event by calling relay.Auth.
|
||||||
|
// Returned status is either success, fail, or sent (if no reply given in the 3 second timeout).
|
||||||
|
auth_status := relay.Auth(ctx, event)
|
||||||
|
|
||||||
|
fmt.Printf("authenticated as %s: %s\n", npub, auth_status)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Example script
|
### Example script
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -13,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
|
||||||
// connect to relay
|
// connect to relay
|
||||||
url := "wss://nostr.zebedee.cloud"
|
url := "wss://nostr.zebedee.cloud"
|
||||||
@ -58,7 +59,7 @@ func main() {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-sub.EndOfStoredEvents
|
<-sub.EndOfStoredEvents
|
||||||
sub.Unsub()
|
cancel()
|
||||||
}()
|
}()
|
||||||
for ev := range sub.Events {
|
for ev := range sub.Events {
|
||||||
evs = append(evs, ev)
|
evs = append(evs, ev)
|
||||||
@ -106,19 +107,14 @@ func main() {
|
|||||||
fmt.Fprintln(os.Stderr, "enter content of note, ending with an empty newline:")
|
fmt.Fprintln(os.Stderr, "enter content of note, ending with an empty newline:")
|
||||||
for {
|
for {
|
||||||
if n, err := reader.Read(b[:]); err == nil {
|
if n, err := reader.Read(b[:]); err == nil {
|
||||||
new_line := strings.TrimSpace(fmt.Sprintf("%s", b[:n]))
|
content = fmt.Sprintf("%s%s", content, fmt.Sprintf("%s", b[:n]))
|
||||||
if new_line == "" {
|
} else if err == io.EOF {
|
||||||
break
|
break
|
||||||
} else if content == "" {
|
|
||||||
content = new_line
|
|
||||||
} else {
|
|
||||||
content = fmt.Sprintf("%s\n%s", content, new_line)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ev.Content = content
|
ev.Content = strings.TrimSpace(content)
|
||||||
ev.Sign(sk)
|
ev.Sign(sk)
|
||||||
for _, url := range []string{"wss://nostr.zebedee.cloud"} {
|
for _, url := range []string{"wss://nostr.zebedee.cloud"} {
|
||||||
ctx := context.WithValue(context.Background(), "url", url)
|
ctx := context.WithValue(context.Background(), "url", url)
|
||||||
|
@ -7,6 +7,23 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CreateUnsignedAuthEvent creates an event which should be sent via an "AUTH" command.
|
||||||
|
// If the authentication succeeds, the user will be authenticated as pubkey.
|
||||||
|
func CreateUnsignedAuthEvent(challenge, pubkey, relayURL string) nostr.Event {
|
||||||
|
return nostr.Event{
|
||||||
|
PubKey: pubkey,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Kind: 22242,
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
nostr.Tag{"relay", relayURL},
|
||||||
|
nostr.Tag{"challenge", challenge},
|
||||||
|
},
|
||||||
|
Content: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAuthEvent checks whether event is a valid NIP-42 event for given challenge and relayURL.
|
||||||
|
// The result of the validation is encoded in the ok bool.
|
||||||
func ValidateAuthEvent(event *nostr.Event, challenge string, relayURL string) (pubkey string, ok bool) {
|
func ValidateAuthEvent(event *nostr.Event, challenge string, relayURL string) (pubkey string, ok bool) {
|
||||||
if ok, _ := event.CheckSignature(); ok == false {
|
if ok, _ := event.CheckSignature(); ok == false {
|
||||||
return "", false
|
return "", false
|
||||||
|
78
relay.go
78
relay.go
@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
s "github.com/SaveTheRbtz/generic-sync-map-go"
|
s "github.com/SaveTheRbtz/generic-sync-map-go"
|
||||||
@ -40,15 +41,16 @@ type Relay struct {
|
|||||||
Connection *Connection
|
Connection *Connection
|
||||||
subscriptions s.MapOf[string, *Subscription]
|
subscriptions s.MapOf[string, *Subscription]
|
||||||
|
|
||||||
|
Challenges chan string // NIP-42 Challenges
|
||||||
Notices chan string
|
Notices chan string
|
||||||
ConnectionError chan error
|
ConnectionError chan error
|
||||||
|
|
||||||
okCallbacks s.MapOf[string, func(bool)]
|
okCallbacks s.MapOf[string, func(bool)]
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelayConnect returns a relay object connected to url
|
// RelayConnect returns a relay object connected to url.
|
||||||
// Once successfully connected, cancelling ctx has no effect
|
// Once successfully connected, cancelling ctx has no effect.
|
||||||
// To close the connection, call r.Close()
|
// To close the connection, call r.Close().
|
||||||
func RelayConnect(ctx context.Context, url string) (*Relay, error) {
|
func RelayConnect(ctx context.Context, url string) (*Relay, error) {
|
||||||
r := &Relay{URL: NormalizeURL(url)}
|
r := &Relay{URL: NormalizeURL(url)}
|
||||||
err := r.Connect(ctx)
|
err := r.Connect(ctx)
|
||||||
@ -80,6 +82,7 @@ func (r *Relay) Connect(ctx context.Context) error {
|
|||||||
return fmt.Errorf("error opening websocket to '%s': %w", r.URL, err)
|
return fmt.Errorf("error opening websocket to '%s': %w", r.URL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.Challenges = make(chan string)
|
||||||
r.Notices = make(chan string)
|
r.Notices = make(chan string)
|
||||||
r.ConnectionError = make(chan error)
|
r.ConnectionError = make(chan error)
|
||||||
|
|
||||||
@ -121,6 +124,12 @@ func (r *Relay) Connect(ctx context.Context) error {
|
|||||||
var content string
|
var content string
|
||||||
json.Unmarshal(jsonMessage[1], &content)
|
json.Unmarshal(jsonMessage[1], &content)
|
||||||
r.Notices <- content
|
r.Notices <- content
|
||||||
|
case "AUTH":
|
||||||
|
var challenge string
|
||||||
|
json.Unmarshal(jsonMessage[1], &challenge)
|
||||||
|
go func() {
|
||||||
|
r.Challenges <- challenge
|
||||||
|
}()
|
||||||
case "EVENT":
|
case "EVENT":
|
||||||
if len(jsonMessage) < 3 {
|
if len(jsonMessage) < 3 {
|
||||||
continue
|
continue
|
||||||
@ -185,8 +194,8 @@ func (r *Relay) Connect(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish sends an "EVENT" command to the relay r as in NIP-01
|
// Publish sends an "EVENT" command to the relay r as in NIP-01.
|
||||||
// status can be: success, failed, or sent (no response from relay before ctx times out)
|
// Status can be: success, failed, or sent (no response from relay before ctx times out).
|
||||||
func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
||||||
status := PublishStatusFailed
|
status := PublishStatusFailed
|
||||||
|
|
||||||
@ -242,9 +251,62 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe sends a "REQ" command to the relay r as in NIP-01
|
// Auth sends an "AUTH" command client -> relay as in NIP-42.
|
||||||
// Events are returned through the channel sub.Events
|
// Status can be: success, failed, or sent (no response from relay before ctx times out).
|
||||||
// the subscription is closed when context ctx is cancelled ("CLOSE" in NIP-01)
|
func (r *Relay) Auth(ctx context.Context, event Event) Status {
|
||||||
|
status := PublishStatusFailed
|
||||||
|
|
||||||
|
// data races on status variable without this mutex
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
if _, ok := ctx.Deadline(); !ok {
|
||||||
|
// if no timeout is set, force it to 3 seconds
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// make it cancellable so we can stop everything upon receiving an "OK"
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// listen for an OK callback
|
||||||
|
okCallback := func(ok bool) {
|
||||||
|
mu.Lock()
|
||||||
|
if ok {
|
||||||
|
status = PublishStatusSucceeded
|
||||||
|
} else {
|
||||||
|
status = PublishStatusFailed
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
r.okCallbacks.Store(event.ID, okCallback)
|
||||||
|
defer r.okCallbacks.Delete(event.ID)
|
||||||
|
|
||||||
|
// send AUTH
|
||||||
|
if err := r.Connection.WriteJSON([]interface{}{"AUTH", event}); err != nil {
|
||||||
|
// status will be "failed"
|
||||||
|
return status
|
||||||
|
} else {
|
||||||
|
// use mu.Lock() just in case the okCallback got called, extremely unlikely.
|
||||||
|
mu.Lock()
|
||||||
|
status = PublishStatusSent
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
// the context either times out, and the status is "sent"
|
||||||
|
// or the okCallback is called and the status is set to "succeeded" or "failed"
|
||||||
|
// NIP-42 does not mandate an "OK" reply to an "AUTH" message
|
||||||
|
<-ctx.Done()
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe sends a "REQ" command to the relay r as in NIP-01.
|
||||||
|
// Events are returned through the channel sub.Events.
|
||||||
|
// The subscription is closed when context ctx is cancelled ("CLOSE" in NIP-01).
|
||||||
func (r *Relay) Subscribe(ctx context.Context, filters Filters) *Subscription {
|
func (r *Relay) Subscribe(ctx context.Context, filters Filters) *Subscription {
|
||||||
if r.Connection == nil {
|
if r.Connection == nil {
|
||||||
panic(fmt.Errorf("must call .Connect() first before calling .Subscribe()"))
|
panic(fmt.Errorf("must call .Connect() first before calling .Subscribe()"))
|
||||||
|
@ -24,8 +24,8 @@ type EventMessage struct {
|
|||||||
Relay string
|
Relay string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsub closes the subscription, sending "CLOSE" to relay as in NIP-01
|
// Unsub closes the subscription, sending "CLOSE" to relay as in NIP-01.
|
||||||
// Unsub() also closes the channel sub.Events
|
// Unsub() also closes the channel sub.Events.
|
||||||
func (sub *Subscription) Unsub() {
|
func (sub *Subscription) Unsub() {
|
||||||
sub.mutex.Lock()
|
sub.mutex.Lock()
|
||||||
defer sub.mutex.Unlock()
|
defer sub.mutex.Unlock()
|
||||||
@ -37,14 +37,14 @@ func (sub *Subscription) Unsub() {
|
|||||||
sub.stopped = true
|
sub.stopped = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub sets sub.Filters and then calls sub.Fire(ctx)
|
// Sub sets sub.Filters and then calls sub.Fire(ctx).
|
||||||
func (sub *Subscription) Sub(ctx context.Context, filters Filters) {
|
func (sub *Subscription) Sub(ctx context.Context, filters Filters) {
|
||||||
sub.Filters = filters
|
sub.Filters = filters
|
||||||
sub.Fire(ctx)
|
sub.Fire(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire sends the "REQ" command to the relay
|
// Fire sends the "REQ" command to the relay.
|
||||||
// when ctx is cancelled, sub.Unsub() is called, closing the subscription
|
// When ctx is cancelled, sub.Unsub() is called, closing the subscription.
|
||||||
func (sub *Subscription) Fire(ctx context.Context) {
|
func (sub *Subscription) Fire(ctx context.Context) {
|
||||||
message := []interface{}{"REQ", sub.id}
|
message := []interface{}{"REQ", sub.id}
|
||||||
for _, filter := range sub.Filters {
|
for _, filter := range sub.Filters {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user