adapt to new nip-01.

This commit is contained in:
fiatjaf
2021-02-14 21:08:02 -03:00
parent 80d244b2c3
commit 9c992c90dc
8 changed files with 183 additions and 457 deletions

133
event.go
View File

@@ -1,133 +0,0 @@
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/fiatjaf/schnorr"
)
const (
KindSetMetadata uint8 = 0
KindTextNote uint8 = 1
KindRecommendServer uint8 = 2
KindContactList uint8 = 3
)
type Event struct {
ID string `db:"id" json:"id"` // it's the hash of the serialized event
PubKey string `db:"pubkey" json:"pubkey"`
CreatedAt uint32 `db:"created_at" json:"created_at"`
Kind uint8 `db:"kind" json:"kind"`
Tags Tags `db:"tags" json:"tags"`
Content string `db:"content" json:"content"`
Sig string `db:"sig" json:"sig"`
}
type Tags []Tag
func (t *Tags) Scan(src interface{}) error {
var jtags []byte = make([]byte, 0)
switch v := src.(type) {
case []byte:
jtags = v
case string:
jtags = []byte(v)
default:
return errors.New("couldn't scan tags, it's not a json string")
}
json.Unmarshal(jtags, &t)
return nil
}
type Tag []interface{}
// Serialize outputs a byte array that can be hashed/signed to identify/authenticate
func (evt *Event) Serialize() []byte {
// the serialization process is just putting everything into a JSON array
// so the order is kept
arr := make([]interface{}, 6)
// version: 0
arr[0] = 0
// pubkey
arr[1] = evt.PubKey
// created_at
arr[2] = int64(evt.CreatedAt)
// kind
arr[3] = int64(evt.Kind)
// tags
if evt.Tags != nil {
arr[4] = evt.Tags
} else {
arr[4] = make([]bool, 0)
}
// content
arr[5] = evt.Content
serialized := new(bytes.Buffer)
enc := json.NewEncoder(serialized)
enc.SetEscapeHTML(false)
_ = enc.Encode(arr)
return serialized.Bytes()[:serialized.Len()-1] // Encode add new line char
}
// CheckSignature checks if the signature is valid for the id
// (which is a hash of the serialized event content).
// 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)
if err != nil {
return false, err
}
if len(pubkeyb) != 32 {
return false, fmt.Errorf("pubkey must be 32 bytes, not %d", len(pubkeyb))
}
// check tags
for _, tag := range evt.Tags {
for _, item := range tag {
switch item.(type) {
case string, int64, float64, int, bool:
// fine
default:
// not fine
return false, fmt.Errorf("tag contains an invalid value %v", item)
}
}
}
sig, 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))
}
var p [32]byte
copy(p[:], pubkeyb)
var s [64]byte
copy(s[:], sig)
h := sha256.Sum256(evt.Serialize())
return schnorr.Verify(p, h, s)
}

9
go.mod
View File

@@ -3,17 +3,16 @@ module github.com/fiatjaf/nostr-relay
go 1.15
require (
github.com/fiatjaf/schnorr v0.2.1-hack
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/fiatjaf/go-nostr v0.0.0-00010101000000-000000000000
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/jmoiron/sqlx v1.2.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/kr/pretty v0.2.1
github.com/lib/pq v1.8.0
github.com/mattn/go-sqlite3 v1.14.4
github.com/rs/cors v1.7.0
github.com/rs/zerolog v1.20.0
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
gopkg.in/antage/eventsource.v1 v1.0.0-20150318155416-803f4c5af225
google.golang.org/appengine v1.6.7 // indirect
)
replace github.com/fiatjaf/go-nostr => /home/fiatjaf/comp/go-nostr

22
go.sum
View File

@@ -15,13 +15,13 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
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/schnorr v0.2.1-hack h1:6NwQNN5O4+ZUm8KliT+l198vDVH8ovv8AJ8CiL4hvs0=
github.com/fiatjaf/schnorr v0.2.1-hack/go.mod h1:6aMsVxPxyO6awpdmNkfkJ8vXqsmUOeGCHp2CdG5LPR0=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
@@ -36,11 +36,6 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -63,6 +58,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -71,12 +67,12 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/antage/eventsource.v1 v1.0.0-20150318155416-803f4c5af225 h1:xy+AV3uSExoRQc2qWXeZdbhFGwBFK/AmGlrBZEjbvuQ=
gopkg.in/antage/eventsource.v1 v1.0.0-20150318155416-803f4c5af225/go.mod h1:SiXNRpUllqhl+GIw2V/BtKI7BUlz+uxov9vBFtXHqh8=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=

View File

@@ -2,15 +2,15 @@ package main
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/fiatjaf/go-nostr/event"
"github.com/fiatjaf/go-nostr/filter"
"github.com/gorilla/websocket"
)
@@ -69,40 +69,66 @@ func handleWebsocket(w http.ResponseWriter, r *http.Request) {
continue
}
text := string(message)
go func(message []byte) {
var err error
switch {
case text == "PING":
conn.WriteMessage(websocket.TextMessage, []byte("PONG"))
defer func() {
if err != nil {
conn.WriteJSON([]interface{}{"NOTICE", err.Error()})
}
}()
case strings.HasPrefix(text, "{"):
// it's a new event
err = saveEvent(message)
var request []json.RawMessage
err = json.Unmarshal(message, &request)
if err == nil && len(request) < 2 {
err = errors.New("request has less than parameters")
return
}
case strings.HasPrefix(text, "sub-key:"):
watchPubKey(strings.TrimSpace(text[8:]), conn)
var typ string
json.Unmarshal(request[0], &typ)
case strings.HasPrefix(text, "unsub-key:"):
unwatchPubKey(strings.TrimSpace(text[10:]), conn)
switch typ {
case "EVENT":
// it's a new event
err = saveEvent(request[1])
case strings.HasPrefix(text, "req-feed:"):
err = requestFeed(message[len([]byte("req-feed:")):], conn)
case "REQ":
var id string
json.Unmarshal(request[0], &id)
if id == "" {
err = errors.New("REQ has no <id>")
return
}
case strings.HasPrefix(text, "req-event:"):
err = requestEvent(message[len([]byte("req-event:")):], conn)
filters := make([]*filter.EventFilter, len(request)-2)
for i, filterReq := range request[2:] {
err = json.Unmarshal(filterReq, &filters[i])
if err != nil {
return
}
case strings.HasPrefix(text, "req-key:"):
err = requestKey(message[len([]byte("req-key:")):], conn)
}
events, err := queryEvents(filters[i])
if err == nil {
for _, event := range events {
conn.WriteJSON([]interface{}{"EVENT", id, event})
}
}
}
if err != nil {
errj, _ := json.Marshal([]interface{}{
"notice",
err.Error(),
})
conn.WriteMessage(websocket.TextMessage, errj)
continue
}
setListener(id, conn, filters)
case "CLOSE":
var id string
json.Unmarshal(request[0], &id)
if id == "" {
err = errors.New("CLOSE has no <id>")
return
}
removeListener(id)
}
}(message)
}
}()
@@ -129,7 +155,7 @@ func handleWebsocket(w http.ResponseWriter, r *http.Request) {
}
func saveEvent(body []byte) error {
var evt Event
var evt event.Event
err := json.Unmarshal(body, &evt)
if err != nil {
log.Warn().Err(err).Msg("couldn't decode body")
@@ -160,16 +186,16 @@ func saveEvent(body []byte) error {
// react to different kinds of events
switch evt.Kind {
case KindSetMetadata:
case event.KindSetMetadata:
// delete past set_metadata events from this user
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 0`, evt.PubKey)
case KindTextNote:
case event.KindTextNote:
// do nothing
case KindRecommendServer:
case event.KindRecommendServer:
// delete past recommend_server events equal to this one
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 2 AND content = $2`,
evt.PubKey, evt.Content)
case KindContactList:
case event.KindContactList:
// delete past contact lists from this same pubkey
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 3`, evt.PubKey)
}
@@ -190,193 +216,6 @@ func saveEvent(body []byte) error {
return errors.New("failed to save event")
}
notifyPubKeyEvent(evt.PubKey, &evt)
return nil
}
func requestFeed(body []byte, conn *websocket.Conn) error {
var data struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
}
json.Unmarshal(body, &data)
if data.Limit <= 0 || data.Limit > 100 {
data.Limit = 50
}
if data.Offset < 0 {
data.Offset = 0
} else if data.Offset > 500 {
return errors.New("offset over 500")
}
keys, ok := backwatchers[conn]
if !ok {
return errors.New("not subscribed to anything")
}
inkeys := make([]string, 0, len(keys))
for _, key := range keys {
// to prevent sql attack here we will check if these keys are valid 32byte hex
parsed, err := hex.DecodeString(key)
if err != nil || len(parsed) != 32 {
continue
}
inkeys = append(inkeys, fmt.Sprintf("'%x'", parsed))
}
var lastUpdates []Event
err := db.Select(&lastUpdates, `
SELECT *
FROM event
WHERE pubkey IN (`+strings.Join(inkeys, ",")+`)
ORDER BY created_at DESC
LIMIT $1
OFFSET $2
`, data.Limit, data.Offset)
if err != nil && err != sql.ErrNoRows {
log.Warn().Err(err).Interface("keys", keys).Msg("failed to fetch events")
return errors.New("failed to fetch events")
}
for _, evt := range lastUpdates {
jevent, _ := json.Marshal([]interface{}{
evt,
"p",
})
conn.WriteMessage(websocket.TextMessage, jevent)
}
return nil
}
func requestKey(body []byte, conn *websocket.Conn) error {
var data struct {
Key string `json:"key"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
json.Unmarshal(body, &data)
if data.Key == "" {
return errors.New("invalid pubkey")
}
if data.Limit <= 0 || data.Limit > 100 {
data.Limit = 30
}
if data.Offset < 0 {
data.Offset = 0
} else if data.Offset > 300 {
return errors.New("offset over 300")
}
go func() {
var metadata Event
if err := db.Get(&metadata, `
SELECT * FROM event
WHERE pubkey = $1 AND kind = 0
`, data.Key); err == nil {
jevent, _ := json.Marshal([]interface{}{
metadata,
"r",
})
conn.WriteMessage(websocket.TextMessage, jevent)
} else if err != sql.ErrNoRows {
log.Warn().Err(err).
Str("key", data.Key).
Msg("error fetching metadata from requested user")
}
}()
go func() {
var lastUpdates []Event
if err := db.Select(&lastUpdates, `
SELECT * FROM event
WHERE pubkey = $1 AND kind != 0
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`, data.Key, data.Limit, data.Offset); err == nil {
for _, evt := range lastUpdates {
jevent, _ := json.Marshal([]interface{}{
evt,
"r",
})
conn.WriteMessage(websocket.TextMessage, jevent)
}
} else if err != sql.ErrNoRows {
log.Warn().Err(err).
Str("key", data.Key).
Msg("error fetching updates from requested user")
}
}()
return nil
}
func requestEvent(body []byte, conn *websocket.Conn) error {
var data struct {
Id string `json:"id"`
Limit int `json:"limit"`
}
json.Unmarshal(body, &data)
if data.Id == "" {
return errors.New("no id provided")
}
if data.Limit > 100 || data.Limit <= 0 {
data.Limit = 50
}
go func() {
// get requested event
var evt Event
if err := db.Get(&evt, `
SELECT * FROM event WHERE id = $1
`, data.Id); err == nil {
jevent, _ := json.Marshal([]interface{}{
evt,
"r",
})
conn.WriteMessage(websocket.TextMessage, jevent)
} else if err != sql.ErrNoRows {
log.Warn().Err(err).
Str("key", data.Id).
Msg("error fetching a specific event")
}
for _, tag := range evt.Tags {
log.Print(tag)
// get referenced event TODO
// var ref Event
// if err := db.Get(&ref, `
// SELECT * FROM event WHERE id = $1
// `, evt.Ref); err == nil {
// jevent, _ := json.Marshal(ref)
// (*es).SendEventMessage(string(jevent), "r", "")
// } else if err != sql.ErrNoRows {
// log.Warn().Err(err).
// Str("key", data.Id).Str("ref", evt.Ref).
// Msg("error fetching a referenced event")
// }
}
}()
go func() {
// get events that reference this
var related []Event
if err := db.Select(&related,
relatedEventsQuery,
data.Id, data.Limit); err == nil {
for _, evt := range related {
jevent, _ := json.Marshal([]interface{}{
evt,
"r",
})
conn.WriteMessage(websocket.TextMessage, jevent)
}
} else if err != sql.ErrNoRows {
log.Warn().Err(err).
Str("key", data.Id).
Msg("error fetching events that reference requested event")
}
}()
notifyListeners(&evt)
return nil
}

View File

@@ -1,99 +1,61 @@
package main
import (
"encoding/json"
"sync"
"github.com/fiatjaf/go-nostr/event"
"github.com/fiatjaf/go-nostr/filter"
"github.com/gorilla/websocket"
)
var watchers = make(map[string][]*websocket.Conn)
var backwatchers = make(map[*websocket.Conn][]string)
var wlock = sync.Mutex{}
type Listener struct {
ws *websocket.Conn
filters []*filter.EventFilter
}
func watchPubKey(key string, ws *websocket.Conn) {
wlock.Lock()
defer wlock.Unlock()
var listeners = make(map[string]*Listener)
var listenersMutex = sync.Mutex{}
currentKeys, _ := backwatchers[ws]
backwatchers[ws] = append(currentKeys, key)
func setListener(id string, conn *websocket.Conn, filters []*filter.EventFilter) {
listenersMutex.Lock()
defer func() {
listenersMutex.Unlock()
}()
if wss, ok := watchers[key]; ok {
watchers[key] = append(wss, ws)
} else {
watchers[key] = []*websocket.Conn{ws}
listeners[id] = &Listener{
ws: conn,
filters: filters,
}
}
func unwatchPubKey(excludedKey string, ws *websocket.Conn) {
wlock.Lock()
defer wlock.Unlock()
func removeListener(id string) {
listenersMutex.Lock()
defer func() {
listenersMutex.Unlock()
}()
if wss, ok := watchers[excludedKey]; ok {
newWss := make([]*websocket.Conn, len(wss)-1)
delete(listeners, id)
}
var i = 0
for _, existingWs := range wss {
if existingWs == ws {
continue
func notifyListeners(event *event.Event) {
listenersMutex.Lock()
defer func() {
listenersMutex.Unlock()
}()
for id, listener := range listeners {
match := false
for _, filter := range listener.filters {
if filter.Matches(event) {
match = true
break
}
if i == len(wss) {
// if we reach this point it is because the key we were
// excluding wasn't here in the first place
return
}
newWss[i] = existingWs
i++
}
watchers[excludedKey] = newWss
}
currentKeys, _ := backwatchers[ws]
newKeys := make([]string, 0, len(currentKeys))
for _, currentKey := range currentKeys {
if excludedKey == currentKey {
if !match {
continue
}
newKeys = append(newKeys, currentKey)
}
backwatchers[ws] = newKeys
}
func removeFromWatchers(es *websocket.Conn) {
wlock.Lock()
defer wlock.Unlock()
for _, key := range backwatchers[es] {
if arr, ok := watchers[key]; ok {
newarr := make([]*websocket.Conn, len(arr)-1)
i := 0
for _, oldes := range arr {
if oldes == es {
continue
}
newarr[i] = oldes
i++
}
watchers[key] = newarr
}
}
delete(backwatchers, es)
}
func notifyPubKeyEvent(key string, evt *Event) {
wlock.Lock()
arr, ok := watchers[key]
wlock.Unlock()
if ok {
for _, conn := range arr {
jevent, _ := json.Marshal([]interface{}{
evt,
"n",
})
conn.WriteMessage(websocket.TextMessage, jevent)
}
listener.ws.WriteJSON([]interface{}{"EVENT", id, event})
}
}

View File

@@ -31,8 +31,4 @@ CREATE INDEX pubkeytimeidx ON event (pubkey, created_at);
return db, nil
}
const relatedEventsQuery = `
SELECT * FROM event
WHERE tags @@ '$[*][1] == "' || $1 || '"'
LIMIT $2
`
const relatedEventsCondition = `tags @@ '$[*][1] == "' || ? || '"'`

71
query.go Normal file
View File

@@ -0,0 +1,71 @@
package main
import (
"database/sql"
"encoding/hex"
"fmt"
"strings"
"github.com/fiatjaf/go-nostr/event"
"github.com/fiatjaf/go-nostr/filter"
)
func queryEvents(filter *filter.EventFilter) (events []event.Event, err error) {
var conditions []string
var params []interface{}
if filter.ID != "" {
conditions = append(conditions, "id = ?")
params = append(params, filter.ID)
}
if filter.Author != "" {
conditions = append(conditions, "pubkey = ?")
params = append(params, filter.Author)
}
if filter.Authors != nil {
inkeys := make([]string, 0, len(filter.Authors))
for _, key := range filter.Authors {
// to prevent sql attack here we will check if
// these keys are valid 32byte hex
parsed, err := hex.DecodeString(key)
if err != nil || len(parsed) != 32 {
continue
}
inkeys = append(inkeys, fmt.Sprintf("'%x'", parsed))
}
conditions = append(conditions, `pubkey IN (`+strings.Join(inkeys, ",")+`)`)
}
if filter.TagEvent != "" {
conditions = append(conditions, relatedEventsCondition)
params = append(params, filter.TagEvent)
}
if filter.TagProfile != "" {
conditions = append(conditions, relatedEventsCondition)
params = append(params, filter.TagProfile)
}
if filter.Since != 0 {
conditions = append(conditions, "created_at > ?")
params = append(params, filter.Since)
}
if len(conditions) == 0 {
// fallback
conditions = append(conditions, "true")
}
err = db.Select(&events, "SELECT * FROM event WHERE "+
strings.Join(conditions, " AND ")+
"ORDER BY created_at LIMIT 100", params...,
)
if err != nil && err != sql.ErrNoRows {
log.Warn().Err(err).Interface("filter", filter).Msg("failed to fetch events")
err = fmt.Errorf("failed to fetch events: %w", err)
}
return
}

View File

@@ -30,8 +30,4 @@ CREATE INDEX pubkeytimeidx ON event (pubkey, created_at);
return db, nil
}
const relatedEventsQuery = `
SELECT * FROM event
WHERE tags LIKE '%' || $1 || '%'
LIMIT $2
`
const relatedEventsCondition = `tags LIKE '%' || ? || '%'`