go-nostr/nip46/client.go

229 lines
5.4 KiB
Go
Raw Permalink Normal View History

2024-02-06 00:45:36 -03:00
package nip46
import (
"context"
"encoding/json"
"fmt"
"math/rand"
2024-02-06 00:45:36 -03:00
"net/url"
"strconv"
"sync/atomic"
"github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"github.com/puzpuzpuz/xsync/v3"
)
type BunkerClient struct {
serial atomic.Uint64
clientSecretKey string
pool *nostr.SimplePool
target string
relays []string
sharedSecret []byte
listeners *xsync.MapOf[string, chan Response]
2024-02-29 20:37:16 -03:00
expectingAuth *xsync.MapOf[string, struct{}]
idPrefix string
2024-02-29 20:37:16 -03:00
onAuth func(string)
2024-02-06 00:45:36 -03:00
// memoized
getPublicKeyResponse string
}
// ConnectBunker establishes an RPC connection to a NIP-46 signer using the relays and secret provided in the bunkerURL.
// pool can be passed to reuse an existing pool, otherwise a new pool will be created.
func ConnectBunker(
ctx context.Context,
clientSecretKey string,
bunkerURLOrNIP05 string,
2024-02-06 00:45:36 -03:00
pool *nostr.SimplePool,
2024-02-29 20:37:16 -03:00
onAuth func(string),
2024-02-06 00:45:36 -03:00
) (*BunkerClient, error) {
parsed, err := url.Parse(bunkerURLOrNIP05)
2024-02-06 00:45:36 -03:00
if err != nil {
return nil, fmt.Errorf("invalid url: %w", err)
}
// assume it's a bunker url (will fail later if not)
secret := parsed.Query().Get("secret")
relays := parsed.Query()["relay"]
2024-02-29 20:29:08 -03:00
targetPublicKey := parsed.Host
if parsed.Scheme == "" {
// could be a NIP-05
pubkey, relays_, err := queryWellKnownNostrJson(ctx, bunkerURLOrNIP05)
if err != nil {
return nil, fmt.Errorf("failed to query nip05: %w", err)
}
2024-02-29 20:29:08 -03:00
targetPublicKey = pubkey
relays = relays_
} else if parsed.Scheme == "bunker" {
// this is what we were expecting, so just move on
} else {
// otherwise fail here
2024-02-06 00:45:36 -03:00
return nil, fmt.Errorf("wrong scheme '%s', must be bunker://", parsed.Scheme)
}
2024-02-29 20:29:08 -03:00
if !nostr.IsValidPublicKey(targetPublicKey) {
return nil, fmt.Errorf("'%s' is not a valid public key hex", targetPublicKey)
2024-02-06 00:45:36 -03:00
}
2024-02-29 20:29:08 -03:00
bunker := NewBunker(
ctx,
clientSecretKey,
targetPublicKey,
relays,
pool,
2024-02-29 20:37:16 -03:00
onAuth,
2024-02-29 20:29:08 -03:00
)
2024-03-04 09:30:45 -03:00
_, err = bunker.RPC(ctx, "connect", []string{targetPublicKey, secret})
2024-02-29 20:29:08 -03:00
return bunker, err
}
func NewBunker(
ctx context.Context,
clientSecretKey string,
targetPublicKey string,
relays []string,
pool *nostr.SimplePool,
2024-02-29 20:37:16 -03:00
onAuth func(string),
2024-02-29 20:29:08 -03:00
) *BunkerClient {
2024-02-06 00:45:36 -03:00
if pool == nil {
pool = nostr.NewSimplePool(ctx)
}
2024-02-29 20:29:08 -03:00
clientPublicKey, _ := nostr.GetPublicKey(clientSecretKey)
sharedSecret, _ := nip04.ComputeSharedSecret(targetPublicKey, clientSecretKey)
2024-02-06 00:45:36 -03:00
bunker := &BunkerClient{
pool: pool,
clientSecretKey: clientSecretKey,
target: targetPublicKey,
relays: relays,
sharedSecret: sharedSecret,
listeners: xsync.NewMapOf[string, chan Response](),
expectingAuth: xsync.NewMapOf[string, struct{}](),
onAuth: onAuth,
idPrefix: "gn-" + strconv.Itoa(rand.Intn(65536)),
2024-02-06 00:45:36 -03:00
}
go func() {
2024-04-25 18:25:35 -03:00
now := nostr.Now()
2024-02-06 00:45:36 -03:00
events := pool.SubMany(ctx, relays, nostr.Filters{
{
2024-05-15 16:59:38 -03:00
Tags: nostr.TagMap{"p": []string{clientPublicKey}},
Kinds: []int{nostr.KindNostrConnect},
Since: &now,
LimitZero: true,
2024-02-06 00:45:36 -03:00
},
})
for ie := range events {
if ie.Kind != nostr.KindNostrConnect {
continue
}
var resp Response
2024-02-29 20:29:08 -03:00
plain, err := nip04.Decrypt(ie.Content, sharedSecret)
2024-02-06 00:45:36 -03:00
if err != nil {
continue
}
err = json.Unmarshal([]byte(plain), &resp)
if err != nil {
continue
}
2024-02-29 20:37:16 -03:00
if resp.Result == "auth_url" {
// special case
authURL := resp.Error
if _, ok := bunker.expectingAuth.Load(resp.ID); ok {
bunker.onAuth(authURL)
}
continue
}
2024-02-06 00:45:36 -03:00
if dispatcher, ok := bunker.listeners.Load(resp.ID); ok {
dispatcher <- resp
}
}
}()
2024-02-29 20:29:08 -03:00
return bunker
2024-02-06 00:45:36 -03:00
}
func (bunker *BunkerClient) Ping(ctx context.Context) error {
_, err := bunker.RPC(ctx, "ping", []string{})
if err != nil {
return err
}
return nil
}
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (string, error) {
if bunker.getPublicKeyResponse != "" {
return bunker.getPublicKeyResponse, nil
}
resp, err := bunker.RPC(ctx, "get_public_key", []string{})
bunker.getPublicKeyResponse = resp
return resp, err
}
func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) error {
resp, err := bunker.RPC(ctx, "sign_event", []string{evt.String()})
if err == nil {
err = easyjson.Unmarshal([]byte(resp), evt)
}
return err
}
func (bunker *BunkerClient) RPC(ctx context.Context, method string, params []string) (string, error) {
id := bunker.idPrefix + "-" + strconv.FormatUint(bunker.serial.Add(1), 10)
2024-02-06 00:45:36 -03:00
req, err := json.Marshal(Request{
ID: id,
Method: method,
Params: params,
})
if err != nil {
return "", err
}
content, err := nip04.Encrypt(string(req), bunker.sharedSecret)
if err != nil {
return "", fmt.Errorf("error encrypting request: %w", err)
}
evt := nostr.Event{
Content: content,
CreatedAt: nostr.Now(),
Kind: nostr.KindNostrConnect,
Tags: nostr.Tags{{"p", bunker.target}},
}
if err := evt.Sign(bunker.clientSecretKey); err != nil {
return "", fmt.Errorf("failed to sign request event: %w", err)
}
respWaiter := make(chan Response)
bunker.listeners.Store(id, respWaiter)
hasWorked := false
2024-04-25 18:25:35 -03:00
2024-02-06 00:45:36 -03:00
for _, r := range bunker.relays {
relay, err := bunker.pool.EnsureRelay(r)
if err == nil {
hasWorked = true
}
relay.Publish(ctx, evt)
}
2024-04-25 18:25:35 -03:00
2024-02-06 00:45:36 -03:00
if !hasWorked {
return "", fmt.Errorf("couldn't connect to any relay")
}
resp := <-respWaiter
if resp.Error != "" {
return "", fmt.Errorf("response error: %s", resp.Error)
}
return resp.Result, nil
}