bring in mysql backend from relayer.

This commit is contained in:
fiatjaf
2023-10-31 16:06:44 -03:00
parent a82b9b4bde
commit 28fc5b0571
7 changed files with 371 additions and 23 deletions

10
go.mod
View File

@@ -7,7 +7,7 @@ require (
github.com/bmatsuo/lmdb-go v1.8.0
github.com/dgraph-io/badger/v4 v4.2.0
github.com/elastic/go-elasticsearch/v8 v8.10.1
github.com/fiatjaf/khatru v0.0.3
github.com/go-sql-driver/mysql v1.6.0
github.com/jmoiron/sqlx v1.3.5
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.17
@@ -16,7 +16,6 @@ require (
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
@@ -27,7 +26,6 @@ require (
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect
github.com/elastic/go-elasticsearch/v7 v7.6.0 // indirect
github.com/fasthttp/websocket v1.5.3 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
@@ -40,21 +38,19 @@ require (
github.com/google/flatbuffers v1.12.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/rs/cors v1.7.0 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.47.0 // indirect
go.opencensus.io v0.22.5 // indirect
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

21
go.sum
View File

@@ -1,7 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aquasecurity/esquery v0.2.0 h1:9WWXve95TE8hbm3736WB7nS6Owl8UGDeu+0jiyE9ttA=
github.com/aquasecurity/esquery v0.2.0/go.mod h1:VU+CIFR6C+H142HHZf9RUkp4Eedpo9UrEKeCQHWf9ao=
github.com/bmatsuo/lmdb-go v1.8.0 h1:ohf3Q4xjXZBKh4AayUY4bb2CXuhRAI8BYGlJq08EfNA=
@@ -14,6 +12,7 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/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=
@@ -35,12 +34,8 @@ github.com/elastic/go-elasticsearch/v7 v7.6.0 h1:sYpGLpEFHgLUKLsZUBfuaVI9QgHjS3J
github.com/elastic/go-elasticsearch/v7 v7.6.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
github.com/elastic/go-elasticsearch/v8 v8.10.1 h1:JJ3i2DimYTsJcUoEGbg6tNB0eehTNdid9c5kTR1TGuI=
github.com/elastic/go-elasticsearch/v8 v8.10.1/go.mod h1:GU1BJHO7WeamP7UhuElYwzzHtvf9SDmeVpSSy9+o6Qg=
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fiatjaf/khatru v0.0.3 h1:vy0Dgztx4hSCu6lGOD/vq1HBQnGBI+VU/Iq9UEXJrpE=
github.com/fiatjaf/khatru v0.0.3/go.mod h1:8shKDuVtrdLfsuHV4FBC3qYTTXnyfOLAgKqUt4u+Okk=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -82,8 +77,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -102,10 +99,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
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/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
@@ -116,10 +109,6 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=

12
mysql/delete.go Normal file
View File

@@ -0,0 +1,12 @@
package mysql
import (
"context"
"github.com/nbd-wtf/go-nostr"
)
func (b MySQLBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
_, err := b.DB.ExecContext(ctx, "DELETE FROM event WHERE id = ?", evt.ID)
return err
}

71
mysql/init.go Normal file
View File

@@ -0,0 +1,71 @@
package mysql
import (
"strings"
"github.com/fiatjaf/eventstore"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/reflectx"
)
const (
queryLimit = 100
queryIDsLimit = 500
queryAuthorsLimit = 500
queryKindsLimit = 10
queryTagsLimit = 10
)
var _ eventstore.Storage = (*MySQLBackend)(nil)
var ddls = []string{
`CREATE TABLE IF NOT EXISTS event (
id char(64) NOT NULL primary key,
pubkey char(64) NOT NULL,
created_at int NOT NULL,
kind integer NOT NULL,
tags json NOT NULL,
content text NOT NULL,
sig text NOT NULL);`,
`CREATE INDEX pubkeyprefix ON event (pubkey);`,
`CREATE INDEX timeidx ON event (created_at DESC);`,
`CREATE INDEX kindidx ON event (kind);`,
}
func (b *MySQLBackend) Init() error {
db, err := sqlx.Connect("mysql", b.DatabaseURL)
if err != nil {
return err
}
// sqlx default is 0 (unlimited), while mysql by default accepts up to 100 connections
db.SetMaxOpenConns(80)
db.Mapper = reflectx.NewMapperFunc("json", sqlx.NameMapper)
b.DB = db
for _, ddl := range ddls {
_, err := b.DB.Exec(ddl)
if err != nil && !strings.HasPrefix(err.Error(), `Error 1061: Duplicate key name`) {
return err
}
}
if b.QueryLimit == 0 {
b.QueryLimit = queryLimit
}
if b.QueryIDsLimit == 0 {
b.QueryIDsLimit = queryIDsLimit
}
if b.QueryAuthorsLimit == 0 {
b.QueryAuthorsLimit = queryAuthorsLimit
}
if b.QueryKindsLimit == 0 {
b.QueryKindsLimit = queryKindsLimit
}
if b.QueryTagsLimit == 0 {
b.QueryTagsLimit = queryTagsLimit
}
return err
}

15
mysql/mysql.go Normal file
View File

@@ -0,0 +1,15 @@
package mysql
import (
"github.com/jmoiron/sqlx"
)
type MySQLBackend struct {
*sqlx.DB
DatabaseURL string
QueryLimit int
QueryIDsLimit int
QueryAuthorsLimit int
QueryKindsLimit int
QueryTagsLimit int
}

189
mysql/query.go Normal file
View File

@@ -0,0 +1,189 @@
package mysql
import (
"context"
"database/sql"
"encoding/hex"
"fmt"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/nbd-wtf/go-nostr"
)
func (b MySQLBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (ch chan *nostr.Event, err error) {
ch = make(chan *nostr.Event)
query, params, err := b.queryEventsSql(filter, false)
if err != nil {
close(ch)
return nil, err
}
rows, err := b.DB.Query(query, params...)
if err != nil && err != sql.ErrNoRows {
close(ch)
return nil, fmt.Errorf("failed to fetch events using query %q: %w", query, err)
}
go func() {
defer rows.Close()
defer close(ch)
for rows.Next() {
var evt nostr.Event
var timestamp int64
err := rows.Scan(&evt.ID, &evt.PubKey, &timestamp,
&evt.Kind, &evt.Tags, &evt.Content, &evt.Sig)
if err != nil {
return
}
evt.CreatedAt = nostr.Timestamp(timestamp)
ch <- &evt
}
}()
return ch, nil
}
func (b MySQLBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
query, params, err := b.queryEventsSql(filter, true)
if err != nil {
return 0, err
}
var count int64
if err = b.DB.QueryRow(query, params...).Scan(&count); err != nil && err != sql.ErrNoRows {
return 0, fmt.Errorf("failed to fetch events using query %q: %w", query, err)
}
return count, nil
}
func (b MySQLBackend) queryEventsSql(filter nostr.Filter, doCount bool) (string, []any, error) {
var conditions []string
var params []any
if filter.IDs != nil {
if len(filter.IDs) > b.QueryIDsLimit {
// too many ids, fail everything
return "", nil, nil
}
likeids := make([]string, 0, len(filter.IDs))
for _, id := range filter.IDs {
// to prevent sql attack here we will check if
// these ids are valid 32byte hex
parsed, err := hex.DecodeString(id)
if err != nil || len(parsed) != 32 {
continue
}
likeids = append(likeids, fmt.Sprintf("id LIKE '%x%%'", parsed))
}
if len(likeids) == 0 {
// ids being [] mean you won't get anything
return "", nil, nil
}
conditions = append(conditions, "("+strings.Join(likeids, " OR ")+")")
}
if filter.Authors != nil {
if len(filter.Authors) > b.QueryAuthorsLimit {
// too many authors, fail everything
return "", nil, nil
}
likekeys := 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
}
likekeys = append(likekeys, fmt.Sprintf("pubkey LIKE '%x%%'", parsed))
}
if len(likekeys) == 0 {
// authors being [] mean you won't get anything
return "", nil, nil
}
conditions = append(conditions, "("+strings.Join(likekeys, " OR ")+")")
}
if filter.Kinds != nil {
if len(filter.Kinds) > b.QueryKindsLimit {
// too many kinds, fail everything
return "", nil, nil
}
if len(filter.Kinds) == 0 {
// kinds being [] mean you won't get anything
return "", nil, nil
}
// no sql injection issues since these are ints
inkinds := make([]string, len(filter.Kinds))
for i, kind := range filter.Kinds {
inkinds[i] = strconv.Itoa(kind)
}
conditions = append(conditions, `kind IN (`+strings.Join(inkinds, ",")+`)`)
}
tagQuery := make([]string, 0, 1)
for _, values := range filter.Tags {
if len(values) == 0 {
// any tag set to [] is wrong
return "", nil, nil
}
// add these tags to the query
tagQuery = append(tagQuery, values...)
if len(tagQuery) > b.QueryTagsLimit {
// too many tags, fail everything
return "", nil, nil
}
}
// we use a very bad implementation in which we only check the tag values and
// ignore the tag names
for _, tagValue := range tagQuery {
params = append(params, "%"+tagValue+"%")
conditions = append(conditions, "tags LIKE ?")
}
if filter.Since != nil {
conditions = append(conditions, "created_at >= ?")
params = append(params, filter.Since)
}
if filter.Until != nil {
conditions = append(conditions, "created_at <= ?")
params = append(params, filter.Until)
}
if len(conditions) == 0 {
// fallback
conditions = append(conditions, "true")
}
if filter.Limit < 1 || filter.Limit > b.QueryLimit {
params = append(params, b.QueryLimit)
} else {
params = append(params, filter.Limit)
}
var query string
if doCount {
query = sqlx.Rebind(sqlx.BindType("mysql"), `SELECT
COUNT(*)
FROM event WHERE `+
strings.Join(conditions, " AND ")+
" ORDER BY created_at DESC LIMIT ?")
} else {
query = sqlx.Rebind(sqlx.BindType("mysql"), `SELECT
id, pubkey, created_at, kind, tags, content, sig
FROM event WHERE `+
strings.Join(conditions, " AND ")+
" ORDER BY created_at DESC LIMIT ?")
}
return query, params, nil
}

76
mysql/save.go Normal file
View File

@@ -0,0 +1,76 @@
package mysql
import (
"context"
"encoding/json"
"github.com/fiatjaf/eventstore"
"github.com/nbd-wtf/go-nostr"
)
func (b *MySQLBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
deleteQuery, deleteParams, shouldDelete := deleteBeforeSaveSql(evt)
if shouldDelete {
_, _ = b.DB.ExecContext(ctx, deleteQuery, deleteParams...)
}
sql, params, _ := saveEventSql(evt)
res, err := b.DB.ExecContext(ctx, sql, params...)
if err != nil {
return err
}
nr, err := res.RowsAffected()
if err != nil {
return err
}
if nr == 0 {
return eventstore.ErrDupEvent
}
return nil
}
func deleteBeforeSaveSql(evt *nostr.Event) (string, []any, bool) {
// react to different kinds of events
var (
query = ""
params []any
shouldDelete bool
)
if evt.Kind == nostr.KindSetMetadata || evt.Kind == nostr.KindContactList || (10000 <= evt.Kind && evt.Kind < 20000) {
// delete past events from this user
query = `DELETE FROM event WHERE pubkey = ? AND kind = ?`
params = []any{evt.PubKey, evt.Kind}
shouldDelete = true
} else if evt.Kind == nostr.KindRecommendServer {
// delete past recommend_server events equal to this one
query = `DELETE FROM event WHERE pubkey = ? AND kind = ? AND content = ?`
params = []any{evt.PubKey, evt.Kind, evt.Content}
shouldDelete = true
} else if evt.Kind >= 30000 && evt.Kind < 40000 {
// NIP-33
d := evt.Tags.GetFirst([]string{"d"})
if d != nil {
query = `DELETE FROM event WHERE pubkey = ? AND kind = ? AND tags LIKE ?`
params = []any{evt.PubKey, evt.Kind, d.Value()}
shouldDelete = true
}
}
return query, params, shouldDelete
}
func saveEventSql(evt *nostr.Event) (string, []any, error) {
const query = `INSERT INTO event (
id, pubkey, created_at, kind, tags, content, sig)
VALUES (?, ?, ?, ?, ?, ?, ?)`
var (
tagsj, _ = json.Marshal(evt.Tags)
params = []any{evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, tagsj, evt.Content, evt.Sig}
)
return query, params, nil
}