upgrade interfaces, add local filtering for subscriptions and some other things I forgot. also a README with examples.

This commit is contained in:
fiatjaf 2021-12-16 20:47:53 -03:00
parent 7dfc354eec
commit 0a60285e30
8 changed files with 141 additions and 40 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
go-nostr

75
README.md Normal file
View File

@ -0,0 +1,75 @@
go-nostr
========
A set of useful things for [Nostr Protocol](https://github.com/fiatjaf/nostr) implementations.
<a href="https://godoc.org/github.com/fiatjaf/go-nostr"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
### Subscribing to a set of relays
```go
pool := relaypool.New()
pool.Add("wss://relay.nostr.com/", &relaypool.Policy{
SimplePolicy: relaypool.SimplePolicy{Read: true, Write: true},
})
pool.Add("wss://nostrrelay.example.com/", &relaypool.Policy{
SimplePolicy: relaypool.SimplePolicy{Read: true, Write: true},
})
for notice := range pool.Notices {
log.Printf("%s has sent a notice: '%s'\n", notice.Relay, notice.Message)
}
```
### Listening for events
```go
kind1 := event.KindTextNote
sub := pool.Sub(filter.EventFilters{
{
Authors: []string{"0ded86bf80c76847320b16f22b7451c08169434837a51ad5fe3b178af6c35f5d"},
Kind: &kind1, // or 1
},
})
go func() {
for event := range sub.UniqueEvents {
log.Print(event)
}
}()
time.Sleep(5 * time.Second)
sub.Unsub()
```
### Publishing an event
```go
secretKey := "3f06a81e0a0c2ad34ee9df2a30d87a810da9e3c3881f780755ace5e5e64d30a7"
pool.SecretKey = &secretKey
event, statuses, _ := pool.PublishEvent(&event.Event{
CreatedAt: uint32(time.Now().Unix()),
Kind: 1, // or event.KindTextNote
Tags: make(event.Tags, 0),
Content: "hello",
})
log.Print(event.PubKey)
log.Print(event.ID)
log.Print(event.Sig)
for status := range statuses {
switch status.Status {
case relaypool.PublishStatusSent:
fmt.Printf("Sent event %s to '%s'.\n", event.ID, status.Relay)
case relaypool.PublishStatusFailed:
fmt.Printf("Failed to send event %s to '%s'.\n", event.ID, status.Relay)
case relaypool.PublishStatusSucceeded:
fmt.Printf("Event seen %s on '%s'.\n", event.ID, status.Relay)
}
}
```

View File

@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math/big"
"github.com/fiatjaf/bip340"
)
@ -95,12 +94,9 @@ func (evt *Event) Serialize() []byte {
// returns an error if the signature itself is invalid.
func (evt Event) CheckSignature() (bool, error) {
// read and check pubkey
pubkeyb, err := hex.DecodeString(evt.PubKey)
pubkey, err := bip340.ParsePublicKey(evt.PubKey)
if err != nil {
return false, err
}
if len(pubkeyb) != 32 {
return false, fmt.Errorf("pubkey must be 32 bytes, not %d", len(pubkeyb))
return false, fmt.Errorf("Event has invalid pubkey '%s': %w", evt.PubKey, err)
}
// check tags
@ -116,32 +112,28 @@ func (evt Event) CheckSignature() (bool, error) {
}
}
sig, err := hex.DecodeString(evt.Sig)
s, err := hex.DecodeString(evt.Sig)
if err != nil {
return false, fmt.Errorf("signature is invalid hex: %w", err)
}
if len(sig) != 64 {
return false, fmt.Errorf("signature must be 64 bytes, not %d", len(sig))
if len(s) != 64 {
return false, fmt.Errorf("signature must be 64 bytes, not %d", len(s))
}
var p [32]byte
copy(p[:], pubkeyb)
var sig [64]byte
copy(sig[:], s)
var s [64]byte
copy(s[:], sig)
h := sha256.Sum256(evt.Serialize())
return bip340.Verify(p, h, s)
hash := sha256.Sum256(evt.Serialize())
return bip340.Verify(pubkey, hash, sig)
}
// Sign signs an event with a given privateKey
func (evt *Event) Sign(privateKey string) error {
h := sha256.Sum256(evt.Serialize())
s, _ := new(big.Int).SetString(privateKey, 16)
if s == nil {
return errors.New("invalid private key " + privateKey)
s, err := bip340.ParsePrivateKey(privateKey)
if err != nil {
return fmt.Errorf("Sign called with invalid private key '%s': %w", privateKey, err)
}
aux := make([]byte, 32)

View File

@ -2,6 +2,8 @@ package filter
import "github.com/fiatjaf/go-nostr/event"
type EventFilters []EventFilter
type EventFilter struct {
ID string `json:"id,omitempty"`
Kind *int `json:"kind,omitempty"`
@ -11,6 +13,16 @@ type EventFilter struct {
Since uint32 `json:"since,omitempty"`
}
func (eff EventFilters) Match(event *event.Event) bool {
for _, filter := range eff {
if filter.Matches(event) {
return true
}
}
return false
}
func (ef EventFilter) Matches(event *event.Event) bool {
if event == nil {
return false

2
go.mod
View File

@ -3,6 +3,6 @@ module github.com/fiatjaf/go-nostr
go 1.15
require (
github.com/fiatjaf/bip340 v1.0.0
github.com/fiatjaf/bip340 v1.1.0
github.com/gorilla/websocket v1.4.2
)

4
go.sum
View File

@ -14,8 +14,8 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fiatjaf/bip340 v1.0.0 h1:mpwbm+0KC9BXB/7/pnac4e4N1TiuppyEVXxtVAXj75k=
github.com/fiatjaf/bip340 v1.0.0/go.mod h1:MxAz+5FQUTW4OT2gnCBC6Our486wmqf72ykZIrh7+is=
github.com/fiatjaf/bip340 v1.1.0 h1:W+CnUU3RyqgMKS2S9t/r2l3L4D+sSkRtU4la7MlVBR8=
github.com/fiatjaf/bip340 v1.1.0/go.mod h1:MxAz+5FQUTW4OT2gnCBC6Our486wmqf72ykZIrh7+is=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=

View File

@ -9,6 +9,7 @@ import (
"log"
"time"
"github.com/fiatjaf/bip340"
"github.com/fiatjaf/go-nostr/event"
"github.com/fiatjaf/go-nostr/filter"
nostrutils "github.com/fiatjaf/go-nostr/utils"
@ -121,11 +122,18 @@ func (r *RelayPool) Add(url string, policy *Policy) error {
if subscription, ok := r.subscriptions[channel]; ok {
var event event.Event
json.Unmarshal(jsonMessage[2], &event)
// check signature of all received events, ignore invalid
ok, _ := event.CheckSignature()
if !ok {
continue
}
// check if the event matches the desired filter, ignore otherwise
if !subscription.filters.Match(&event) {
continue
}
subscription.Events <- EventMessage{
Relay: nm,
Event: event,
@ -153,7 +161,7 @@ func (r *RelayPool) Remove(url string) {
delete(r.websockets, nm)
}
func (r *RelayPool) Sub(filter filter.EventFilter) *Subscription {
func (r *RelayPool) Sub(filters filter.EventFilters) *Subscription {
random := make([]byte, 7)
rand.Read(random)
@ -170,17 +178,25 @@ func (r *RelayPool) Sub(filter filter.EventFilter) *Subscription {
subscription.UniqueEvents = make(chan event.Event)
r.subscriptions[subscription.channel] = &subscription
subscription.Sub(&filter)
subscription.Sub(filters)
return &subscription
}
func (r *RelayPool) PublishEvent(evt *event.Event) (*event.Event, chan PublishStatus, error) {
status := make(chan PublishStatus, 1)
if r.SecretKey == nil && evt.Sig == "" {
if r.SecretKey == nil && (evt.PubKey == "" || evt.Sig == "") {
return nil, status, errors.New("PublishEvent needs either a signed event to publish or to have been configured with a .SecretKey.")
}
if evt.PubKey == "" {
secretKeyN, err := bip340.ParsePrivateKey(*r.SecretKey)
if err != nil {
return nil, status, fmt.Errorf("The pool's global SecretKey is invalid: %w", err)
}
evt.PubKey = fmt.Sprintf("%x", bip340.GetPublicKey(secretKeyN))
}
if evt.Sig == "" {
err := evt.Sign(*r.SecretKey)
if err != nil {
@ -197,7 +213,7 @@ func (r *RelayPool) PublishEvent(evt *event.Event) (*event.Event, chan PublishSt
}
status <- PublishStatus{relay, PublishStatusSent}
subscription := r.Sub(filter.EventFilter{ID: evt.ID})
subscription := r.Sub(filter.EventFilters{{ID: evt.ID}})
for {
select {
case event := <-subscription.UniqueEvents:

View File

@ -10,8 +10,8 @@ type Subscription struct {
channel string
relays map[string]*websocket.Conn
filter *filter.EventFilter
Events chan EventMessage
filters filter.EventFilters
Events chan EventMessage
started bool
UniqueEvents chan event.Event
@ -38,17 +38,17 @@ func (subscription Subscription) Unsub() {
}
}
func (subscription Subscription) Sub(filter *filter.EventFilter) {
if filter != nil {
subscription.filter = filter
}
func (subscription Subscription) Sub(filters filter.EventFilters) {
for _, ws := range subscription.relays {
ws.WriteJSON([]interface{}{
message := []interface{}{
"REQ",
subscription.channel,
subscription.filter,
})
}
for _, filter := range subscription.filters {
message = append(message, filter)
}
ws.WriteJSON(message)
}
if !subscription.started {
@ -79,9 +79,14 @@ func (subscription Subscription) removeRelay(relay string) {
func (subscription Subscription) addRelay(relay string, ws *websocket.Conn) {
subscription.relays[relay] = ws
ws.WriteJSON([]interface{}{
message := []interface{}{
"REQ",
subscription.channel,
subscription.filter,
})
}
for _, filter := range subscription.filters {
message = append(message, filter)
}
ws.WriteJSON(message)
}