turn relayer into a server framework and put actual relay code into ./basic

This commit is contained in:
fiatjaf
2021-12-25 21:22:40 -03:00
parent ac93e5c028
commit 30eae726c1
12 changed files with 268 additions and 203 deletions

7
basic/Makefile Normal file
View File

@@ -0,0 +1,7 @@
relayer: $(shell find . -name "*.go")
go build -ldflags="-s -w" -o ./relayer
deploy: relayer
ssh root@turgot 'systemctl stop relayer'
scp relayer turgot:relayer/relayer
ssh root@turgot 'systemctl start relayer'

15
basic/cleanup.go Normal file
View File

@@ -0,0 +1,15 @@
package main
import (
"time"
"github.com/jmoiron/sqlx"
)
// every hour, delete all very old events
func cleanupRoutine(db *sqlx.DB) {
for {
time.Sleep(60 * time.Minute)
db.Exec(`DELETE FROM event WHERE created_at < $1`, time.Now().AddDate(0, -3, 0))
}
}

44
basic/main.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"fmt"
"github.com/fiatjaf/relayer"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/reflectx"
"github.com/kelseyhightower/envconfig"
)
type BasicRelay struct {
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
DB *sqlx.DB
}
func (b *BasicRelay) Name() string {
return "BasicRelay"
}
func (b *BasicRelay) Init() error {
err := envconfig.Process("", b)
if err != nil {
return fmt.Errorf("couldn't process envconfig: %w", err)
}
if db, err := initDB(b.PostgresDatabase); err != nil {
return fmt.Errorf("failed to open database: %w", err)
} else {
db.Mapper = reflectx.NewMapperFunc("json", sqlx.NameMapper)
b.DB = db
}
go cleanupRoutine(b.DB)
return nil
}
func main() {
var b BasicRelay
relayer.Start(&b)
}

33
basic/postgresql.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/rs/zerolog/log"
)
func initDB(dburl string) (*sqlx.DB, error) {
db, err := sqlx.Connect("postgres", dburl)
if err != nil {
return nil, err
}
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS ididx ON event (id);
CREATE UNIQUE INDEX IF NOT EXISTS pubkeytimeidx ON event (pubkey, created_at);
`)
log.Print(err)
return db, nil
}
const tagConditions = `jsonb_path_match(tags, '$[*][1] == $value', jsonb_build_object('value', ?::text))`

86
basic/query.go Normal file
View File

@@ -0,0 +1,86 @@
package main
import (
"database/sql"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/fiatjaf/go-nostr/event"
"github.com/fiatjaf/go-nostr/filter"
"github.com/rs/zerolog/log"
)
func (b *BasicRelay) QueryEvents(
filter *filter.EventFilter,
) (events []event.Event, err error) {
var conditions []string
var params []interface{}
if filter == nil {
err = errors.New("filter cannot be null")
return
}
if filter.ID != "" {
conditions = append(conditions, "id = ?")
params = append(params, filter.ID)
}
if filter.Kind != nil && *filter.Kind != 0 {
conditions = append(conditions, "kind = ?")
params = append(params, filter.Kind)
}
if filter.Authors != nil {
if len(filter.Authors) == 0 {
// authors being [] means you won't get anything
return
} else {
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, tagConditions)
params = append(params, filter.TagEvent)
}
if filter.TagProfile != "" {
conditions = append(conditions, tagConditions)
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")
}
query := b.DB.Rebind("SELECT * FROM event WHERE " +
strings.Join(conditions, " AND ") +
" ORDER BY created_at LIMIT 100")
err = b.DB.Select(&events, query, 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
}

55
basic/save.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/fiatjaf/go-nostr/event"
)
func (b *BasicRelay) SaveEvent(evt *event.Event) error {
// disallow large contents
if len(evt.Content) > 1000 {
return errors.New("event content too large")
}
// react to different kinds of events
switch evt.Kind {
case event.KindSetMetadata:
// delete past set_metadata events from this user
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 0`, evt.PubKey)
case event.KindRecommendServer:
// delete past recommend_server events equal to this one
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 2 AND content = $2`,
evt.PubKey, evt.Content)
case event.KindContactList:
// delete past contact lists from this same pubkey
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 3`, evt.PubKey)
default:
// delete all but the 10 most recent ones
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND created_at < (
SELECT created_at FROM event WHERE pubkey = $1
ORDER BY created_at DESC OFFSET 10 LIMIT 1
)`,
evt.PubKey, evt.Kind)
}
// insert
tagsj, _ := json.Marshal(evt.Tags)
_, err := b.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
}
return fmt.Errorf("failed to save event from %s", evt.PubKey)
}
return nil
}