migrate off of main nostr repository.

This commit is contained in:
fiatjaf 2021-01-13 23:46:06 -03:00
commit e9cbff07a4
11 changed files with 848 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
relay-lite
relay-full
*.sqlite

5
Makefile Normal file
View File

@ -0,0 +1,5 @@
relay-lite: $(shell find . -name "*.go")
go build -ldflags="-s -w" -o ./relay-lite
relay-full: $(shell find . -name "*.go")
go build -ldflags="-s -w" -tags full -o ./relay-full

128
event.go Normal file
View File

@ -0,0 +1,128 @@
package main
import (
"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, _ := json.Marshal(arr)
return serialized
}
// 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)
}

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
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/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
)

85
go.sum Normal file
View File

@ -0,0 +1,85 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.0.0-20190109040709-5bda5314ca95 h1:bmv+LE3sbjb/M06u2DBi92imeKj7KnCUBOvyZYqI8d8=
github.com/btcsuite/btcd v0.0.0-20190109040709-5bda5314ca95/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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/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=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
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=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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-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=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/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=
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=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

392
handlers.go Normal file
View File

@ -0,0 +1,392 @@
package main
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/gorilla/websocket"
"golang.org/x/time/rate"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = pongWait / 2
// Maximum message size allowed from peer.
maxMessageSize = 512000
)
var ratelimiter = rate.NewLimiter(rate.Every(time.Second*40), 2)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func handleWebsocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Warn().Err(err).Msg("failed to upgrade websocket")
return
}
// reader
go func() {
defer func() {
conn.Close()
}()
conn.SetReadLimit(maxMessageSize)
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
typ, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Warn().Err(err).Msg("unexpected close error")
}
break
}
if typ == websocket.PingMessage {
conn.WriteMessage(websocket.PongMessage, nil)
continue
}
text := string(message)
switch {
case text == "PING":
conn.WriteMessage(websocket.TextMessage, []byte("PONG"))
case strings.HasPrefix(text, "{"):
// it's a new event
err = saveEvent(message)
case strings.HasPrefix(text, "sub-key:"):
watchPubKey(strings.TrimSpace(text[8:]), conn)
case strings.HasPrefix(text, "unsub-key:"):
unwatchPubKey(strings.TrimSpace(text[10:]), conn)
case strings.HasPrefix(text, "req-feed:"):
err = requestFeed(message[len([]byte("req-feed:")):], conn)
case strings.HasPrefix(text, "req-event:"):
err = requestEvent(message[len([]byte("req-event")):], conn)
case strings.HasPrefix(text, "req-key:"):
err = requestKey(message[len([]byte("req-key")):], conn)
}
if err != nil {
errj, _ := json.Marshal([]interface{}{
"notice",
err.Error(),
})
conn.WriteMessage(websocket.TextMessage, errj)
continue
}
}
}()
// writer
go func() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
conn.Close()
}()
for {
select {
case <-ticker.C:
conn.SetWriteDeadline(time.Now().Add(writeWait))
err := conn.WriteMessage(websocket.TextMessage, []byte("PING"))
if err != nil {
log.Warn().Err(err).Msg("error writing ping, closing websocket")
return
}
conn.WriteMessage(websocket.PingMessage, nil)
}
}
}()
}
func saveEvent(body []byte) error {
if !ratelimiter.Allow() {
return errors.New("rate-limit")
}
var evt Event
err := json.Unmarshal(body, &evt)
if err != nil {
log.Warn().Err(err).Msg("couldn't decode body")
return errors.New("failed to decode event")
}
// disallow large contents
if len(evt.Content) > 1000 {
log.Warn().Err(err).Msg("event content too large")
return errors.New("event content too large")
}
// check serialization
serialized := evt.Serialize()
// assign ID
hash := sha256.Sum256(serialized)
evt.ID = hex.EncodeToString(hash[:])
// check signature (requires the ID to be set)
if ok, err := evt.CheckSignature(); err != nil {
log.Warn().Err(err).Msg("signature verification error")
return errors.New("signature verification error")
} else if !ok {
log.Warn().Err(err).Msg("signature invalid")
return errors.New("signature invalid")
}
// react to different kinds of events
switch evt.Kind {
case KindSetMetadata:
// delete past set_metadata events from this user
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 1`, evt.PubKey)
case KindTextNote:
// do nothing
case 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:
// delete past contact lists from this same pubkey
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 3`, evt.PubKey)
}
// insert
tagsj, _ := json.Marshal(evt.Tags)
_, err = db.Exec(`
INSERT INTO event (id, pubkey, created_at, kind, tags, content, sig)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`, evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, tagsj, evt.Content, evt.Sig)
if err != nil {
if strings.Index(err.Error(), "UNIQUE") != -1 {
// already exists
return nil
}
log.Warn().Err(err).Str("pubkey", evt.PubKey).Msg("failed to save")
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, `
SELECT * FROM event
WHERE ref = $1
LIMIT $2
`, 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")
}
}()
return nil
}

94
listener.go Normal file
View File

@ -0,0 +1,94 @@
package main
import (
"encoding/json"
"sync"
"github.com/gorilla/websocket"
)
var watchers = make(map[string][]*websocket.Conn)
var backwatchers = make(map[*websocket.Conn][]string)
var wlock = sync.Mutex{}
func watchPubKey(key string, ws *websocket.Conn) {
wlock.Lock()
defer wlock.Unlock()
currentKeys, _ := backwatchers[ws]
backwatchers[ws] = append(currentKeys, key)
if wss, ok := watchers[key]; ok {
watchers[key] = append(wss, ws)
} else {
watchers[key] = []*websocket.Conn{ws}
}
}
func unwatchPubKey(excludedKey string, ws *websocket.Conn) {
wlock.Lock()
defer wlock.Unlock()
if wss, ok := watchers[excludedKey]; ok {
newWss := make([]*websocket.Conn, len(wss)-1)
var i = 0
for _, existingWs := range wss {
if existingWs == ws {
continue
}
newWss[i] = existingWs
i++
}
watchers[excludedKey] = newWss
}
currentKeys, _ := backwatchers[ws]
newKeys := make([]string, 0, len(currentKeys))
for _, currentKey := range currentKeys {
if excludedKey == currentKey {
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)
}
}
}

53
main.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"net/http"
"os"
"time"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/kelseyhightower/envconfig"
"github.com/rs/cors"
"github.com/rs/zerolog"
)
type Settings struct {
Host string `envconfig:"HOST" default:"0.0.0.0"`
Port string `envconfig:"PORT" default:"7447"`
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
SQLiteDatabase string `envconfig:"SQLITE_DATABASE"`
}
var s Settings
var err error
var db *sqlx.DB
var log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stderr})
var router = mux.NewRouter()
func main() {
err = envconfig.Process("", &s)
if err != nil {
log.Fatal().Err(err).Msg("couldn't process envconfig")
}
db, err = initDB()
if err != nil {
log.Fatal().Err(err).Msg("failed to open database")
}
// NIP01
router.Path("/ws").Methods("GET").HandlerFunc(handleWebsocket)
srv := &http.Server{
Handler: cors.Default().Handler(router),
Addr: s.Host + ":" + s.Port,
WriteTimeout: 2 * time.Second,
ReadTimeout: 2 * time.Second,
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
}
log.Debug().Str("addr", srv.Addr).Msg("listening")
srv.ListenAndServe()
}

6
notice.go Normal file
View File

@ -0,0 +1,6 @@
package main
type Notice struct {
Kind string `json:"kind"`
Message string `json:"message"`
}

32
postgresql.go Normal file
View File

@ -0,0 +1,32 @@
// +build full
package main
import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
func initDB() (*sqlx.DB, error) {
db, err := sqlx.Connect("postgres", s.PostgresDatabase)
if err != nil {
return nil, err
}
_, err = db.Exec(`
CREATE TABLE event (
id text NOT NULL,
pubkey text NOT NULL,
created_at integer NOT NULL,
kind integer NOT NULL,
tags jsonb NOT NULL,
content text NOT NULL,
sig text NOT NULL
);
CREATE UNIQUE INDEX ididx ON event (id);
CREATE INDEX pubkeytimeidx ON event (pubkey, created_at);
`)
log.Print(err)
return db, nil
}

31
sqlite.go Normal file
View File

@ -0,0 +1,31 @@
// +build !full
package main
import (
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
func initDB() (*sqlx.DB, error) {
db, err := sqlx.Connect("sqlite3", s.SQLiteDatabase)
if err != nil {
return nil, err
}
_, err = db.Exec(`
CREATE TABLE event (
id text NOT NULL,
pubkey text NOT NULL,
created_at integer NOT NULL,
kind integer NOT NULL,
tags text NOT NULL,
content text NOT NULL,
sig text NOT NULL
);
CREATE UNIQUE INDEX ididx ON event (id);
CREATE INDEX pubkeytimeidx ON event (pubkey, created_at);
`)
return db, nil
}