mirror of
https://github.com/aljazceru/khatru.git
synced 2026-01-31 10:54:18 +01:00
relayer, second attempt, now much better.
This commit is contained in:
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: build all relays for all platforms
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
make-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-release@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
build-all-for-all:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- make-release
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin, freebsd, openbsd]
|
||||
goarch: [amd64, arm64]
|
||||
project: ['basic', 'whitelisted', 'expensive', 'rss-bridge', 'search']
|
||||
exclude:
|
||||
- goarch: arm64
|
||||
goos: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: wangyoucao577/go-release-action@v1.30
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
goversion: "1.18"
|
||||
project_path: ${{ matrix.project }}
|
||||
binary_name: "relayer-${{ matrix.project }}-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
overwrite: true
|
||||
@@ -1,6 +1 @@
|
||||
Nostr Relay Framework -- use it to implement your own custom relay.
|
||||
|
||||
There is an example/reference implementation at [basic](/examples/basic/). Binaries for that are also available under [Releases](
|
||||
https://github.com/fiatjaf/relayer/releases).
|
||||
|
||||
<a href="https://godoc.org/github.com/fiatjaf/relayer"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
|
||||
khatru
|
||||
|
||||
138
add-event.go
138
add-event.go
@@ -1,56 +1,134 @@
|
||||
package relayer
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2/storage"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
var nip20prefixmatcher = regexp.MustCompile(`^\w+: `)
|
||||
|
||||
// AddEvent has a business rule to add an event to the relayer
|
||||
func AddEvent(ctx context.Context, relay Relay, evt *nostr.Event) (accepted bool, message string) {
|
||||
func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
if evt == nil {
|
||||
return false, ""
|
||||
return fmt.Errorf("event is nil")
|
||||
}
|
||||
|
||||
store := relay.Storage(ctx)
|
||||
advancedSaver, _ := store.(AdvancedSaver)
|
||||
msg := ""
|
||||
rejecting := false
|
||||
for _, reject := range rl.RejectEvent {
|
||||
rejecting, msg = reject(ctx, evt)
|
||||
if rejecting {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !relay.AcceptEvent(ctx, evt) {
|
||||
return false, "blocked: event blocked by relay"
|
||||
if rejecting {
|
||||
if msg == "" {
|
||||
msg = "no reason"
|
||||
}
|
||||
return fmt.Errorf(msg)
|
||||
}
|
||||
|
||||
if 20000 <= evt.Kind && evt.Kind < 30000 {
|
||||
// do not store ephemeral events
|
||||
} else {
|
||||
if advancedSaver != nil {
|
||||
advancedSaver.BeforeSave(ctx, evt)
|
||||
}
|
||||
|
||||
if saveErr := store.SaveEvent(ctx, evt); saveErr != nil {
|
||||
switch saveErr {
|
||||
case storage.ErrDupEvent:
|
||||
return true, saveErr.Error()
|
||||
default:
|
||||
errmsg := saveErr.Error()
|
||||
if nip20prefixmatcher.MatchString(errmsg) {
|
||||
return false, errmsg
|
||||
} else {
|
||||
return false, fmt.Sprintf("error: failed to save (%s)", errmsg)
|
||||
if evt.Kind == 0 || evt.Kind == 3 || (10000 <= evt.Kind && evt.Kind < 20000) {
|
||||
// replaceable event, delete before storing
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
previous := <-ch
|
||||
if previous != nil {
|
||||
for _, del := range rl.DeleteEvent {
|
||||
del(ctx, previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if 30000 <= evt.Kind && evt.Kind < 40000 {
|
||||
// parameterized replaceable event, delete before storing
|
||||
d := evt.Tags.GetFirst([]string{"d", ""})
|
||||
if d != nil {
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}, Tags: nostr.TagMap{"d": []string{d.Value()}}})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
previous := <-ch
|
||||
if previous != nil {
|
||||
for _, del := range rl.DeleteEvent {
|
||||
del(ctx, previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if advancedSaver != nil {
|
||||
advancedSaver.AfterSave(evt)
|
||||
// store
|
||||
for _, store := range rl.StoreEvent {
|
||||
if saveErr := store(ctx, evt); saveErr != nil {
|
||||
switch saveErr {
|
||||
case ErrDupEvent:
|
||||
return nil
|
||||
default:
|
||||
errmsg := saveErr.Error()
|
||||
if nip20prefixmatcher.MatchString(errmsg) {
|
||||
return saveErr
|
||||
} else {
|
||||
return fmt.Errorf("error: failed to save (%s)", errmsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ons := range rl.OnEventSaved {
|
||||
ons(ctx, evt)
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners(evt)
|
||||
|
||||
return true, ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) error {
|
||||
// event deletion -- nip09
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "e" {
|
||||
// first we fetch the event
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, nostr.Filter{IDs: []string{tag[1]}})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
target := <-ch
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
// got the event, now check if the user can delete it
|
||||
acceptDeletion := target.PubKey == evt.PubKey
|
||||
var msg string
|
||||
if acceptDeletion == false {
|
||||
msg = "you are not the author of this event"
|
||||
}
|
||||
// but if we have a function to overwrite this outcome, use that instead
|
||||
for _, odo := range rl.OverwriteDeletionOutcome {
|
||||
acceptDeletion, msg = odo(ctx, target, evt)
|
||||
}
|
||||
if acceptDeletion {
|
||||
// delete it
|
||||
for _, del := range rl.DeleteEvent {
|
||||
del(ctx, target)
|
||||
}
|
||||
} else {
|
||||
// fail and stop here
|
||||
return fmt.Errorf("blocked: %s", msg)
|
||||
}
|
||||
|
||||
// don't try to query this same event again
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
5
errors.go
Normal file
5
errors.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package khatru
|
||||
|
||||
import "fmt"
|
||||
|
||||
var ErrDupEvent = fmt.Errorf("duplicate: event already exists")
|
||||
26
examples/basic-badgern/main.go
Normal file
26
examples/basic-badgern/main.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/plugins/storage/badgern"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
db := badgern.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"}
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
26
examples/basic-elasticsearch/main.go
Normal file
26
examples/basic-elasticsearch/main.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/plugins/storage/elasticsearch"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
db := elasticsearch.ElasticsearchStorage{URL: ""}
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
28
examples/basic-lmdbn/main.go
Normal file
28
examples/basic-lmdbn/main.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/plugins/storage/lmdbn"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
db := lmdbn.LMDBBackend{Path: "/tmp/khatru-lmdbn-tmp"}
|
||||
os.MkdirAll(db.Path, 0755)
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
26
examples/basic-postgres/main.go
Normal file
26
examples/basic-postgres/main.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/plugins/storage/postgresql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
db := postgresql.PostgresBackend{DatabaseURL: "postgresql://localhost:5432/tmp-khatru-relay"}
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
26
examples/basic-sqlite3/main.go
Normal file
26
examples/basic-sqlite3/main.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/plugins/storage/sqlite3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
db := sqlite3.SQLite3Backend{DatabaseURL: "/tmp/khatru-sqlite-tmp"}
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
1
examples/basic/.gitignore
vendored
1
examples/basic/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
relayer-basic
|
||||
@@ -1,8 +0,0 @@
|
||||
FROM golang:1.18
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY ./ .
|
||||
|
||||
RUN go get -d -v ./...
|
||||
RUN go install -v ./...
|
||||
RUN cd basic && make
|
||||
@@ -1,2 +0,0 @@
|
||||
relayer-basic: $(shell find .. -name "*.go")
|
||||
CC=$$(which musl-gcc) go build -ldflags='-s -w -linkmode external -extldflags "-static"' -o ./relayer-basic
|
||||
@@ -1,24 +0,0 @@
|
||||
relayer basic
|
||||
=============
|
||||
|
||||
- a basic relay implementation based on relayer.
|
||||
- uses postgres, which I think must be over version 12 since it uses generated columns.
|
||||
- it has some antispam limits, tries to delete old stuff so things don't get out of control, and some other small optimizations.
|
||||
|
||||
running
|
||||
-------
|
||||
|
||||
grab a binary from the releases page and run it with the environment variable POSTGRESQL_DATABASE set to some postgres url:
|
||||
|
||||
POSTGRESQL_DATABASE=postgres://name:pass@localhost:5432/dbname ./relayer-basic
|
||||
|
||||
it also accepts a HOST and a PORT environment variables.
|
||||
|
||||
compiling
|
||||
---------
|
||||
|
||||
if you know Go you already know this:
|
||||
|
||||
go install github.com/fiatjaf/relayer/basic
|
||||
|
||||
or something like that.
|
||||
@@ -1,32 +0,0 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
|
||||
relay:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./basic/Dockerfile
|
||||
environment:
|
||||
PORT: 2700
|
||||
POSTGRESQL_DATABASE: postgres://nostr:nostr@postgres:5432/nostr?sslmode=disable
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 2700:2700
|
||||
command: "./basic/relayer-basic"
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: nostr
|
||||
POSTGRES_USER: nostr
|
||||
POSTGRES_PASSWORD: nostr
|
||||
POSTGRES_HOST_AUTH_METHOD: trust # allow all connections without a password. This is *not* recommended for prod
|
||||
ports:
|
||||
- 5432:5432
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U nostr"] # database username here - nostr, should be changed if other user
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -1,73 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/fiatjaf/relayer/v2/storage/postgresql"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
|
||||
|
||||
storage *postgresql.PostgresBackend
|
||||
}
|
||||
|
||||
func (r *Relay) Name() string {
|
||||
return "BasicRelay"
|
||||
}
|
||||
|
||||
func (r *Relay) Storage(ctx context.Context) relayer.Storage {
|
||||
return r.storage
|
||||
}
|
||||
|
||||
func (r *Relay) Init() error {
|
||||
err := envconfig.Process("", r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't process envconfig: %w", err)
|
||||
}
|
||||
|
||||
// every hour, delete all very old events
|
||||
go func() {
|
||||
db := r.Storage(context.TODO()).(*postgresql.PostgresBackend)
|
||||
|
||||
for {
|
||||
time.Sleep(60 * time.Minute)
|
||||
db.DB.Exec(`DELETE FROM event WHERE created_at < $1`, time.Now().AddDate(0, -3, 0).Unix()) // 3 months
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(ctx context.Context, evt *nostr.Event) bool {
|
||||
// block events that are too large
|
||||
jsonb, _ := json.Marshal(evt)
|
||||
if len(jsonb) > 10000 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := Relay{}
|
||||
if err := envconfig.Process("", &r); err != nil {
|
||||
log.Fatalf("failed to read from env: %v", err)
|
||||
return
|
||||
}
|
||||
r.storage = &postgresql.PostgresBackend{DatabaseURL: r.PostgresDatabase}
|
||||
server, err := relayer.NewServer(&r)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
if err := server.Start("0.0.0.0", 7447); err != nil {
|
||||
log.Fatalf("server terminated: %v", err)
|
||||
}
|
||||
}
|
||||
40
examples/exclusive/main.go
Normal file
40
examples/exclusive/main.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/plugins"
|
||||
"github.com/fiatjaf/khatru/plugins/storage/lmdbn"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
db := lmdbn.LMDBBackend{Path: "/tmp/exclusive"}
|
||||
os.MkdirAll(db.Path, 0755)
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
relay.RejectEvent = append(relay.RejectEvent, plugins.PreventTooManyIndexableTags(10))
|
||||
relay.RejectFilter = append(relay.RejectFilter, plugins.NoPrefixFilters, plugins.NoComplexFilters)
|
||||
|
||||
relay.OnEventSaved = append(relay.OnEventSaved, func(ctx context.Context, event *nostr.Event) {
|
||||
})
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
|
||||
func deleteStuffThatCanBeFoundElsewhere() {
|
||||
}
|
||||
1
examples/expensive/.gitignore
vendored
1
examples/expensive/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
relayer-expensive
|
||||
@@ -1,2 +0,0 @@
|
||||
relayer-expensive: $(shell find .. -name "*.go")
|
||||
CC=$$(which musl-gcc) go build -ldflags='-s -w -linkmode external -extldflags "-static"' -o ./relayer-expensive
|
||||
@@ -1,31 +0,0 @@
|
||||
expensive-relay, a sybil-free corner of nostr
|
||||
=============================================
|
||||
|
||||
- a nostr relay implementation based on relayer.
|
||||
- uses postgres, which I think must be over version 12 since it uses generated columns.
|
||||
- requires users to manually register themselves to be able to publish events and pay a fee. this should prevent spam.
|
||||
- aside from that it's basically the same thing as relayer basic.
|
||||
|
||||
running
|
||||
-------
|
||||
|
||||
this requires a recent CLN version with Commando.
|
||||
|
||||
grab a binary from the releases page and run it with the following environment variables:
|
||||
|
||||
POSTGRESQL_DATABASE=postgresql://...
|
||||
CLN_NODE_ID=02fed8723...
|
||||
CLN_HOST=127.0.0.1:9735
|
||||
CLN_RUNE=...
|
||||
TICKET_PRICE_SATS=500
|
||||
|
||||
adjust the values above accordingly.
|
||||
|
||||
compiling
|
||||
---------
|
||||
|
||||
if you know Go you already know this:
|
||||
|
||||
go install github.com/fiatjaf/relayer/expensive
|
||||
|
||||
or something like that.
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleWebpage(w http.ResponseWriter, rq *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`
|
||||
<meta charset=utf-8>
|
||||
<title>expensive relay</title>
|
||||
<h1>expensive relay</h1>
|
||||
<a href="https://github.com/fiatjaf/expensive-relay">https://github.com/fiatjaf/expensive-relay</a>
|
||||
<p>this is a nostr relay that only accepts events published from keys that pay a registration fee. this is an antispam measure. you can still be banned if you're spamming or doing something bad.</p>
|
||||
<p>to register your nostr public key, type it below and click the button.</p>
|
||||
<form>
|
||||
<label>
|
||||
nostr public key:
|
||||
<input name=pubkey />
|
||||
</label>
|
||||
<button>Get Invoice</button>
|
||||
</form>
|
||||
<p id=message></p>
|
||||
<a id=link><canvas id=qr /></a>
|
||||
<code id=invoice></code>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
|
||||
<script>
|
||||
document.querySelector('form').addEventListener('submit', async ev => {
|
||||
ev.preventDefault()
|
||||
let res = await (await fetch('/invoice?pubkey=' + ev.target.pubkey.value)).text()
|
||||
let { bolt11, error } = JSON.parse(res)
|
||||
if (bolt11) {
|
||||
invoice.innerHTML = bolt11
|
||||
link.href = 'lightning:' + bolt11
|
||||
new QRious({
|
||||
element: qr,
|
||||
value: bolt11.toUpperCase(),
|
||||
size: 300
|
||||
});
|
||||
} else {
|
||||
message.innerHTML = error
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 10px auto;
|
||||
width: 800px;
|
||||
max-width: 90%;
|
||||
}
|
||||
</style>
|
||||
`))
|
||||
}
|
||||
|
||||
func handleInvoice(w http.ResponseWriter, rq *http.Request, r *Relay) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
invoice, err := generateInvoice(r, rq.URL.Query().Get("pubkey"))
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Error string `json:"error"`
|
||||
}{err.Error()})
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Invoice string `json:"bolt11"`
|
||||
}{invoice})
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
lnsocket "github.com/jb55/lnsocket/go"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func generateLabel(pubkey string) string { return fmt.Sprintf("relayer-expensive:ticket:%s", pubkey) }
|
||||
|
||||
func generateInvoice(r *Relay, pubkey string) (string, error) {
|
||||
label := generateLabel(pubkey)
|
||||
cln := lnsocket.LNSocket{}
|
||||
cln.GenKey()
|
||||
|
||||
err := cln.ConnectAndInit(r.CLNHost, r.CLNNodeId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cln.Disconnect()
|
||||
|
||||
// check if there is an invoice already
|
||||
jparams, _ := json.Marshal(map[string]any{
|
||||
"label": label,
|
||||
})
|
||||
result, _ := cln.Rpc(r.CLNRune, "listinvoices", string(jparams))
|
||||
if gjson.Get(result, "result.invoices.#").Int() == 1 {
|
||||
timestamp := time.Now().Unix()
|
||||
if (gjson.Get(result, "result.invoices.0.expires_at").Int() > timestamp) {
|
||||
return gjson.Get(result, "result.invoices.0.bolt11").String(), nil
|
||||
}
|
||||
jparams, _ := json.Marshal(map[string]any{
|
||||
"label": label,
|
||||
"status": "expired",
|
||||
})
|
||||
cln.Rpc(r.CLNRune, "delinvoice", string(jparams))
|
||||
}
|
||||
|
||||
// otherwise generate an invoice
|
||||
jparams, _ = json.Marshal(map[string]any{
|
||||
"amount_msat": r.TicketPriceSats * 1000,
|
||||
"label": label,
|
||||
"description": fmt.Sprintf("%s's ticket for writing to relayer-expensive", pubkey),
|
||||
})
|
||||
result, err = cln.Rpc(r.CLNRune, "invoice", string(jparams))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resErr := gjson.Get(result, "error")
|
||||
if resErr.Type != gjson.Null {
|
||||
if resErr.Type == gjson.JSON {
|
||||
return "", errors.New(resErr.Get("message").String())
|
||||
} else if resErr.Type == gjson.String {
|
||||
return "", errors.New(resErr.String())
|
||||
}
|
||||
return "", fmt.Errorf("Unknown commando error: '%v'", resErr)
|
||||
}
|
||||
|
||||
invoice := gjson.Get(result, "result.bolt11")
|
||||
if invoice.Type != gjson.String {
|
||||
return "", fmt.Errorf("No bolt11 result found in invoice response, got %v", result)
|
||||
}
|
||||
|
||||
return invoice.String(), nil
|
||||
}
|
||||
|
||||
func checkInvoicePaidOk(pubkey string) bool {
|
||||
cln := lnsocket.LNSocket{}
|
||||
cln.GenKey()
|
||||
|
||||
err := cln.ConnectAndInit(r.CLNHost, r.CLNNodeId)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer cln.Disconnect()
|
||||
|
||||
jparams, _ := json.Marshal(map[string]any{
|
||||
"label": generateLabel(pubkey),
|
||||
})
|
||||
result, _ := cln.Rpc(r.CLNRune, "listinvoices", string(jparams))
|
||||
|
||||
return gjson.Get(result, "result.invoices.0.status").String() == "paid"
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/fiatjaf/relayer/v2/storage/postgresql"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
|
||||
CLNNodeId string `envconfig:"CLN_NODE_ID"`
|
||||
CLNHost string `envconfig:"CLN_HOST"`
|
||||
CLNRune string `envconfig:"CLN_RUNE"`
|
||||
TicketPriceSats int64 `envconfig:"TICKET_PRICE_SATS"`
|
||||
|
||||
storage *postgresql.PostgresBackend
|
||||
}
|
||||
|
||||
var r = &Relay{}
|
||||
|
||||
func (r *Relay) Name() string {
|
||||
return "ExpensiveRelay"
|
||||
}
|
||||
|
||||
func (r *Relay) Storage(ctx context.Context) relayer.Storage {
|
||||
return r.storage
|
||||
}
|
||||
|
||||
func (r *Relay) Init() error {
|
||||
// every hour, delete all very old events
|
||||
go func() {
|
||||
db := r.Storage(context.TODO()).(*postgresql.PostgresBackend)
|
||||
|
||||
for {
|
||||
time.Sleep(60 * time.Minute)
|
||||
db.DB.Exec(`DELETE FROM event WHERE created_at < $1`, time.Now().AddDate(0, -3, 0).Unix()) // 6 months
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(ctx context.Context, evt *nostr.Event) bool {
|
||||
// only accept they have a good preimage for a paid invoice for their public key
|
||||
if !checkInvoicePaidOk(evt.PubKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
// block events that are too large
|
||||
jsonb, _ := json.Marshal(evt)
|
||||
if len(jsonb) > 100000 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := Relay{}
|
||||
if err := envconfig.Process("", &r); err != nil {
|
||||
log.Fatalf("failed to read from env: %v", err)
|
||||
return
|
||||
}
|
||||
r.storage = &postgresql.PostgresBackend{DatabaseURL: r.PostgresDatabase}
|
||||
server, err := relayer.NewServer(&r)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
// special handlers
|
||||
server.Router().HandleFunc("/", handleWebpage)
|
||||
server.Router().HandleFunc("/invoice", func(w http.ResponseWriter, rq *http.Request) {
|
||||
handleInvoice(w, rq, &r)
|
||||
})
|
||||
if err := server.Start("0.0.0.0", 7447); err != nil {
|
||||
log.Fatalf("server terminated: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
DOMAIN=dev2.hazlitt.fiatjaf.com
|
||||
SECRET=ie32uyg48o72iv
|
||||
HOST=0.0.0.0
|
||||
PORT=3002
|
||||
2
examples/rss-bridge/.gitignore
vendored
2
examples/rss-bridge/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
relayer-rss-bridge
|
||||
db
|
||||
@@ -1,2 +0,0 @@
|
||||
relayer-rss-bridge: $(shell find . -name "*.go")
|
||||
CC=$$(which musl-gcc) go build -ldflags="-s -w -linkmode external -extldflags '-static'" -o ./relayer-rss-bridge
|
||||
@@ -1,26 +0,0 @@
|
||||
rss-bridge, a relay that creates virtual nostr profiles for each rss feed
|
||||
=========================================================================
|
||||
|
||||
- a nostr relay implementation based on relayer.
|
||||
- doesn't accept any events, only emits them.
|
||||
- does so by manually reading and parsing rss feeds.
|
||||
|
||||

|
||||
|
||||
running
|
||||
-------
|
||||
|
||||
grab a binary from the releases page and run it with the following environment variable:
|
||||
|
||||
SECRET=just-a-random-string-to-be-used-when-generating-the-virtual-private-keys
|
||||
|
||||
it will create a local database file to store the currently known rss feed urls.
|
||||
|
||||
compiling
|
||||
---------
|
||||
|
||||
if you know Go you already know this:
|
||||
|
||||
go install github.com/fiatjaf/relayer/rss-bridge
|
||||
|
||||
or something like that.
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
MANIFEST-000164
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,43 +0,0 @@
|
||||
[Version]
|
||||
pebble_version=0.1
|
||||
|
||||
[Options]
|
||||
bytes_per_sync=524288
|
||||
cache_size=8388608
|
||||
cleaner=delete
|
||||
compaction_debt_concurrency=1073741824
|
||||
comparer=leveldb.BytewiseComparator
|
||||
delete_range_flush_delay=0s
|
||||
disable_wal=false
|
||||
flush_split_bytes=4194304
|
||||
format_major_version=1
|
||||
l0_compaction_concurrency=10
|
||||
l0_compaction_threshold=4
|
||||
l0_stop_writes_threshold=12
|
||||
lbase_max_bytes=67108864
|
||||
max_concurrent_compactions=1
|
||||
max_manifest_file_size=134217728
|
||||
max_open_files=1000
|
||||
mem_table_size=4194304
|
||||
mem_table_stop_writes_threshold=2
|
||||
min_compaction_rate=4194304
|
||||
min_deletion_rate=0
|
||||
min_flush_rate=1048576
|
||||
merger=pebble.concatenate
|
||||
read_compaction_rate=16000
|
||||
read_sampling_multiplier=16
|
||||
strict_wal_tail=true
|
||||
table_cache_shards=4
|
||||
table_property_collectors=[]
|
||||
validate_on_ingest=false
|
||||
wal_dir=
|
||||
wal_bytes_per_sync=0
|
||||
|
||||
[Level "0"]
|
||||
block_restart_interval=16
|
||||
block_size=4096
|
||||
compression=Snappy
|
||||
filter_policy=none
|
||||
filter_type=table
|
||||
index_block_size=4096
|
||||
target_file_size=2097152
|
||||
@@ -1,157 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
strip "github.com/grokify/html-strip-tags-go"
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/rif/cache2go"
|
||||
)
|
||||
|
||||
var (
|
||||
fp = gofeed.NewParser()
|
||||
feedCache = cache2go.New(512, time.Minute*19)
|
||||
client = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
type Entity struct {
|
||||
PrivateKey string
|
||||
URL string
|
||||
}
|
||||
|
||||
var types = []string{
|
||||
"rss+xml",
|
||||
"atom+xml",
|
||||
"feed+json",
|
||||
"text/xml",
|
||||
"application/xml",
|
||||
}
|
||||
|
||||
func getFeedURL(url string) string {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil || resp.StatusCode >= 300 {
|
||||
return ""
|
||||
}
|
||||
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
for _, typ := range types {
|
||||
if strings.Contains(ct, typ) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(ct, "text/html") {
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, typ := range types {
|
||||
href, _ := doc.Find(fmt.Sprintf("link[type*='%s']", typ)).Attr("href")
|
||||
if href == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(href, "http") {
|
||||
href, _ = urljoin(url, href)
|
||||
}
|
||||
return href
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseFeed(url string) (*gofeed.Feed, error) {
|
||||
if feed, ok := feedCache.Get(url); ok {
|
||||
return feed.(*gofeed.Feed), nil
|
||||
}
|
||||
|
||||
feed, err := fp.ParseURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// cleanup a little so we don't store too much junk
|
||||
for i := range feed.Items {
|
||||
feed.Items[i].Content = ""
|
||||
}
|
||||
feedCache.Set(url, feed)
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
func feedToSetMetadata(pubkey string, feed *gofeed.Feed) nostr.Event {
|
||||
metadata := map[string]string{
|
||||
"name": feed.Title,
|
||||
"about": feed.Description + "\n\n" + feed.Link,
|
||||
}
|
||||
if feed.Image != nil {
|
||||
metadata["picture"] = feed.Image.URL
|
||||
}
|
||||
content, _ := json.Marshal(metadata)
|
||||
|
||||
createdAt := time.Now()
|
||||
if feed.PublishedParsed != nil {
|
||||
createdAt = *feed.PublishedParsed
|
||||
}
|
||||
|
||||
evt := nostr.Event{
|
||||
PubKey: pubkey,
|
||||
CreatedAt: nostr.Timestamp(createdAt.Unix()),
|
||||
Kind: nostr.KindSetMetadata,
|
||||
Tags: nostr.Tags{},
|
||||
Content: string(content),
|
||||
}
|
||||
evt.ID = string(evt.Serialize())
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func itemToTextNote(pubkey string, item *gofeed.Item) nostr.Event {
|
||||
content := ""
|
||||
if item.Title != "" {
|
||||
content = "**" + item.Title + "**\n\n"
|
||||
}
|
||||
content += strip.StripTags(item.Description)
|
||||
if len(content) > 250 {
|
||||
content += content[0:249] + "…"
|
||||
}
|
||||
content += "\n\n" + item.Link
|
||||
|
||||
createdAt := time.Now()
|
||||
if item.UpdatedParsed != nil {
|
||||
createdAt = *item.UpdatedParsed
|
||||
}
|
||||
if item.PublishedParsed != nil {
|
||||
createdAt = *item.PublishedParsed
|
||||
}
|
||||
|
||||
evt := nostr.Event{
|
||||
PubKey: pubkey,
|
||||
CreatedAt: nostr.Timestamp(createdAt.Unix()),
|
||||
Kind: nostr.KindTextNote,
|
||||
Tags: nostr.Tags{},
|
||||
Content: content,
|
||||
}
|
||||
evt.ID = string(evt.Serialize())
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func privateKeyFromFeed(url string) string {
|
||||
m := hmac.New(sha256.New, []byte(relay.Secret))
|
||||
m.Write([]byte(url))
|
||||
r := m.Sum(nil)
|
||||
return hex.EncodeToString(r)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
. "github.com/stevelacy/daz"
|
||||
)
|
||||
|
||||
var head = H("head",
|
||||
H("meta", Attr{"charset": "utf-8"}),
|
||||
H("meta", Attr{
|
||||
"name": "viewport",
|
||||
"content": "width=device-width, initial-scale=1.0",
|
||||
}),
|
||||
H("title", "rsslay"),
|
||||
)
|
||||
|
||||
func handleWebpage(w http.ResponseWriter, r *http.Request) {
|
||||
items := make([]HTML, 0, 200)
|
||||
iter := relay.db.NewIter(nil)
|
||||
for iter.First(); iter.Valid(); iter.Next() {
|
||||
pubkey := string(iter.Key())
|
||||
var entity Entity
|
||||
if err := json.Unmarshal(iter.Value(), &entity); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, H("tr",
|
||||
H("td",
|
||||
H("code",
|
||||
pubkey),
|
||||
),
|
||||
H("td",
|
||||
H("a", Attr{
|
||||
"href": entity.URL,
|
||||
}, entity.URL),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
body := H("body",
|
||||
H("h1", "rsslay"),
|
||||
H("p", "rsslay turns RSS or Atom feeds into ",
|
||||
H("a", Attr{
|
||||
"href": "https://github.com/fiatjaf/nostr",
|
||||
}, "Nostr"),
|
||||
" profiles.",
|
||||
),
|
||||
H("h2", "How to use"),
|
||||
H("ol",
|
||||
H("li", "Get the blog URL or RSS or Atom feed URL and paste below;"),
|
||||
H("li", "Click the button to get its corresponding public key"),
|
||||
H("li", "Add this relay to your Nostr client"),
|
||||
H("li", "Follow the feed's public key from your Nostr client."),
|
||||
),
|
||||
H("form", Attr{
|
||||
"action": "/create",
|
||||
"method": "GET",
|
||||
"class": "my-4",
|
||||
},
|
||||
H("label",
|
||||
H("input", Attr{
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"placeholder": "https://.../feed",
|
||||
}),
|
||||
),
|
||||
H("button", "Get Public Key"),
|
||||
),
|
||||
|
||||
H("h2", "Some of the existing feeds"),
|
||||
H("table", items),
|
||||
H("h2", "Source Code"),
|
||||
H("p", "You can find it at ",
|
||||
H("a", Attr{"href": "https://github.com/fiatjaf/rsslay"},
|
||||
"https://github.com/fiatjaf/rsslay"),
|
||||
),
|
||||
)
|
||||
|
||||
w.Header().Set("content-type", "text/html")
|
||||
w.Write([]byte(
|
||||
H("html",
|
||||
head,
|
||||
body,
|
||||
)()))
|
||||
}
|
||||
|
||||
func handleCreateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("url")
|
||||
|
||||
feedurl := getFeedURL(url)
|
||||
if feedurl == "" {
|
||||
w.WriteHeader(400)
|
||||
fmt.Fprint(w, "couldn't find a feed url")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := parseFeed(feedurl); err != nil {
|
||||
w.WriteHeader(400)
|
||||
fmt.Fprint(w, "bad feed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sk := privateKeyFromFeed(feedurl)
|
||||
pubkey, err := nostr.GetPublicKey(sk)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, "bad private key: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(Entity{
|
||||
PrivateKey: sk,
|
||||
URL: feedurl,
|
||||
})
|
||||
|
||||
if err := relay.db.Set([]byte(pubkey), j, nil); err != nil {
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, "failure: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("saved feed at url %q as pubkey %s", feedurl, pubkey)
|
||||
|
||||
fmt.Fprintf(w, "url : %s\npubkey: %s", feedurl, pubkey)
|
||||
return
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
func urljoin(baseUrl string, elem ...string) (result string, err error) {
|
||||
u, err := url.Parse(baseUrl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(elem) > 0 {
|
||||
elem = append([]string{u.Path}, elem...)
|
||||
u.Path = path.Join(elem...)
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/pebble"
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var relay = &Relay{
|
||||
updates: make(chan nostr.Event),
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
Secret string `envconfig:"SECRET" required:"true"`
|
||||
|
||||
updates chan nostr.Event
|
||||
lastEmitted sync.Map
|
||||
db *pebble.DB
|
||||
}
|
||||
|
||||
func (relay *Relay) Name() string {
|
||||
return "relayer-rss-bridge"
|
||||
}
|
||||
|
||||
func (relay *Relay) Init() error {
|
||||
err := envconfig.Process("", relay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't process envconfig: %w", err)
|
||||
}
|
||||
|
||||
if db, err := pebble.Open("db", nil); err != nil {
|
||||
log.Fatalf("failed to open db: %v", err)
|
||||
} else {
|
||||
relay.db = db
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Minute)
|
||||
|
||||
filters := relayer.GetListeningFilters()
|
||||
log.Printf("checking for updates; %d filters active", len(filters))
|
||||
|
||||
for _, filter := range filters {
|
||||
if filter.Kinds == nil || slices.Contains(filter.Kinds, nostr.KindTextNote) {
|
||||
for _, pubkey := range filter.Authors {
|
||||
if val, closer, err := relay.db.Get([]byte(pubkey)); err == nil {
|
||||
defer closer.Close()
|
||||
|
||||
var entity Entity
|
||||
if err := json.Unmarshal(val, &entity); err != nil {
|
||||
log.Printf("got invalid json from db at key %s: %v", pubkey, err)
|
||||
continue
|
||||
}
|
||||
|
||||
feed, err := parseFeed(entity.URL)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse feed at url %q: %v", entity.URL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, item := range feed.Items {
|
||||
evt := itemToTextNote(pubkey, item)
|
||||
last, ok := relay.lastEmitted.Load(entity.URL)
|
||||
if !ok || time.Unix(last.(int64), 0).Before(evt.CreatedAt.Time()) {
|
||||
evt.Sign(entity.PrivateKey)
|
||||
relay.updates <- evt
|
||||
relay.lastEmitted.Store(entity.URL, last)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (relay *Relay) AcceptEvent(ctx context.Context, _ *nostr.Event) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (relay *Relay) Storage(ctx context.Context) relayer.Storage {
|
||||
return store{relay.db}
|
||||
}
|
||||
|
||||
type store struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
|
||||
func (b store) Init() error { return nil }
|
||||
func (b store) SaveEvent(ctx context.Context, _ *nostr.Event) error {
|
||||
return errors.New("blocked: we don't accept any events")
|
||||
}
|
||||
|
||||
func (b store) DeleteEvent(ctx context.Context, id string, pubkey string) error {
|
||||
return errors.New("blocked: we can't delete any events")
|
||||
}
|
||||
|
||||
func (b store) QueryEvents(ctx context.Context, filter *nostr.Filter) (chan *nostr.Event, error) {
|
||||
if filter.IDs != nil || len(filter.Tags) > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
evts := make(chan *nostr.Event)
|
||||
go func() {
|
||||
for _, pubkey := range filter.Authors {
|
||||
if val, closer, err := relay.db.Get([]byte(pubkey)); err == nil {
|
||||
defer closer.Close()
|
||||
|
||||
var entity Entity
|
||||
if err := json.Unmarshal(val, &entity); err != nil {
|
||||
log.Printf("got invalid json from db at key %s: %v", pubkey, err)
|
||||
continue
|
||||
}
|
||||
|
||||
feed, err := parseFeed(entity.URL)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse feed at url %q: %v", entity.URL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if filter.Kinds == nil || slices.Contains(filter.Kinds, nostr.KindSetMetadata) {
|
||||
evt := feedToSetMetadata(pubkey, feed)
|
||||
|
||||
if filter.Since != nil && evt.CreatedAt.Time().Before(filter.Since.Time()) {
|
||||
continue
|
||||
}
|
||||
if filter.Until != nil && evt.CreatedAt.Time().After(filter.Until.Time()) {
|
||||
continue
|
||||
}
|
||||
|
||||
evt.Sign(entity.PrivateKey)
|
||||
evts <- &evt
|
||||
}
|
||||
|
||||
if filter.Kinds == nil || slices.Contains(filter.Kinds, nostr.KindTextNote) {
|
||||
var last uint32 = 0
|
||||
for _, item := range feed.Items {
|
||||
evt := itemToTextNote(pubkey, item)
|
||||
|
||||
if filter.Since != nil && evt.CreatedAt.Time().Before(filter.Since.Time()) {
|
||||
continue
|
||||
}
|
||||
if filter.Until != nil && evt.CreatedAt.Time().After(filter.Until.Time()) {
|
||||
continue
|
||||
}
|
||||
|
||||
evt.Sign(entity.PrivateKey)
|
||||
|
||||
if evt.CreatedAt.Time().After(time.Unix(int64(last), 0)) {
|
||||
last = uint32(evt.CreatedAt.Time().Unix())
|
||||
}
|
||||
|
||||
evts <- &evt
|
||||
}
|
||||
|
||||
relay.lastEmitted.Store(entity.URL, last)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return evts, nil
|
||||
}
|
||||
|
||||
func (relay *Relay) InjectEvents() chan nostr.Event {
|
||||
return relay.updates
|
||||
}
|
||||
|
||||
func main() {
|
||||
server, err := relayer.NewServer(relay)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
server.Router().HandleFunc("/", handleWebpage)
|
||||
server.Router().HandleFunc("/create", handleCreateFeed)
|
||||
if err := server.Start("0.0.0.0", 7447); err != nil {
|
||||
log.Fatalf("server terminated: %v", err)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
1
examples/search/.gitignore
vendored
1
examples/search/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
search
|
||||
@@ -1,26 +0,0 @@
|
||||
# Search Relay
|
||||
|
||||
Uses ElasticSearch storage backend for all queries, with some basic full text search support.
|
||||
|
||||
Index some events:
|
||||
|
||||
```
|
||||
bzip2 -cd nostr-wellorder-early-1m-v1.jsonl.bz2 | \
|
||||
jq -c '["EVENT", .]' | \
|
||||
awk 'length($0)<131072' | \
|
||||
websocat -n -B 200000 ws://127.0.0.1:7447
|
||||
```
|
||||
|
||||
Do a search:
|
||||
|
||||
```
|
||||
echo '["REQ", "asdf", {"search": "steve", "kinds": [0]}]' | websocat -n ws://127.0.0.1:7447
|
||||
```
|
||||
|
||||
|
||||
## Customize
|
||||
|
||||
Currently the indexing is very basic: It will index the `contents` field for all events where kind != 4.
|
||||
Some additional mapping and pre-processing could add better support for different content types.
|
||||
See comments in `storage/elasticsearch/elasticsearch.go`.
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
|
||||
relay:
|
||||
image: golang
|
||||
# build:
|
||||
# context: ../
|
||||
# dockerfile: ./basic/Dockerfile
|
||||
environment:
|
||||
PORT: 2700
|
||||
ES_URL: http://elasticsearch:9200
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 2700:2700
|
||||
- 7447:7447
|
||||
volumes:
|
||||
- ./nostres:/bin
|
||||
command: "/bin/relay"
|
||||
|
||||
elasticsearch:
|
||||
container_name: elasticsearch
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.6.0
|
||||
restart: always
|
||||
environment:
|
||||
- network.host=0.0.0.0
|
||||
- discovery.type=single-node
|
||||
- cluster.name=docker-cluster
|
||||
- node.name=cluster1-node1
|
||||
- xpack.license.self_generated.type=basic
|
||||
- xpack.security.enabled=false
|
||||
- "ES_JAVA_OPTS=-Xms${ES_MEM:-4g} -Xmx${ES_MEM:-4g}"
|
||||
ports:
|
||||
- '127.0.0.1:9200:9200'
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD-SHELL", "curl --silent --fail elasticsearch:9200/_cluster/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -1,67 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/fiatjaf/relayer/v2/storage/elasticsearch"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
storage *elasticsearch.ElasticsearchStorage
|
||||
}
|
||||
|
||||
func (r *Relay) Name() string {
|
||||
return "SearchRelay"
|
||||
}
|
||||
|
||||
func (r *Relay) Storage(ctx context.Context) relayer.Storage {
|
||||
return r.storage
|
||||
}
|
||||
|
||||
func (r *Relay) Init() error {
|
||||
err := envconfig.Process("", r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't process envconfig: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(ctx context.Context, evt *nostr.Event) bool {
|
||||
// block events that are too large
|
||||
// jsonb, _ := json.Marshal(evt)
|
||||
// if len(jsonb) > 100000 {
|
||||
// return false
|
||||
// }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Relay) BeforeSave(evt *nostr.Event) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (r *Relay) AfterSave(evt *nostr.Event) {
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := Relay{}
|
||||
if err := envconfig.Process("", &r); err != nil {
|
||||
log.Fatalf("failed to read from env: %v", err)
|
||||
return
|
||||
}
|
||||
r.storage = &elasticsearch.ElasticsearchStorage{}
|
||||
server, err := relayer.NewServer(&r)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
if err := server.Start("0.0.0.0", 7447); err != nil {
|
||||
log.Fatalf("server terminated: %v", err)
|
||||
}
|
||||
}
|
||||
1
examples/whitelisted/.gitignore
vendored
1
examples/whitelisted/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
relayer-whitelisted
|
||||
@@ -1,2 +0,0 @@
|
||||
relayer-whitelisted: $(shell find .. -name "*.go")
|
||||
CC=$$(which musl-gcc) go build -ldflags='-s -w -linkmode external -extldflags "-static"' -o ./relayer-whitelisted
|
||||
@@ -1,24 +0,0 @@
|
||||
whitelisted relay
|
||||
=================
|
||||
|
||||
- a basic relay implementation based on relayer.
|
||||
- uses postgres, which I think must be over version 12 since it uses generated columns.
|
||||
- only accepts events from specific pubkeys defined via the environment variable `WHITELIST` (comma-separated).
|
||||
|
||||
running
|
||||
-------
|
||||
|
||||
grab a binary from the releases page and run it with the environment variable POSTGRESQL_DATABASE set to some postgres url:
|
||||
|
||||
POSTGRESQL_DATABASE=postgres://name:pass@localhost:5432/dbname ./relayer-whitelisted
|
||||
|
||||
it also accepts a HOST and a PORT environment variables.
|
||||
|
||||
compiling
|
||||
---------
|
||||
|
||||
if you know Go you already know this:
|
||||
|
||||
go install github.com/fiatjaf/relayer/whitelisted
|
||||
|
||||
or something like that.
|
||||
@@ -1,69 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/fiatjaf/relayer/v2/storage/postgresql"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
|
||||
Whitelist []string `envconfig:"WHITELIST"`
|
||||
|
||||
storage *postgresql.PostgresBackend
|
||||
}
|
||||
|
||||
func (r *Relay) Name() string {
|
||||
return "WhitelistedRelay"
|
||||
}
|
||||
|
||||
func (r *Relay) Storage(ctx context.Context) relayer.Storage {
|
||||
return r.storage
|
||||
}
|
||||
|
||||
func (r *Relay) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(ctx context.Context, evt *nostr.Event) bool {
|
||||
// disallow anything from non-authorized pubkeys
|
||||
found := false
|
||||
for _, pubkey := range r.Whitelist {
|
||||
if pubkey == evt.PubKey {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
||||
// block events that are too large
|
||||
jsonb, _ := json.Marshal(evt)
|
||||
if len(jsonb) > 100000 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := Relay{}
|
||||
if err := envconfig.Process("", &r); err != nil {
|
||||
log.Fatalf("failed to read from env: %v", err)
|
||||
return
|
||||
}
|
||||
r.storage = &postgresql.PostgresBackend{DatabaseURL: r.PostgresDatabase}
|
||||
server, err := relayer.NewServer(&r)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
if err := server.Start("0.0.0.0", 7447); err != nil {
|
||||
log.Fatalf("server terminated: %v", err)
|
||||
}
|
||||
}
|
||||
13
extra.go
13
extra.go
@@ -1,13 +0,0 @@
|
||||
package relayer
|
||||
|
||||
import "context"
|
||||
|
||||
const AUTH_CONTEXT_KEY = iota
|
||||
|
||||
func GetAuthStatus(ctx context.Context) (pubkey string, ok bool) {
|
||||
authedPubkey := ctx.Value(AUTH_CONTEXT_KEY)
|
||||
if authedPubkey == nil {
|
||||
return "", false
|
||||
}
|
||||
return authedPubkey.(string), true
|
||||
}
|
||||
73
go.mod
73
go.mod
@@ -1,99 +1,60 @@
|
||||
module github.com/fiatjaf/relayer/v2
|
||||
module github.com/fiatjaf/khatru
|
||||
|
||||
go 1.18
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/aquasecurity/esquery v0.2.0
|
||||
github.com/cockroachdb/pebble v0.0.0-20220723153705-3fc374e4dc66
|
||||
github.com/bmatsuo/lmdb-go v1.8.0
|
||||
github.com/dgraph-io/badger/v4 v4.1.0
|
||||
github.com/elastic/go-elasticsearch/v8 v8.6.0
|
||||
github.com/fasthttp/websocket v1.5.3
|
||||
github.com/gobwas/ws v1.2.0
|
||||
github.com/grokify/html-strip-tags-go v0.0.1
|
||||
github.com/jb55/lnsocket/go v0.0.0-20220725174341-b98b5cd37bb6
|
||||
github.com/jmoiron/sqlx v1.3.1
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/lib/pq v1.10.3
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/mmcdole/gofeed v1.1.3
|
||||
github.com/nbd-wtf/go-nostr v0.18.11
|
||||
github.com/rif/cache2go v1.0.0
|
||||
github.com/nbd-wtf/go-nostr v0.20.0
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/stevelacy/daz v0.1.4
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DataDog/zstd v1.4.5 // indirect
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||
github.com/aead/siphash v1.0.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/btcsuite/btcd v0.23.1 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.4 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/btcsuite/btcwallet v0.15.1 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect
|
||||
github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect
|
||||
github.com/btcsuite/btcwallet/wtxmgr v1.5.0 // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cockroachdb/errors v1.8.1 // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect
|
||||
github.com/cockroachdb/redact v1.0.8 // indirect
|
||||
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/decred/dcrd/lru v1.0.0 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.0.0-20211216131617-bbee439d559c // indirect
|
||||
github.com/elastic/go-elasticsearch/v7 v7.6.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
|
||||
github.com/golang/protobuf v1.3.1 // indirect
|
||||
github.com/golang/snappy v0.0.3 // indirect
|
||||
github.com/google/flatbuffers v1.12.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/kkdai/bstream v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
||||
github.com/lightninglabs/neutrino v0.14.2 // indirect
|
||||
github.com/lightningnetwork/lnd v0.15.0-beta // indirect
|
||||
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/queue v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/tlv v1.0.3 // indirect
|
||||
github.com/lightningnetwork/lnd/tor v1.0.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/puzpuzpuz/xsync v1.5.2 // 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
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
go.opencensus.io v0.22.5 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
519
go.sum
519
go.sum
@@ -1,647 +1,188 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
|
||||
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
|
||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879/go.mod h1:osu7EoKiL36UThEgzYPqdRaxeo0NU8VoXqgcnwpey0g=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220316175102-8d5c75c28923/go.mod h1:taIcYprAW2g6Z9S0gGUxyR+zDwimyDMK5ePOX+iJ2ds=
|
||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||
github.com/btcsuite/btcd v0.23.1 h1:IB8cVQcC2X5mHbnfirLG5IZnkWYNTPlLZVrxUYSotbE=
|
||||
github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/bmatsuo/lmdb-go v1.8.0 h1:ohf3Q4xjXZBKh4AayUY4bb2CXuhRAI8BYGlJq08EfNA=
|
||||
github.com/bmatsuo/lmdb-go v1.8.0/go.mod h1:wWPZmKdOAZsl4qOqkowQ1aCrFie1HU8gWloHMCeAUdM=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.4 h1:Edx4AfBn+YPam2KP5AobDitulGp4r1Oibm8oruzkMdI=
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.4/go.mod h1:9AyU6EQVJ9Iw9zPyNT1lcdHd6cnEZdno5wLu5FY74os=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcwallet v0.15.1 h1:SKfh/l2Bgz9sJwHZvfiVbZ8Pl3N/8fFcWWXzsAPz9GU=
|
||||
github.com/btcsuite/btcwallet v0.15.1/go.mod h1:7OFsQ8ypiRwmr67hE0z98uXgJgXGAihE79jCib9x6ag=
|
||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3 h1:M2yr5UlULvpqtxUqpMxTME/pA92Z9cpqeyvAFk9lAg0=
|
||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3/go.mod h1:T2xSiKGpUkSLCh68aF+FMXmKK9mFqNdHl9VaqOr+JjU=
|
||||
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 h1:BtEN5Empw62/RVnZ0VcJaVtVlBijnLlJY+dwjAye2Bg=
|
||||
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0/go.mod h1:AtkqiL7ccKWxuLYtZm8Bu8G6q82w4yIZdgq6riy60z0=
|
||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 h1:wZnOolEAeNOHzHTnznw/wQv+j35ftCIokNrnOTOU5o8=
|
||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs=
|
||||
github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
|
||||
github.com/btcsuite/btcwallet/walletdb v1.4.0 h1:/C5JRF+dTuE2CNMCO/or5N8epsrhmSM4710uBQoYPTQ=
|
||||
github.com/btcsuite/btcwallet/walletdb v1.4.0/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
|
||||
github.com/btcsuite/btcwallet/wtxmgr v1.5.0 h1:WO0KyN4l6H3JWnlFxfGR7r3gDnlGT7W2cL8vl6av4SU=
|
||||
github.com/btcsuite/btcwallet/wtxmgr v1.5.0/go.mod h1:TQVDhFxseiGtZwEPvLgtfyxuNUDsIdaJdshvWzR0HJ4=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
|
||||
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 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4=
|
||||
github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM=
|
||||
github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y=
|
||||
github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac=
|
||||
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY=
|
||||
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
|
||||
github.com/cockroachdb/pebble v0.0.0-20220723153705-3fc374e4dc66 h1:zB1MHj8vfz6QDMSkO8ASmNZW8JRqDMqEukXBRVIPzjM=
|
||||
github.com/cockroachdb/pebble v0.0.0-20220723153705-3fc374e4dc66/go.mod h1:890yq1fUb9b6dGNwssgeUO5vQV9qfXnCPxAJhBQfXw0=
|
||||
github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw=
|
||||
github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM=
|
||||
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/lru v1.0.0 h1:Kbsb1SFDsIlaupWPwsPp+dkxiBY1frcS07PCPgotKz8=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4APOQ=
|
||||
github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210602112143-b1f3d6f4ef4e h1:qTP1telKJHlToHlwPQNmVg4yfMDMHe4Z3SYmzkrvA2M=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.0.0-20211216131617-bbee439d559c h1:onA2RpIyeCPvYAj1LFYiiMTrSpqVINWMfYFRS7lofJs=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.0.0-20211216131617-bbee439d559c/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI=
|
||||
github.com/elastic/go-elasticsearch/v7 v7.6.0 h1:sYpGLpEFHgLUKLsZUBfuaVI9QgHjS3JdH9fX4/z8QI8=
|
||||
github.com/elastic/go-elasticsearch/v7 v7.6.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.6.0 h1:xMaSe8jIh7NHzmNo9YBkewmaD2Pr+tX+zLkXxhieny4=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.6.0/go.mod h1:Usvydt+x0dv9a1TzEUaovqbJor8rmOHy5dSmPeMAE2k=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
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/fergusstrange/embedded-postgres v1.10.0 h1:YnwF6xAQYmKLAXXrrRx4rHDLih47YJwVPvg8jeKfdNg=
|
||||
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
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/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
|
||||
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
|
||||
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs=
|
||||
github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
|
||||
github.com/jb55/lnsocket/go v0.0.0-20220725174341-b98b5cd37bb6 h1:SANEj+rD0vZlCfN7cl5pi2StfpH2/PTYlhVc1H4JLsI=
|
||||
github.com/jb55/lnsocket/go v0.0.0-20220725174341-b98b5cd37bb6/go.mod h1:atFK/q48qtjbWF2CWxopp41+dYEPbBTACOXIXsBY3z8=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jgroeneveld/schema v1.0.0 h1:J0E10CrOkiSEsw6dfb1IfrDJD14pf6QLVJ3tRPl/syI=
|
||||
github.com/jgroeneveld/schema v1.0.0/go.mod h1:M14lv7sNMtGvo3ops1MwslaSYgDYxrSmbzWIQ0Mr5rs=
|
||||
github.com/jgroeneveld/trial v2.0.0+incompatible h1:d59ctdgor+VqdZCAiUfVN8K13s0ALDioG5DWwZNtRuQ=
|
||||
github.com/jgroeneveld/trial v2.0.0+incompatible/go.mod h1:I6INLW96EN8WysNBXUFI3M4RIC8ePg9ntAc/Wy+U/+M=
|
||||
github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE=
|
||||
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
|
||||
github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
|
||||
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk=
|
||||
github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U=
|
||||
github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
|
||||
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
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/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8=
|
||||
github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
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/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
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/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc=
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
|
||||
github.com/lightninglabs/neutrino v0.14.2 h1:yrnZUCYMZ5ECtXhgDrzqPq2oX8awoAN2D/cgCewJcCo=
|
||||
github.com/lightninglabs/neutrino v0.14.2/go.mod h1:OICUeTCn+4Tu27YRJIpWvvqySxx4oH4vgdP33Sw9RDc=
|
||||
github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5 h1:TkKwqFcQTGYoI+VEqyxA8rxpCin8qDaYX0AfVRinT3k=
|
||||
github.com/lightningnetwork/lnd v0.15.0-beta h1:smzYjJqL4nGuj4qrAWdikrPzPJ8fcPRFHQ86S2tHR1M=
|
||||
github.com/lightningnetwork/lnd v0.15.0-beta/go.mod h1:Tm7LZrYeR2JQH1gEOKmd0NTCgjJ1Bnujkx4lcz9b5+A=
|
||||
github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
|
||||
github.com/lightningnetwork/lnd/clock v1.1.0 h1:/yfVAwtPmdx45aQBoXQImeY7sOIEr7IXlImRMBOZ7GQ=
|
||||
github.com/lightningnetwork/lnd/clock v1.1.0/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
|
||||
github.com/lightningnetwork/lnd/healthcheck v1.2.2 h1:im+qcpgSuteqRCGeorT9yqVXuLrS6A7/acYzGgarMS4=
|
||||
github.com/lightningnetwork/lnd/kvdb v1.3.1 h1:gEz3zudNNRrCLEvqRaktYoKwsUblyHX+MKjR0aI3QnM=
|
||||
github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms=
|
||||
github.com/lightningnetwork/lnd/queue v1.1.0 h1:YpCJjlIvVxN/R7ww2aNiY8ex7U2fucZDLJ67tI3HFx8=
|
||||
github.com/lightningnetwork/lnd/queue v1.1.0/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg=
|
||||
github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0=
|
||||
github.com/lightningnetwork/lnd/ticker v1.1.0 h1:ShoBiRP3pIxZHaETndfQ5kEe+S4NdAY1hiX7YbZ4QE4=
|
||||
github.com/lightningnetwork/lnd/ticker v1.1.0/go.mod h1:ubqbSVCn6RlE0LazXuBr7/Zi6QT0uQo++OgIRBxQUrk=
|
||||
github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqWRMqEDCNxZJSKr9C4=
|
||||
github.com/lightningnetwork/lnd/tlv v1.0.3 h1:0xBZcPuXagP6f7TY/RnLNR4igE21ov6qUdTr5NyvhhI=
|
||||
github.com/lightningnetwork/lnd/tlv v1.0.3/go.mod h1:dzR/aZetBri+ZY/fHbwV06fNn/3UID6htQzbHfREFdo=
|
||||
github.com/lightningnetwork/lnd/tor v1.0.1 h1:A11FrpU0Y//g+fA827W4VnjOeoIvExONdchlLX8wYkA=
|
||||
github.com/lightningnetwork/lnd/tor v1.0.1/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64=
|
||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
|
||||
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
|
||||
github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
|
||||
github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
|
||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
|
||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
|
||||
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbd-wtf/go-nostr v0.18.11 h1:X0TRMw+5iint1sh1W65RlGv+pPsLDOqo1oI+SBX+8Hk=
|
||||
github.com/nbd-wtf/go-nostr v0.18.11/go.mod h1:F9y6+M8askJCjilLgMC3rD0moA6UtG1MCnyClNYXeys=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
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/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
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/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/nbd-wtf/go-nostr v0.20.0 h1:97SYhg68jWh5G1bW1g454hA0dTV7btwtPg836n4no0o=
|
||||
github.com/nbd-wtf/go-nostr v0.20.0/go.mod h1:iFfiZr8YYSC1vmdUei0VfDB7GH/RjS3cbmiD1I5BKyo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY=
|
||||
github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg=
|
||||
github.com/rif/cache2go v1.0.0 h1:DhvZcxXvsuD9ExQ6ZO6f/sOE66OaAQIwB8Mfumap4w4=
|
||||
github.com/rif/cache2go v1.0.0/go.mod h1:reDqW0mGufW34CGJ1tvjMobI1BY3dCTxA0ZWdbvm06s=
|
||||
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/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
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/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stevelacy/daz v0.1.4 h1:ugmff/D7D764wZjXSgSryEINE/bi+Xddllw3JQQGbWk=
|
||||
github.com/stevelacy/daz v0.1.4/go.mod h1:AbK6DzjiIL15r4bQtcFvOBAvDGMXoh+uIG26NRUugt0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
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/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
|
||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
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.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
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/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0 h1:GsV3S+OfZEOCNXdtNkBSR7kgLobAa/SO6tCxRa0GAYw=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0 h1:2aQv6F436YnN7I4VbI8PPYrBhu+SmrTaADcf8Mi/6PU=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.0 h1:ntrg6vvKRW26JRmHTE0iNlDgYK6JX3hg/4cD62X0ixk=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.0 h1:kw2TmO3yFTgE+F0mdKkG7xMxkit2duBDa2Hu6D/HMlw=
|
||||
go.etcd.io/etcd/server/v3 v3.5.0 h1:jk8D/lwGEDlQU9kZXUFMSANkE22Sg5+mW27ip8xcF9E=
|
||||
go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 h1:sO4WKdPAudZGKPcpZT4MJn6JaDmpyLrMPDGGyA1SttE=
|
||||
go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g=
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg=
|
||||
go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8=
|
||||
go.opentelemetry.io/otel/sdk v0.20.0 h1:JsxtGXd06J8jrnya7fdI/U/MR6yXA5DtbZy+qoHQlr8=
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8=
|
||||
go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced h1:c5geK1iMU3cDKtFrCVQIcjR3W+JOZMuhIyICMCTbtus=
|
||||
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
|
||||
304
handlers.go
304
handlers.go
@@ -1,4 +1,4 @@
|
||||
package relayer
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,86 +6,72 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip11"
|
||||
"github.com/nbd-wtf/go-nostr/nip42"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// TODO: consider moving these to Server as config params
|
||||
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
|
||||
)
|
||||
|
||||
// TODO: consider moving these to Server as config params
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
// ServeHTTP implements http.Handler interface.
|
||||
func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
rl.HandleWebsocket(w, r)
|
||||
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
||||
rl.HandleNIP11(w, r)
|
||||
} else {
|
||||
rl.serveMux.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
store := s.relay.Storage(ctx)
|
||||
advancedDeleter, _ := store.(AdvancedDeleter)
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
conn, err := rl.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
s.Log.Errorf("failed to upgrade websocket: %v", err)
|
||||
rl.Log.Printf("failed to upgrade websocket: %v\n", err)
|
||||
return
|
||||
}
|
||||
s.clientsMu.Lock()
|
||||
defer s.clientsMu.Unlock()
|
||||
s.clients[conn] = struct{}{}
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
rl.clientsMu.Lock()
|
||||
defer rl.clientsMu.Unlock()
|
||||
rl.clients[conn] = struct{}{}
|
||||
ticker := time.NewTicker(rl.PingPeriod)
|
||||
|
||||
// NIP-42 challenge
|
||||
challenge := make([]byte, 8)
|
||||
rand.Read(challenge)
|
||||
|
||||
ws := &WebSocket{
|
||||
conn: conn,
|
||||
challenge: hex.EncodeToString(challenge),
|
||||
conn: conn,
|
||||
Challenge: hex.EncodeToString(challenge),
|
||||
WaitingForAuth: make(chan struct{}),
|
||||
}
|
||||
|
||||
// reader
|
||||
go func() {
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
s.clientsMu.Lock()
|
||||
if _, ok := s.clients[conn]; ok {
|
||||
rl.clientsMu.Lock()
|
||||
if _, ok := rl.clients[conn]; ok {
|
||||
conn.Close()
|
||||
delete(s.clients, conn)
|
||||
delete(rl.clients, conn)
|
||||
removeListener(ws)
|
||||
}
|
||||
s.clientsMu.Unlock()
|
||||
rl.clientsMu.Unlock()
|
||||
}()
|
||||
|
||||
conn.SetReadLimit(maxMessageSize)
|
||||
conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
conn.SetReadLimit(rl.MaxMessageSize)
|
||||
conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
||||
return nil
|
||||
})
|
||||
|
||||
// NIP-42 auth challenge
|
||||
if _, ok := s.relay.(Auther); ok {
|
||||
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.challenge})
|
||||
for _, onconnect := range rl.OnConnect {
|
||||
onconnect(ctx)
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -97,7 +83,7 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
websocket.CloseNoStatusReceived, // 1005
|
||||
websocket.CloseAbnormalClosure, // 1006
|
||||
) {
|
||||
s.Log.Warningf("unexpected close error from %s: %v", r.Header.Get("X-Forwarded-For"), err)
|
||||
rl.Log.Printf("unexpected close error from %s: %v\n", r.Header.Get("X-Forwarded-For"), err)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -109,12 +95,6 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
go func(message []byte) {
|
||||
ctx = context.Background()
|
||||
var notice string
|
||||
defer func() {
|
||||
if notice != "" {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(notice))
|
||||
}
|
||||
}()
|
||||
|
||||
var request []json.RawMessage
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
@@ -123,7 +103,7 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if len(request) < 2 {
|
||||
notice = "request has less than 2 parameters"
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("request has less than 2 parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,7 +115,7 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
// it's a new event
|
||||
var evt nostr.Event
|
||||
if err := json.Unmarshal(request[1], &evt); err != nil {
|
||||
notice = "failed to decode event: " + err.Error()
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode event: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -157,89 +137,63 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if evt.Kind == 5 {
|
||||
// event deletion -- nip09
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "e" {
|
||||
if advancedDeleter != nil {
|
||||
advancedDeleter.BeforeDelete(ctx, tag[1], evt.PubKey)
|
||||
}
|
||||
|
||||
if err := store.DeleteEvent(ctx, tag[1], evt.PubKey); err != nil {
|
||||
reason := fmt.Sprintf("error: %s", err.Error())
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: &reason})
|
||||
return
|
||||
}
|
||||
|
||||
if advancedDeleter != nil {
|
||||
advancedDeleter.AfterDelete(tag[1], evt.PubKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
err = rl.handleDeleteRequest(ctx, &evt)
|
||||
} else {
|
||||
err = rl.AddEvent(ctx, &evt)
|
||||
}
|
||||
|
||||
ok, message := AddEvent(ctx, s.relay, &evt)
|
||||
var reason *string
|
||||
if message != "" {
|
||||
reason = &message
|
||||
if err == nil {
|
||||
ok = true
|
||||
} else {
|
||||
msg := err.Error()
|
||||
reason = &msg
|
||||
}
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: ok, Reason: reason})
|
||||
case "COUNT":
|
||||
counter, ok := store.(EventCounter)
|
||||
if !ok {
|
||||
notice = "restricted: this relay does not support NIP-45"
|
||||
if rl.CountEvents == nil {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("this relay does not support NIP-45"))
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
json.Unmarshal(request[1], &id)
|
||||
if id == "" {
|
||||
notice = "COUNT has no <id>"
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("COUNT has no <id>"))
|
||||
return
|
||||
}
|
||||
|
||||
total := int64(0)
|
||||
var total int64
|
||||
filters := make(nostr.Filters, len(request)-2)
|
||||
for i, filterReq := range request[2:] {
|
||||
if err := json.Unmarshal(filterReq, &filters[i]); err != nil {
|
||||
notice = "failed to decode filter"
|
||||
return
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
|
||||
continue
|
||||
}
|
||||
|
||||
filter := &filters[i]
|
||||
filter := filters[i]
|
||||
|
||||
// prevent kind-4 events from being returned to unauthed users,
|
||||
// only when authentication is a thing
|
||||
if _, ok := s.relay.(Auther); ok {
|
||||
if slices.Contains(filter.Kinds, 4) {
|
||||
senders := filter.Authors
|
||||
receivers, _ := filter.Tags["p"]
|
||||
switch {
|
||||
case ws.authed == "":
|
||||
// not authenticated
|
||||
notice = "restricted: this relay does not serve kind-4 to unauthenticated users, does your client implement NIP-42?"
|
||||
return
|
||||
case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.authed):
|
||||
// allowed filter: ws.authed is sole sender (filter specifies one or all receivers)
|
||||
case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.authed):
|
||||
// allowed filter: ws.authed is sole receiver (filter specifies one or all senders)
|
||||
default:
|
||||
// restricted filter: do not return any events,
|
||||
// even if other elements in filters array were not restricted).
|
||||
// client should know better.
|
||||
notice = "restricted: authenticated user does not have authorization for requested filters."
|
||||
return
|
||||
}
|
||||
for _, reject := range rl.RejectFilter {
|
||||
if rejecting, msg := reject(ctx, filter); rejecting {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, reject := range rl.RejectCountFilter {
|
||||
if rejecting, msg := reject(ctx, filter); rejecting {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
count, err := counter.CountEvents(ctx, filter)
|
||||
if err != nil {
|
||||
s.Log.Errorf("store: %v", err)
|
||||
continue
|
||||
for _, count := range rl.CountEvents {
|
||||
res, err := count(ctx, filter)
|
||||
if err != nil {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
||||
}
|
||||
total += res
|
||||
}
|
||||
total += count
|
||||
}
|
||||
|
||||
ws.WriteJSON([]interface{}{"COUNT", id, map[string]int64{"count": total}})
|
||||
@@ -247,91 +201,79 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
var id string
|
||||
json.Unmarshal(request[1], &id)
|
||||
if id == "" {
|
||||
notice = "REQ has no <id>"
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("REQ has no <id>"))
|
||||
return
|
||||
}
|
||||
|
||||
filters := make(nostr.Filters, len(request)-2)
|
||||
eose := sync.WaitGroup{}
|
||||
eose.Add(len(request[2:]))
|
||||
|
||||
for i, filterReq := range request[2:] {
|
||||
if err := json.Unmarshal(
|
||||
filterReq,
|
||||
&filters[i],
|
||||
); err != nil {
|
||||
notice = "failed to decode filter"
|
||||
return
|
||||
}
|
||||
|
||||
filter := &filters[i]
|
||||
|
||||
// prevent kind-4 events from being returned to unauthed users,
|
||||
// only when authentication is a thing
|
||||
if _, ok := s.relay.(Auther); ok {
|
||||
if slices.Contains(filter.Kinds, 4) {
|
||||
senders := filter.Authors
|
||||
receivers, _ := filter.Tags["p"]
|
||||
switch {
|
||||
case ws.authed == "":
|
||||
// not authenticated
|
||||
notice = "restricted: this relay does not serve kind-4 to unauthenticated users, does your client implement NIP-42?"
|
||||
return
|
||||
case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.authed):
|
||||
// allowed filter: ws.authed is sole sender (filter specifies one or all receivers)
|
||||
case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.authed):
|
||||
// allowed filter: ws.authed is sole receiver (filter specifies one or all senders)
|
||||
default:
|
||||
// restricted filter: do not return any events,
|
||||
// even if other elements in filters array were not restricted).
|
||||
// client should know better.
|
||||
notice = "restricted: authenticated user does not have authorization for requested filters."
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
events, err := store.QueryEvents(ctx, filter)
|
||||
if err != nil {
|
||||
s.Log.Errorf("store: %v", err)
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
|
||||
eose.Done()
|
||||
continue
|
||||
}
|
||||
|
||||
// ensures the client won't be bombarded with events in case Storage doesn't do limits right
|
||||
if filter.Limit == 0 {
|
||||
filter.Limit = 9999999999
|
||||
}
|
||||
i := 0
|
||||
for event := range events {
|
||||
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
|
||||
i++
|
||||
if i > filter.Limit {
|
||||
break
|
||||
filter := filters[i]
|
||||
|
||||
for _, reject := range rl.RejectCountFilter {
|
||||
if rejecting, msg := reject(ctx, filter); rejecting {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||
eose.Done()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// exhaust the channel (in case we broke out of it early) so it is closed by the storage
|
||||
for range events {
|
||||
eose.Add(len(rl.QueryEvents))
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, filter)
|
||||
if err != nil {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
||||
eose.Done()
|
||||
continue
|
||||
}
|
||||
|
||||
go func(ch chan *nostr.Event) {
|
||||
for event := range ch {
|
||||
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
|
||||
}
|
||||
eose.Done()
|
||||
}(ch)
|
||||
}
|
||||
|
||||
eose.Done()
|
||||
}
|
||||
|
||||
ws.WriteJSON(nostr.EOSEEnvelope(id))
|
||||
go func() {
|
||||
eose.Wait()
|
||||
ws.WriteJSON(nostr.EOSEEnvelope(id))
|
||||
}()
|
||||
|
||||
setListener(id, ws, filters)
|
||||
case "CLOSE":
|
||||
var id string
|
||||
json.Unmarshal(request[1], &id)
|
||||
if id == "" {
|
||||
notice = "CLOSE has no <id>"
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("CLOSE has no <id>"))
|
||||
return
|
||||
}
|
||||
|
||||
removeListenerId(ws, id)
|
||||
case "AUTH":
|
||||
if auther, ok := s.relay.(Auther); ok {
|
||||
if rl.ServiceURL != "" {
|
||||
var evt nostr.Event
|
||||
if err := json.Unmarshal(request[1], &evt); err != nil {
|
||||
notice = "failed to decode auth event: " + err.Error()
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode auth event: " + err.Error()))
|
||||
return
|
||||
}
|
||||
if pubkey, ok := nip42.ValidateAuthEvent(&evt, ws.challenge, auther.ServiceURL()); ok {
|
||||
ws.authed = pubkey
|
||||
if pubkey, ok := nip42.ValidateAuthEvent(&evt, ws.Challenge, rl.ServiceURL); ok {
|
||||
ws.Authed = pubkey
|
||||
close(ws.WaitingForAuth)
|
||||
ctx = context.WithValue(ctx, AUTH_CONTEXT_KEY, pubkey)
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: true})
|
||||
} else {
|
||||
@@ -339,12 +281,6 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: &reason})
|
||||
}
|
||||
}
|
||||
default:
|
||||
if cwh, ok := s.relay.(CustomWebSocketHandler); ok {
|
||||
cwh.HandleUnknownType(ws, typ, request)
|
||||
} else {
|
||||
notice = "unknown message type " + typ
|
||||
}
|
||||
}
|
||||
}(message)
|
||||
}
|
||||
@@ -362,7 +298,7 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
case <-ticker.C:
|
||||
err := ws.WriteMessage(websocket.PingMessage, nil)
|
||||
if err != nil {
|
||||
s.Log.Errorf("error writing ping: %v; closing websocket", err)
|
||||
rl.Log.Printf("error writing ping: %v; closing websocket\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -370,31 +306,29 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Server) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
||||
func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
supportedNIPs := []int{9, 11, 12, 15, 16, 20, 33}
|
||||
if _, ok := s.relay.(Auther); ok {
|
||||
if rl.ServiceURL != "" {
|
||||
supportedNIPs = append(supportedNIPs, 42)
|
||||
}
|
||||
if storage, ok := s.relay.(Storage); ok && storage != nil {
|
||||
if _, ok = storage.(EventCounter); ok {
|
||||
supportedNIPs = append(supportedNIPs, 45)
|
||||
}
|
||||
if rl.CountEvents != nil {
|
||||
supportedNIPs = append(supportedNIPs, 45)
|
||||
}
|
||||
|
||||
info := nip11.RelayInformationDocument{
|
||||
Name: s.relay.Name(),
|
||||
Description: "relay powered by the relayer framework",
|
||||
PubKey: "~",
|
||||
Contact: "~",
|
||||
Name: rl.Name,
|
||||
Description: rl.Description,
|
||||
PubKey: rl.PubKey,
|
||||
Contact: rl.Contact,
|
||||
SupportedNIPs: supportedNIPs,
|
||||
Software: "https://github.com/fiatjaf/relayer",
|
||||
Version: "~",
|
||||
Software: "https://github.com/trailriver/khatru",
|
||||
Version: "n/a",
|
||||
}
|
||||
|
||||
if ifmer, ok := s.relay.(Informationer); ok {
|
||||
info = ifmer.GetNIP11InformationDocument()
|
||||
for _, edit := range rl.EditInformation {
|
||||
edit(r.Context(), &info)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(info)
|
||||
|
||||
96
interface.go
96
interface.go
@@ -1,96 +0,0 @@
|
||||
package relayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip11"
|
||||
)
|
||||
|
||||
// Relay is the main interface for implementing a nostr relay.
|
||||
type Relay interface {
|
||||
// Name is used as the "name" field in NIP-11 and as a prefix in default Server logging.
|
||||
// For other NIP-11 fields, see [Informationer].
|
||||
Name() string
|
||||
// Init is called at the very beginning by [Server.Start], allowing a relay
|
||||
// to initialize its internal resources.
|
||||
// Also see [Storage.Init].
|
||||
Init() error
|
||||
// AcceptEvent is called for every nostr event received by the server.
|
||||
// If the returned value is true, the event is passed on to [Storage.SaveEvent].
|
||||
// Otherwise, the server responds with a negative and "blocked" message as described
|
||||
// in NIP-20.
|
||||
AcceptEvent(context.Context, *nostr.Event) bool
|
||||
// Storage returns the relay storage implementation.
|
||||
Storage(context.Context) Storage
|
||||
}
|
||||
|
||||
// Auther is the interface for implementing NIP-42.
|
||||
// ServiceURL() returns the URL used to verify the "AUTH" event from clients.
|
||||
type Auther interface {
|
||||
ServiceURL() string
|
||||
}
|
||||
|
||||
type Injector interface {
|
||||
InjectEvents() chan nostr.Event
|
||||
}
|
||||
|
||||
// Informationer is called to compose NIP-11 response to an HTTP request
|
||||
// with application/nostr+json mime type.
|
||||
// See also [Relay.Name].
|
||||
type Informationer interface {
|
||||
GetNIP11InformationDocument() nip11.RelayInformationDocument
|
||||
}
|
||||
|
||||
// CustomWebSocketHandler, if implemented, is passed nostr message types unrecognized
|
||||
// by the server.
|
||||
// The server handles "EVENT", "REQ" and "CLOSE" messages, as described in NIP-01.
|
||||
type CustomWebSocketHandler interface {
|
||||
HandleUnknownType(ws *WebSocket, typ string, request []json.RawMessage)
|
||||
}
|
||||
|
||||
// ShutdownAware is called during the server shutdown.
|
||||
// See [Server.Shutdown] for details.
|
||||
type ShutdownAware interface {
|
||||
OnShutdown(context.Context)
|
||||
}
|
||||
|
||||
// Logger is what [Server] uses to log messages.
|
||||
type Logger interface {
|
||||
Infof(format string, v ...any)
|
||||
Warningf(format string, v ...any)
|
||||
Errorf(format string, v ...any)
|
||||
}
|
||||
|
||||
// Storage is a persistence layer for nostr events handled by a relay.
|
||||
type Storage interface {
|
||||
// Init is called at the very beginning by [Server.Start], after [Relay.Init],
|
||||
// allowing a storage to initialize its internal resources.
|
||||
Init() error
|
||||
|
||||
// QueryEvents is invoked upon a client's REQ as described in NIP-01.
|
||||
// it should return a channel with the events as they're recovered from a database.
|
||||
// the channel should be closed after the events are all delivered.
|
||||
QueryEvents(ctx context.Context, filter *nostr.Filter) (chan *nostr.Event, error)
|
||||
// DeleteEvent is used to handle deletion events, as per NIP-09.
|
||||
DeleteEvent(ctx context.Context, id string, pubkey string) error
|
||||
// SaveEvent is called once Relay.AcceptEvent reports true.
|
||||
SaveEvent(ctx context.Context, event *nostr.Event) error
|
||||
}
|
||||
|
||||
// AdvancedDeleter methods are called before and after [Storage.DeleteEvent].
|
||||
type AdvancedDeleter interface {
|
||||
BeforeDelete(ctx context.Context, id string, pubkey string)
|
||||
AfterDelete(id string, pubkey string)
|
||||
}
|
||||
|
||||
// AdvancedSaver methods are called before and after [Storage.SaveEvent].
|
||||
type AdvancedSaver interface {
|
||||
BeforeSave(context.Context, *nostr.Event)
|
||||
AfterSave(*nostr.Event)
|
||||
}
|
||||
|
||||
type EventCounter interface {
|
||||
CountEvents(ctx context.Context, filter *nostr.Filter) (int64, error)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package relayer
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package relayer
|
||||
|
||||
type Notice struct {
|
||||
Kind string `json:"kind"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
22
plugins/events.go
Normal file
22
plugins/events.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func PreventTooManyIndexableTags(max int) func(context.Context, *nostr.Event) (bool, string) {
|
||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
ntags := 0
|
||||
for _, tag := range event.Tags {
|
||||
if len(tag) > 0 && len(tag[0]) == 1 {
|
||||
ntags++
|
||||
}
|
||||
}
|
||||
if ntags > max {
|
||||
return true, "too many indexable tags"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
33
plugins/filters.go
Normal file
33
plugins/filters.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func NoPrefixFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
for _, id := range filter.IDs {
|
||||
if len(id) != 64 {
|
||||
return true, fmt.Sprintf("filters can only contain full ids")
|
||||
}
|
||||
}
|
||||
for _, pk := range filter.Authors {
|
||||
if len(pk) != 64 {
|
||||
return true, fmt.Sprintf("filters can only contain full pubkeys")
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
items := len(filter.Tags) + len(filter.Kinds)
|
||||
|
||||
if items > 4 && len(filter.Tags) > 2 {
|
||||
return true, "too many things to filter for"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
37
plugins/nip04.go
Normal file
37
plugins/nip04.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func rejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, string) {
|
||||
// prevent kind-4 events from being returned to unauthed users,
|
||||
// only when authentication is a thing
|
||||
if !slices.Contains(filter.Kinds, 4) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
ws := khatru.GetConnection(ctx)
|
||||
senders := filter.Authors
|
||||
receivers, _ := filter.Tags["p"]
|
||||
switch {
|
||||
case ws.Authed == "":
|
||||
// not authenticated
|
||||
return true, "restricted: this relay does not serve kind-4 to unauthenticated users, does your client implement NIP-42?"
|
||||
case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.Authed):
|
||||
// allowed filter: ws.authed is sole sender (filter specifies one or all receivers)
|
||||
return false, ""
|
||||
case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.Authed):
|
||||
// allowed filter: ws.authed is sole receiver (filter specifies one or all senders)
|
||||
return false, ""
|
||||
default:
|
||||
// restricted filter: do not return any events,
|
||||
// even if other elements in filters array were not restricted).
|
||||
// client should know better.
|
||||
return true, "restricted: authenticated user does not have authorization for requested filters."
|
||||
}
|
||||
}
|
||||
83
plugins/storage/badgern/count.go
Normal file
83
plugins/storage/badgern/count.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package badgern
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
func (b BadgerBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
|
||||
var count int64 = 0
|
||||
|
||||
queries, extraFilter, since, prefixLen, idxOffset, err := prepareQueries(filter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
err = b.View(func(txn *badger.Txn) error {
|
||||
// iterate only through keys and in reverse order
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchValues = false
|
||||
opts.Reverse = true
|
||||
|
||||
// actually iterate
|
||||
for _, q := range queries {
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
|
||||
for it.Seek(q.startingPoint); it.ValidForPrefix(q.prefix); it.Next() {
|
||||
item := it.Item()
|
||||
key := item.Key()
|
||||
|
||||
if !q.skipTimestamp {
|
||||
createdAt := binary.BigEndian.Uint32(key[prefixLen:idxOffset])
|
||||
if createdAt < since {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
idx := make([]byte, 5)
|
||||
idx[0] = rawEventStorePrefix
|
||||
copy(idx[1:], key[idxOffset:])
|
||||
|
||||
// fetch actual event
|
||||
item, err := txn.Get(idx)
|
||||
if err != nil {
|
||||
if err == badger.ErrDiscardedTxn {
|
||||
return err
|
||||
}
|
||||
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if extraFilter == nil {
|
||||
count++
|
||||
} else {
|
||||
err = item.Value(func(val []byte) error {
|
||||
evt := &nostr.Event{}
|
||||
if err := nson.Unmarshal(string(val), evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if this matches the other filters that were not part of the index
|
||||
if extraFilter == nil || extraFilter.Matches(evt) {
|
||||
count++
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return count, err
|
||||
}
|
||||
78
plugins/storage/badgern/delete.go
Normal file
78
plugins/storage/badgern/delete.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package badgern
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
func (b *BadgerBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
deletionHappened := false
|
||||
|
||||
err := b.Update(func(txn *badger.Txn) error {
|
||||
idx := make([]byte, 1, 5)
|
||||
idx[0] = rawEventStorePrefix
|
||||
|
||||
// query event by id to get its idx
|
||||
id, _ := hex.DecodeString(evt.ID)
|
||||
prefix := make([]byte, 1+32)
|
||||
copy(prefix[1:], id)
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchValues = false
|
||||
it := txn.NewIterator(opts)
|
||||
it.Seek(prefix)
|
||||
if it.ValidForPrefix(prefix) {
|
||||
// the key is the last 32 bytes
|
||||
idx = append(idx, it.Item().Key()[1+32:]...)
|
||||
}
|
||||
it.Close()
|
||||
|
||||
// if no idx was found, end here, this event doesn't exist
|
||||
if len(idx) == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetch the event
|
||||
item, err := txn.Get(idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item.Value(func(val []byte) error {
|
||||
evt := &nostr.Event{}
|
||||
if err := nson.Unmarshal(string(val), evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set this so we'll run the GC later
|
||||
deletionHappened = true
|
||||
|
||||
// calculate all index keys we have for this event and delete them
|
||||
for _, k := range getIndexKeysForEvent(evt, idx[1:]) {
|
||||
if err := txn.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete the raw event
|
||||
return txn.Delete(idx)
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// after deleting, run garbage collector
|
||||
if deletionHappened {
|
||||
if err := b.RunValueLogGC(0.8); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
159
plugins/storage/badgern/lib.go
Normal file
159
plugins/storage/badgern/lib.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package badgern
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
const (
|
||||
rawEventStorePrefix byte = 0
|
||||
indexCreatedAtPrefix byte = 1
|
||||
indexIdPrefix byte = 2
|
||||
indexKindPrefix byte = 3
|
||||
indexPubkeyPrefix byte = 4
|
||||
indexPubkeyKindPrefix byte = 5
|
||||
indexTagPrefix byte = 6
|
||||
)
|
||||
|
||||
type BadgerBackend struct {
|
||||
Path string
|
||||
MaxLimit int
|
||||
|
||||
*badger.DB
|
||||
seq *badger.Sequence
|
||||
}
|
||||
|
||||
func (b *BadgerBackend) Init() error {
|
||||
db, err := badger.Open(badger.DefaultOptions(b.Path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.DB = db
|
||||
b.seq, err = db.GetSequence([]byte("events"), 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.MaxLimit == 0 {
|
||||
b.MaxLimit = 500
|
||||
}
|
||||
|
||||
// DEBUG: inspecting keys on startup
|
||||
// db.View(func(txn *badger.Txn) error {
|
||||
// opts := badger.DefaultIteratorOptions
|
||||
// opts.PrefetchSize = 10
|
||||
// it := txn.NewIterator(opts)
|
||||
// defer it.Close()
|
||||
// for it.Rewind(); it.Valid(); it.Next() {
|
||||
// item := it.Item()
|
||||
// k := item.Key()
|
||||
// err := item.Value(func(v []byte) error {
|
||||
// fmt.Println("key:", k)
|
||||
// return nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// })
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BadgerBackend) Close() {
|
||||
b.DB.Close()
|
||||
b.seq.Release()
|
||||
}
|
||||
|
||||
func (b BadgerBackend) Serial() []byte {
|
||||
v, _ := b.seq.Next()
|
||||
vb := make([]byte, 5)
|
||||
vb[0] = rawEventStorePrefix
|
||||
binary.BigEndian.PutUint32(vb[1:], uint32(v))
|
||||
return vb
|
||||
}
|
||||
|
||||
func getIndexKeysForEvent(evt *nostr.Event, idx []byte) [][]byte {
|
||||
keys := make([][]byte, 0, 18)
|
||||
|
||||
// indexes
|
||||
{
|
||||
// ~ by id
|
||||
id, _ := hex.DecodeString(evt.ID)
|
||||
k := make([]byte, 1+32+4)
|
||||
k[0] = indexIdPrefix
|
||||
copy(k[1:], id)
|
||||
copy(k[1+32:], idx)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by pubkey+date
|
||||
pubkey, _ := hex.DecodeString(evt.PubKey)
|
||||
k := make([]byte, 1+32+4+4)
|
||||
k[0] = indexPubkeyPrefix
|
||||
copy(k[1:], pubkey)
|
||||
binary.BigEndian.PutUint32(k[1+32:], uint32(evt.CreatedAt))
|
||||
copy(k[1+32+4:], idx)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by kind+date
|
||||
k := make([]byte, 1+2+4+4)
|
||||
k[0] = indexKindPrefix
|
||||
binary.BigEndian.PutUint16(k[1:], uint16(evt.Kind))
|
||||
binary.BigEndian.PutUint32(k[1+2:], uint32(evt.CreatedAt))
|
||||
copy(k[1+2+4:], idx)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by pubkey+kind+date
|
||||
pubkey, _ := hex.DecodeString(evt.PubKey)
|
||||
k := make([]byte, 1+32+2+4+4)
|
||||
k[0] = indexPubkeyKindPrefix
|
||||
copy(k[1:], pubkey)
|
||||
binary.BigEndian.PutUint16(k[1+32:], uint16(evt.Kind))
|
||||
binary.BigEndian.PutUint32(k[1+32+2:], uint32(evt.CreatedAt))
|
||||
copy(k[1+32+2+4:], idx)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
// ~ by tagvalue+date
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) < 2 || len(tag[0]) != 1 || len(tag[1]) == 0 || len(tag[1]) > 100 {
|
||||
continue
|
||||
}
|
||||
|
||||
var v []byte
|
||||
if vb, _ := hex.DecodeString(tag[1]); len(vb) == 32 {
|
||||
// store value as bytes
|
||||
v = vb
|
||||
} else {
|
||||
v = []byte(tag[1])
|
||||
}
|
||||
|
||||
k := make([]byte, 1+len(v)+4+4)
|
||||
k[0] = indexTagPrefix
|
||||
copy(k[1:], v)
|
||||
binary.BigEndian.PutUint32(k[1+len(v):], uint32(evt.CreatedAt))
|
||||
copy(k[1+len(v)+4:], idx)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by date only
|
||||
k := make([]byte, 1+4+4)
|
||||
k[0] = indexCreatedAtPrefix
|
||||
binary.BigEndian.PutUint32(k[1:], uint32(evt.CreatedAt))
|
||||
copy(k[1+4:], idx)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
318
plugins/storage/badgern/query.go
Normal file
318
plugins/storage/badgern/query.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package badgern
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
type query struct {
|
||||
i int
|
||||
prefix []byte
|
||||
startingPoint []byte
|
||||
results chan *nostr.Event
|
||||
skipTimestamp bool
|
||||
}
|
||||
|
||||
type queryEvent struct {
|
||||
*nostr.Event
|
||||
query int
|
||||
}
|
||||
|
||||
func (b BadgerBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
ch := make(chan *nostr.Event)
|
||||
|
||||
queries, extraFilter, since, prefixLen, idxOffset, err := prepareQueries(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := b.View(func(txn *badger.Txn) error {
|
||||
// iterate only through keys and in reverse order
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchValues = false
|
||||
opts.Reverse = true
|
||||
|
||||
// actually iterate
|
||||
iteratorClosers := make([]func(), len(queries))
|
||||
for i, q := range queries {
|
||||
go func(i int, q query) {
|
||||
it := txn.NewIterator(opts)
|
||||
iteratorClosers[i] = it.Close
|
||||
|
||||
defer close(q.results)
|
||||
|
||||
for it.Seek(q.startingPoint); it.ValidForPrefix(q.prefix); it.Next() {
|
||||
item := it.Item()
|
||||
key := item.Key()
|
||||
|
||||
if !q.skipTimestamp {
|
||||
createdAt := binary.BigEndian.Uint32(key[prefixLen:idxOffset])
|
||||
if createdAt < since {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
idx := make([]byte, 5)
|
||||
idx[0] = rawEventStorePrefix
|
||||
copy(idx[1:], key[idxOffset:])
|
||||
|
||||
// fetch actual event
|
||||
item, err := txn.Get(idx)
|
||||
if err != nil {
|
||||
if err == badger.ErrDiscardedTxn {
|
||||
return
|
||||
}
|
||||
|
||||
panic(err)
|
||||
}
|
||||
err = item.Value(func(val []byte) error {
|
||||
evt := &nostr.Event{}
|
||||
if err := nson.Unmarshal(string(val), evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if this matches the other filters that were not part of the index
|
||||
if extraFilter == nil || extraFilter.Matches(evt) {
|
||||
q.results <- evt
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}(i, q)
|
||||
}
|
||||
|
||||
// max number of events we'll return
|
||||
limit := b.MaxLimit
|
||||
if filter.Limit > 0 && filter.Limit < limit {
|
||||
limit = filter.Limit
|
||||
}
|
||||
|
||||
// receive results and ensure we only return the most recent ones always
|
||||
emittedEvents := 0
|
||||
|
||||
// first pass
|
||||
emitQueue := make(priorityQueue, 0, len(queries)+limit)
|
||||
for _, q := range queries {
|
||||
evt, ok := <-q.results
|
||||
if ok {
|
||||
emitQueue = append(emitQueue, &queryEvent{Event: evt, query: q.i})
|
||||
}
|
||||
}
|
||||
|
||||
// now it's a good time to schedule this
|
||||
defer func() {
|
||||
close(ch)
|
||||
for _, itclose := range iteratorClosers {
|
||||
itclose()
|
||||
}
|
||||
}()
|
||||
|
||||
// queue may be empty here if we have literally nothing
|
||||
if len(emitQueue) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
heap.Init(&emitQueue)
|
||||
|
||||
// iterate until we've emitted all events required
|
||||
for {
|
||||
// emit latest event in queue
|
||||
latest := emitQueue[0]
|
||||
ch <- latest.Event
|
||||
|
||||
// stop when reaching limit
|
||||
emittedEvents++
|
||||
if emittedEvents == limit {
|
||||
break
|
||||
}
|
||||
|
||||
// fetch a new one from query results and replace the previous one with it
|
||||
if evt, ok := <-queries[latest.query].results; ok {
|
||||
emitQueue[0].Event = evt
|
||||
heap.Fix(&emitQueue, 0)
|
||||
} else {
|
||||
// if this query has no more events we just remove this and proceed normally
|
||||
heap.Remove(&emitQueue, 0)
|
||||
|
||||
// check if the list is empty and end
|
||||
if len(emitQueue) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type priorityQueue []*queryEvent
|
||||
|
||||
func (pq priorityQueue) Len() int { return len(pq) }
|
||||
|
||||
func (pq priorityQueue) Less(i, j int) bool {
|
||||
return pq[i].CreatedAt > pq[j].CreatedAt
|
||||
}
|
||||
|
||||
func (pq priorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Push(x any) {
|
||||
item := x.(*queryEvent)
|
||||
*pq = append(*pq, item)
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Pop() any {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
old[n-1] = nil // avoid memory leak
|
||||
*pq = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
|
||||
func prepareQueries(filter nostr.Filter) (
|
||||
queries []query,
|
||||
extraFilter *nostr.Filter,
|
||||
since uint32,
|
||||
prefixLen int,
|
||||
idxOffset int,
|
||||
err error,
|
||||
) {
|
||||
var index byte
|
||||
|
||||
if len(filter.IDs) > 0 {
|
||||
index = indexIdPrefix
|
||||
queries = make([]query, len(filter.IDs))
|
||||
for i, idHex := range filter.IDs {
|
||||
prefix := make([]byte, 1+32)
|
||||
prefix[0] = index
|
||||
id, _ := hex.DecodeString(idHex)
|
||||
if len(id) != 32 {
|
||||
return nil, nil, 0, 0, 0, fmt.Errorf("invalid id '%s'", idHex)
|
||||
}
|
||||
copy(prefix[1:], id)
|
||||
queries[i] = query{i: i, prefix: prefix, skipTimestamp: true}
|
||||
}
|
||||
} else if len(filter.Authors) > 0 {
|
||||
if len(filter.Kinds) == 0 {
|
||||
index = indexPubkeyPrefix
|
||||
queries = make([]query, len(filter.Authors))
|
||||
for i, pubkeyHex := range filter.Authors {
|
||||
pubkey, _ := hex.DecodeString(pubkeyHex)
|
||||
if len(pubkey) != 32 {
|
||||
continue
|
||||
}
|
||||
prefix := make([]byte, 1+32)
|
||||
prefix[0] = index
|
||||
copy(prefix[1:], pubkey)
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
}
|
||||
} else {
|
||||
index = indexPubkeyKindPrefix
|
||||
queries = make([]query, len(filter.Authors)*len(filter.Kinds))
|
||||
i := 0
|
||||
for _, pubkeyHex := range filter.Authors {
|
||||
for _, kind := range filter.Kinds {
|
||||
pubkey, _ := hex.DecodeString(pubkeyHex)
|
||||
if len(pubkey) != 32 {
|
||||
return nil, nil, 0, 0, 0, fmt.Errorf("invalid pubkey '%s'", pubkeyHex)
|
||||
}
|
||||
prefix := make([]byte, 1+32+2)
|
||||
prefix[0] = index
|
||||
copy(prefix[1:], pubkey)
|
||||
binary.BigEndian.PutUint16(prefix[1+32:], uint16(kind))
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
extraFilter = &nostr.Filter{Tags: filter.Tags}
|
||||
} else if len(filter.Tags) > 0 {
|
||||
index = indexTagPrefix
|
||||
queries = make([]query, len(filter.Tags))
|
||||
extraFilter = &nostr.Filter{Kinds: filter.Kinds}
|
||||
i := 0
|
||||
for _, values := range filter.Tags {
|
||||
for _, value := range values {
|
||||
bv, _ := hex.DecodeString(value)
|
||||
var size int
|
||||
if len(bv) == 32 {
|
||||
// hex tag
|
||||
size = 32
|
||||
} else {
|
||||
// string tag
|
||||
bv = []byte(value)
|
||||
size = len(bv)
|
||||
}
|
||||
prefix := make([]byte, 1+size)
|
||||
prefix[0] = index
|
||||
copy(prefix[1:], bv)
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
i++
|
||||
}
|
||||
}
|
||||
} else if len(filter.Kinds) > 0 {
|
||||
index = indexKindPrefix
|
||||
queries = make([]query, len(filter.Kinds))
|
||||
for i, kind := range filter.Kinds {
|
||||
prefix := make([]byte, 1+2)
|
||||
prefix[0] = index
|
||||
binary.BigEndian.PutUint16(prefix[1:], uint16(kind))
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
}
|
||||
} else {
|
||||
index = indexCreatedAtPrefix
|
||||
queries = make([]query, 1)
|
||||
prefix := make([]byte, 1)
|
||||
prefix[0] = index
|
||||
queries[0] = query{i: 0, prefix: prefix}
|
||||
extraFilter = nil
|
||||
}
|
||||
|
||||
prefixLen = len(queries[0].prefix)
|
||||
|
||||
if index == indexIdPrefix {
|
||||
idxOffset = prefixLen
|
||||
} else {
|
||||
idxOffset = prefixLen + 4
|
||||
}
|
||||
|
||||
var until uint32 = 4294967295
|
||||
if filter.Until != nil {
|
||||
if fu := uint32(*filter.Until); fu < until {
|
||||
until = fu + 1
|
||||
}
|
||||
}
|
||||
for i, q := range queries {
|
||||
queries[i].startingPoint = binary.BigEndian.AppendUint32(q.prefix, uint32(until))
|
||||
queries[i].results = make(chan *nostr.Event, 12)
|
||||
}
|
||||
|
||||
// this is where we'll end the iteration
|
||||
if filter.Since != nil {
|
||||
if fs := uint32(*filter.Since); fs > since {
|
||||
since = fs
|
||||
}
|
||||
}
|
||||
|
||||
return queries, extraFilter, since, prefixLen, idxOffset, nil
|
||||
}
|
||||
32
plugins/storage/badgern/save.go
Normal file
32
plugins/storage/badgern/save.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package badgern
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
func (b *BadgerBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
return b.Update(func(txn *badger.Txn) error {
|
||||
nson, err := nson.Marshal(evt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx := b.Serial()
|
||||
// raw event store
|
||||
if err := txn.Set(idx, []byte(nson)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, k := range getIndexKeysForEvent(evt, idx[1:]) {
|
||||
if err := txn.Set(k, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -6,18 +6,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
"github.com/elastic/go-elasticsearch/v8/esutil"
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
var _ relayer.Storage = (*ElasticsearchStorage)(nil)
|
||||
|
||||
type IndexedEvent struct {
|
||||
Event nostr.Event `json:"event"`
|
||||
ContentSearch string `json:"content_search"`
|
||||
@@ -49,6 +45,7 @@ var indexMapping = `
|
||||
`
|
||||
|
||||
type ElasticsearchStorage struct {
|
||||
URL string
|
||||
IndexName string
|
||||
|
||||
es *elasticsearch.Client
|
||||
@@ -61,8 +58,8 @@ func (ess *ElasticsearchStorage) Init() error {
|
||||
}
|
||||
|
||||
cfg := elasticsearch.Config{}
|
||||
if x := os.Getenv("ES_URL"); x != "" {
|
||||
cfg.Addresses = strings.Split(x, ",")
|
||||
if ess.URL != "" {
|
||||
cfg.Addresses = strings.Split(ess.URL, ",")
|
||||
}
|
||||
es, err := elasticsearch.NewClient(cfg)
|
||||
if err != nil {
|
||||
@@ -98,20 +95,13 @@ func (ess *ElasticsearchStorage) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ess *ElasticsearchStorage) DeleteEvent(ctx context.Context, id string, pubkey string) error {
|
||||
// first do get by ID and check that pubkeys match
|
||||
// this is cheaper than doing delete by query, which also doesn't work with bulk indexer.
|
||||
found, _ := ess.getByID(&nostr.Filter{IDs: []string{id}})
|
||||
if len(found) == 0 || found[0].PubKey != pubkey {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ess *ElasticsearchStorage) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
done := make(chan error)
|
||||
err := ess.bi.Add(
|
||||
ctx,
|
||||
esutil.BulkIndexerItem{
|
||||
Action: "delete",
|
||||
DocumentID: id,
|
||||
DocumentID: evt.ID,
|
||||
OnSuccess: func(ctx context.Context, item esutil.BulkIndexerItem, res esutil.BulkIndexerResponseItem) {
|
||||
close(done)
|
||||
},
|
||||
@@ -162,50 +152,6 @@ func (ess *ElasticsearchStorage) SaveEvent(ctx context.Context, evt *nostr.Event
|
||||
|
||||
done := make(chan error)
|
||||
|
||||
// delete replaceable events
|
||||
deleteIDs := []string{}
|
||||
queryForDelete := func(filter *nostr.Filter) {
|
||||
toDelete, _ := ess.QueryEvents(ctx, filter)
|
||||
for e := range toDelete {
|
||||
// KindRecommendServer: we can't query ES for exact content match
|
||||
// so query by kind and loop over results to compare content
|
||||
if evt.Kind == nostr.KindRecommendServer {
|
||||
if e.Content == evt.Content {
|
||||
deleteIDs = append(deleteIDs, e.ID)
|
||||
}
|
||||
} else {
|
||||
deleteIDs = append(deleteIDs, e.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if evt.Kind == nostr.KindSetMetadata || evt.Kind == nostr.KindContactList || evt.Kind == nostr.KindRecommendServer || (10000 <= evt.Kind && evt.Kind < 20000) {
|
||||
// delete past events from this user
|
||||
queryForDelete(&nostr.Filter{
|
||||
Authors: []string{evt.PubKey},
|
||||
Kinds: []int{evt.Kind},
|
||||
})
|
||||
} else if evt.Kind >= 30000 && evt.Kind < 40000 {
|
||||
// NIP-33
|
||||
d := evt.Tags.GetFirst([]string{"d"})
|
||||
if d != nil {
|
||||
queryForDelete(&nostr.Filter{
|
||||
Authors: []string{evt.PubKey},
|
||||
Kinds: []int{evt.Kind},
|
||||
Tags: nostr.TagMap{
|
||||
"d": []string{d.Value()},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, id := range deleteIDs {
|
||||
ess.bi.Add(
|
||||
ctx,
|
||||
esutil.BulkIndexerItem{
|
||||
Action: "delete",
|
||||
DocumentID: id,
|
||||
})
|
||||
}
|
||||
|
||||
// adapted from:
|
||||
// https://github.com/elastic/go-elasticsearch/blob/main/_examples/bulk/indexer.go#L196
|
||||
err = ess.bi.Add(
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -33,7 +32,7 @@ type EsCountResult struct {
|
||||
Count int64
|
||||
}
|
||||
|
||||
func buildDsl(filter *nostr.Filter) ([]byte, error) {
|
||||
func buildDsl(filter nostr.Filter) ([]byte, error) {
|
||||
dsl := esquery.Bool()
|
||||
|
||||
prefixFilter := func(fieldName string, values []string) {
|
||||
@@ -74,12 +73,12 @@ func buildDsl(filter *nostr.Filter) ([]byte, error) {
|
||||
|
||||
// since
|
||||
if filter.Since != nil {
|
||||
dsl.Must(esquery.Range("event.created_at").Gte(filter.Since))
|
||||
dsl.Must(esquery.Range("event.created_at").Gt(filter.Since))
|
||||
}
|
||||
|
||||
// until
|
||||
if filter.Until != nil {
|
||||
dsl.Must(esquery.Range("event.created_at").Lte(filter.Until))
|
||||
dsl.Must(esquery.Range("event.created_at").Lt(filter.Until))
|
||||
}
|
||||
|
||||
// search
|
||||
@@ -90,7 +89,7 @@ func buildDsl(filter *nostr.Filter) ([]byte, error) {
|
||||
return json.Marshal(esquery.Query(dsl))
|
||||
}
|
||||
|
||||
func (ess *ElasticsearchStorage) getByID(filter *nostr.Filter) ([]*nostr.Event, error) {
|
||||
func (ess *ElasticsearchStorage) getByID(filter nostr.Filter) ([]*nostr.Event, error) {
|
||||
got, err := ess.es.Mget(
|
||||
esutil.NewJSONReader(filter),
|
||||
ess.es.Mget.WithIndex(ess.IndexName))
|
||||
@@ -118,13 +117,9 @@ func (ess *ElasticsearchStorage) getByID(filter *nostr.Filter) ([]*nostr.Event,
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (ess *ElasticsearchStorage) QueryEvents(ctx context.Context, filter *nostr.Filter) (chan *nostr.Event, error) {
|
||||
func (ess *ElasticsearchStorage) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
ch := make(chan *nostr.Event)
|
||||
|
||||
if filter == nil {
|
||||
return nil, errors.New("filter cannot be null")
|
||||
}
|
||||
|
||||
// optimization: get by id
|
||||
if isGetByID(filter) {
|
||||
if evts, err := ess.getByID(filter); err == nil {
|
||||
@@ -182,7 +177,7 @@ func (ess *ElasticsearchStorage) QueryEvents(ctx context.Context, filter *nostr.
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func isGetByID(filter *nostr.Filter) bool {
|
||||
func isGetByID(filter nostr.Filter) bool {
|
||||
isGetById := len(filter.IDs) > 0 &&
|
||||
len(filter.Authors) == 0 &&
|
||||
len(filter.Kinds) == 0 &&
|
||||
@@ -222,11 +217,7 @@ func toInterfaceSlice(slice interface{}) []interface{} {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ess *ElasticsearchStorage) CountEvents(ctx context.Context, filter *nostr.Filter) (int64, error) {
|
||||
if filter == nil {
|
||||
return 0, errors.New("filter cannot be null")
|
||||
}
|
||||
|
||||
func (ess *ElasticsearchStorage) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
|
||||
count := int64(0)
|
||||
|
||||
// optimization: get by id
|
||||
91
plugins/storage/lmdbn/count.go
Normal file
91
plugins/storage/lmdbn/count.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package lmdbn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/bmatsuo/lmdb-go/lmdb"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
func (b *LMDBBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
|
||||
var count int64 = 0
|
||||
|
||||
dbi, queries, extraFilter, since, prefixLen, err := b.prepareQueries(filter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
err = b.lmdbEnv.View(func(txn *lmdb.Txn) error {
|
||||
// actually iterate
|
||||
for _, q := range queries {
|
||||
cursor, err := txn.OpenCursor(dbi)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var k []byte
|
||||
var idx []byte
|
||||
var iterr error
|
||||
|
||||
if _, _, errsr := cursor.Get(q.startingPoint, nil, lmdb.SetRange); errsr != nil {
|
||||
if operr, ok := errsr.(*lmdb.OpError); !ok || operr.Errno != lmdb.NotFound {
|
||||
// in this case it's really an error
|
||||
panic(err)
|
||||
} else {
|
||||
// we're at the end and we just want notes before this,
|
||||
// so we just need to set the cursor the last key, this is not a real error
|
||||
k, idx, iterr = cursor.Get(nil, nil, lmdb.Last)
|
||||
}
|
||||
} else {
|
||||
// move one back as the first step
|
||||
k, idx, iterr = cursor.Get(nil, nil, lmdb.Prev)
|
||||
}
|
||||
|
||||
for {
|
||||
// we already have a k and a v and an err from the cursor setup, so check and use these
|
||||
if iterr != nil || !bytes.Equal(q.prefix, k[0:prefixLen]) {
|
||||
break
|
||||
}
|
||||
|
||||
if !q.skipTimestamp {
|
||||
createdAt := binary.BigEndian.Uint32(k[prefixLen:])
|
||||
if createdAt < since {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// fetch actual event
|
||||
val, err := txn.Get(b.rawEventStore, idx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if extraFilter == nil {
|
||||
count++
|
||||
} else {
|
||||
evt := &nostr.Event{}
|
||||
if err := nson.Unmarshal(string(val), evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if this matches the other filters that were not part of the index
|
||||
if extraFilter == nil || extraFilter.Matches(evt) {
|
||||
count++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// move one back (we'll look into k and v and err in the next iteration)
|
||||
k, idx, iterr = cursor.Get(nil, nil, lmdb.Prev)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return count, err
|
||||
}
|
||||
50
plugins/storage/lmdbn/delete.go
Normal file
50
plugins/storage/lmdbn/delete.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package lmdbn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/bmatsuo/lmdb-go/lmdb"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
func (b *LMDBBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
err := b.lmdbEnv.Update(func(txn *lmdb.Txn) error {
|
||||
id, _ := hex.DecodeString(evt.ID)
|
||||
idx, err := txn.Get(b.indexId, id)
|
||||
if operr, ok := err.(*lmdb.OpError); ok && operr.Errno == lmdb.NotFound {
|
||||
// we already do not have this
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// fetch the event
|
||||
val, err := txn.Get(b.rawEventStore, idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evt := &nostr.Event{}
|
||||
if err := nson.Unmarshal(string(val), evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// calculate all index keys we have for this event and delete them
|
||||
for _, k := range b.getIndexKeysForEvent(evt) {
|
||||
if err := txn.Del(k.dbi, k.key, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete the raw event
|
||||
return txn.Del(b.rawEventStore, idx, nil)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
208
plugins/storage/lmdbn/lib.go
Normal file
208
plugins/storage/lmdbn/lib.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package lmdbn
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/bmatsuo/lmdb-go/lmdb"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
const (
|
||||
maxuint16 = 65535
|
||||
maxuint32 = 4294967295
|
||||
)
|
||||
|
||||
type LMDBBackend struct {
|
||||
Path string
|
||||
MaxLimit int
|
||||
|
||||
lmdbEnv *lmdb.Env
|
||||
|
||||
rawEventStore lmdb.DBI
|
||||
indexCreatedAt lmdb.DBI
|
||||
indexId lmdb.DBI
|
||||
indexKind lmdb.DBI
|
||||
indexPubkey lmdb.DBI
|
||||
indexPubkeyKind lmdb.DBI
|
||||
indexTag lmdb.DBI
|
||||
|
||||
lastId atomic.Uint32
|
||||
}
|
||||
|
||||
func (b *LMDBBackend) Init() error {
|
||||
if b.MaxLimit == 0 {
|
||||
b.MaxLimit = 500
|
||||
}
|
||||
|
||||
// open lmdb
|
||||
env, err := lmdb.NewEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env.SetMaxDBs(7)
|
||||
env.SetMaxReaders(500)
|
||||
env.SetMapSize(1 << 38) // ~273GB
|
||||
|
||||
err = env.Open(b.Path, lmdb.NoTLS, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.lmdbEnv = env
|
||||
|
||||
// open each db
|
||||
if err := b.lmdbEnv.Update(func(txn *lmdb.Txn) error {
|
||||
if dbi, err := txn.OpenDBI("raw", lmdb.Create); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b.rawEventStore = dbi
|
||||
return nil
|
||||
}
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.lmdbEnv.Update(func(txn *lmdb.Txn) error {
|
||||
if dbi, err := txn.OpenDBI("created_at", lmdb.Create); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b.indexCreatedAt = dbi
|
||||
}
|
||||
if dbi, err := txn.OpenDBI("id", lmdb.Create); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b.indexId = dbi
|
||||
}
|
||||
if dbi, err := txn.OpenDBI("kind", lmdb.Create); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b.indexKind = dbi
|
||||
}
|
||||
if dbi, err := txn.OpenDBI("pubkey", lmdb.Create); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b.indexPubkey = dbi
|
||||
}
|
||||
if dbi, err := txn.OpenDBI("pubkeyKind", lmdb.Create); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b.indexPubkeyKind = dbi
|
||||
}
|
||||
if dbi, err := txn.OpenDBI("tag", lmdb.Create); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b.indexTag = dbi
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get lastId
|
||||
if err := b.lmdbEnv.View(func(txn *lmdb.Txn) error {
|
||||
txn.RawRead = true
|
||||
cursor, err := txn.OpenCursor(b.rawEventStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cursor.Close()
|
||||
k, _, err := cursor.Get(nil, nil, lmdb.Last)
|
||||
if operr, ok := err.(*lmdb.OpError); ok && operr.Errno == lmdb.NotFound {
|
||||
// nothing found, so we're at zero
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
}
|
||||
b.lastId.Store(binary.BigEndian.Uint32(k))
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LMDBBackend) Close() {
|
||||
b.lmdbEnv.Close()
|
||||
}
|
||||
|
||||
func (b *LMDBBackend) Serial() []byte {
|
||||
v := b.lastId.Add(1)
|
||||
vb := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(vb[:], uint32(v))
|
||||
return vb
|
||||
}
|
||||
|
||||
type key struct {
|
||||
dbi lmdb.DBI
|
||||
key []byte
|
||||
}
|
||||
|
||||
func (b *LMDBBackend) getIndexKeysForEvent(evt *nostr.Event) []key {
|
||||
keys := make([]key, 0, 18)
|
||||
|
||||
// indexes
|
||||
{
|
||||
// ~ by id
|
||||
k, _ := hex.DecodeString(evt.ID)
|
||||
keys = append(keys, key{dbi: b.indexId, key: k})
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by pubkey+date
|
||||
pubkey, _ := hex.DecodeString(evt.PubKey)
|
||||
k := make([]byte, 32+4)
|
||||
copy(k[:], pubkey)
|
||||
binary.BigEndian.PutUint32(k[32:], uint32(evt.CreatedAt))
|
||||
keys = append(keys, key{dbi: b.indexPubkey, key: k})
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by kind+date
|
||||
k := make([]byte, 2+4)
|
||||
binary.BigEndian.PutUint16(k[:], uint16(evt.Kind))
|
||||
binary.BigEndian.PutUint32(k[2:], uint32(evt.CreatedAt))
|
||||
keys = append(keys, key{dbi: b.indexKind, key: k})
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by pubkey+kind+date
|
||||
pubkey, _ := hex.DecodeString(evt.PubKey)
|
||||
k := make([]byte, 32+2+4)
|
||||
copy(k[:], pubkey)
|
||||
binary.BigEndian.PutUint16(k[32:], uint16(evt.Kind))
|
||||
binary.BigEndian.PutUint32(k[32+2:], uint32(evt.CreatedAt))
|
||||
keys = append(keys, key{dbi: b.indexPubkeyKind, key: k})
|
||||
}
|
||||
|
||||
// ~ by tagvalue+date
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) < 2 || len(tag[0]) != 1 || len(tag[1]) == 0 || len(tag[1]) > 100 {
|
||||
continue
|
||||
}
|
||||
|
||||
var v []byte
|
||||
if vb, _ := hex.DecodeString(tag[1]); len(vb) == 32 {
|
||||
// store value as bytes
|
||||
v = vb
|
||||
} else {
|
||||
v = []byte(tag[1])
|
||||
}
|
||||
|
||||
k := make([]byte, len(v)+4)
|
||||
copy(k[:], v)
|
||||
binary.BigEndian.PutUint32(k[len(v):], uint32(evt.CreatedAt))
|
||||
keys = append(keys, key{dbi: b.indexTag, key: k})
|
||||
}
|
||||
|
||||
{
|
||||
// ~ by date only
|
||||
k := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(k[:], uint32(evt.CreatedAt))
|
||||
keys = append(keys, key{dbi: b.indexCreatedAt, key: k})
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
314
plugins/storage/lmdbn/query.go
Normal file
314
plugins/storage/lmdbn/query.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package lmdbn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/heap"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/bmatsuo/lmdb-go/lmdb"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
type query struct {
|
||||
i int
|
||||
prefix []byte
|
||||
startingPoint []byte
|
||||
results chan *nostr.Event
|
||||
skipTimestamp bool
|
||||
}
|
||||
|
||||
type queryEvent struct {
|
||||
*nostr.Event
|
||||
query int
|
||||
}
|
||||
|
||||
func (b *LMDBBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
ch := make(chan *nostr.Event)
|
||||
|
||||
dbi, queries, extraFilter, since, prefixLen, err := b.prepareQueries(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := b.lmdbEnv.View(func(txn *lmdb.Txn) error {
|
||||
// actually iterate
|
||||
cursorClosers := make([]func(), len(queries))
|
||||
for i, q := range queries {
|
||||
go func(i int, q query) {
|
||||
defer close(q.results)
|
||||
|
||||
cursor, err := txn.OpenCursor(dbi)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cursorClosers[i] = cursor.Close
|
||||
|
||||
var k []byte
|
||||
var idx []byte
|
||||
var iterr error
|
||||
|
||||
if _, _, errsr := cursor.Get(q.startingPoint, nil, lmdb.SetRange); errsr != nil {
|
||||
if operr, ok := errsr.(*lmdb.OpError); !ok || operr.Errno != lmdb.NotFound {
|
||||
// in this case it's really an error
|
||||
panic(err)
|
||||
} else {
|
||||
// we're at the end and we just want notes before this,
|
||||
// so we just need to set the cursor the last key, this is not a real error
|
||||
k, idx, iterr = cursor.Get(nil, nil, lmdb.Last)
|
||||
}
|
||||
} else {
|
||||
// move one back as the first step
|
||||
k, idx, iterr = cursor.Get(nil, nil, lmdb.Prev)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
default:
|
||||
}
|
||||
|
||||
// we already have a k and a v and an err from the cursor setup, so check and use these
|
||||
if iterr != nil || !bytes.Equal(q.prefix, k[0:prefixLen]) {
|
||||
return
|
||||
}
|
||||
|
||||
if !q.skipTimestamp {
|
||||
createdAt := binary.BigEndian.Uint32(k[prefixLen:])
|
||||
if createdAt < since {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// fetch actual event
|
||||
val, err := txn.Get(b.rawEventStore, idx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
evt := &nostr.Event{}
|
||||
if err := nson.Unmarshal(string(val), evt); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// check if this matches the other filters that were not part of the index
|
||||
if extraFilter == nil || extraFilter.Matches(evt) {
|
||||
q.results <- evt
|
||||
}
|
||||
|
||||
// move one back (we'll look into k and v and err in the next iteration)
|
||||
k, idx, iterr = cursor.Get(nil, nil, lmdb.Prev)
|
||||
}
|
||||
}(i, q)
|
||||
}
|
||||
|
||||
// max number of events we'll return
|
||||
limit := b.MaxLimit
|
||||
if filter.Limit > 0 && filter.Limit < limit {
|
||||
limit = filter.Limit
|
||||
}
|
||||
|
||||
// receive results and ensure we only return the most recent ones always
|
||||
emittedEvents := 0
|
||||
|
||||
// first pass
|
||||
emitQueue := make(priorityQueue, 0, len(queries)+limit)
|
||||
for _, q := range queries {
|
||||
evt, ok := <-q.results
|
||||
if ok {
|
||||
emitQueue = append(emitQueue, &queryEvent{Event: evt, query: q.i})
|
||||
}
|
||||
}
|
||||
|
||||
// now it's a good time to schedule this
|
||||
defer func() {
|
||||
close(ch)
|
||||
for _, cclose := range cursorClosers {
|
||||
cclose()
|
||||
}
|
||||
}()
|
||||
|
||||
// queue may be empty here if we have literally nothing
|
||||
if len(emitQueue) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
heap.Init(&emitQueue)
|
||||
|
||||
// iterate until we've emitted all events required
|
||||
for {
|
||||
// emit latest event in queue
|
||||
latest := emitQueue[0]
|
||||
ch <- latest.Event
|
||||
|
||||
// stop when reaching limit
|
||||
emittedEvents++
|
||||
if emittedEvents >= limit {
|
||||
break
|
||||
}
|
||||
|
||||
// fetch a new one from query results and replace the previous one with it
|
||||
if evt, ok := <-queries[latest.query].results; ok {
|
||||
emitQueue[0].Event = evt
|
||||
heap.Fix(&emitQueue, 0)
|
||||
} else {
|
||||
// if this query has no more events we just remove this and proceed normally
|
||||
heap.Remove(&emitQueue, 0)
|
||||
|
||||
// check if the list is empty and end
|
||||
if len(emitQueue) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type priorityQueue []*queryEvent
|
||||
|
||||
func (pq priorityQueue) Len() int { return len(pq) }
|
||||
|
||||
func (pq priorityQueue) Less(i, j int) bool {
|
||||
return pq[i].CreatedAt > pq[j].CreatedAt
|
||||
}
|
||||
|
||||
func (pq priorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Push(x any) {
|
||||
item := x.(*queryEvent)
|
||||
*pq = append(*pq, item)
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Pop() any {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
old[n-1] = nil // avoid memory leak
|
||||
*pq = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
|
||||
func (b *LMDBBackend) prepareQueries(filter nostr.Filter) (
|
||||
dbi lmdb.DBI,
|
||||
queries []query,
|
||||
extraFilter *nostr.Filter,
|
||||
since uint32,
|
||||
prefixLen int,
|
||||
err error,
|
||||
) {
|
||||
if len(filter.IDs) > 0 {
|
||||
dbi = b.indexId
|
||||
queries = make([]query, len(filter.IDs))
|
||||
for i, idHex := range filter.IDs {
|
||||
prefix, _ := hex.DecodeString(idHex)
|
||||
if len(prefix) != 32 {
|
||||
return dbi, nil, nil, 0, 0, fmt.Errorf("invalid id '%s'", idHex)
|
||||
}
|
||||
queries[i] = query{i: i, prefix: prefix, skipTimestamp: true}
|
||||
}
|
||||
} else if len(filter.Authors) > 0 {
|
||||
if len(filter.Kinds) == 0 {
|
||||
dbi = b.indexPubkey
|
||||
queries = make([]query, len(filter.Authors))
|
||||
for i, pubkeyHex := range filter.Authors {
|
||||
prefix, _ := hex.DecodeString(pubkeyHex)
|
||||
if len(prefix) != 32 {
|
||||
return dbi, nil, nil, 0, 0, fmt.Errorf("invalid pubkey '%s'", pubkeyHex)
|
||||
}
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
}
|
||||
} else {
|
||||
dbi = b.indexPubkeyKind
|
||||
queries = make([]query, len(filter.Authors)*len(filter.Kinds))
|
||||
i := 0
|
||||
for _, pubkeyHex := range filter.Authors {
|
||||
for _, kind := range filter.Kinds {
|
||||
pubkey, _ := hex.DecodeString(pubkeyHex)
|
||||
if len(pubkey) != 32 {
|
||||
return dbi, nil, nil, 0, 0, fmt.Errorf("invalid pubkey '%s'", pubkeyHex)
|
||||
}
|
||||
prefix := make([]byte, 32+2)
|
||||
copy(prefix[:], pubkey)
|
||||
binary.BigEndian.PutUint16(prefix[+32:], uint16(kind))
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
extraFilter = &nostr.Filter{Tags: filter.Tags}
|
||||
} else if len(filter.Tags) > 0 {
|
||||
dbi = b.indexTag
|
||||
queries = make([]query, len(filter.Tags))
|
||||
extraFilter = &nostr.Filter{Kinds: filter.Kinds}
|
||||
i := 0
|
||||
for _, values := range filter.Tags {
|
||||
for _, value := range values {
|
||||
bv, _ := hex.DecodeString(value)
|
||||
var size int
|
||||
if len(bv) == 32 {
|
||||
// hex tag
|
||||
size = 32
|
||||
} else {
|
||||
// string tag
|
||||
bv = []byte(value)
|
||||
size = len(bv)
|
||||
}
|
||||
prefix := make([]byte, size)
|
||||
copy(prefix[:], bv)
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
i++
|
||||
}
|
||||
}
|
||||
} else if len(filter.Kinds) > 0 {
|
||||
dbi = b.indexKind
|
||||
queries = make([]query, len(filter.Kinds))
|
||||
for i, kind := range filter.Kinds {
|
||||
prefix := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(prefix[:], uint16(kind))
|
||||
queries[i] = query{i: i, prefix: prefix}
|
||||
}
|
||||
} else {
|
||||
dbi = b.indexCreatedAt
|
||||
queries = make([]query, 1)
|
||||
prefix := make([]byte, 0)
|
||||
queries[0] = query{i: 0, prefix: prefix}
|
||||
extraFilter = nil
|
||||
}
|
||||
|
||||
prefixLen = len(queries[0].prefix)
|
||||
|
||||
var until uint32 = 4294967295
|
||||
if filter.Until != nil {
|
||||
if fu := uint32(*filter.Until); fu < until {
|
||||
until = fu + 1
|
||||
}
|
||||
}
|
||||
for i, q := range queries {
|
||||
queries[i].startingPoint = binary.BigEndian.AppendUint32(q.prefix, uint32(until))
|
||||
queries[i].results = make(chan *nostr.Event, 12)
|
||||
}
|
||||
|
||||
// this is where we'll end the iteration
|
||||
if filter.Since != nil {
|
||||
if fs := uint32(*filter.Since); fs > since {
|
||||
since = fs
|
||||
}
|
||||
}
|
||||
|
||||
return dbi, queries, extraFilter, since, prefixLen, nil
|
||||
}
|
||||
38
plugins/storage/lmdbn/save.go
Normal file
38
plugins/storage/lmdbn/save.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package lmdbn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/bmatsuo/lmdb-go/lmdb"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
)
|
||||
|
||||
func (b *LMDBBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
// sanity checking
|
||||
if evt.CreatedAt > maxuint32 || evt.Kind > maxuint16 {
|
||||
return fmt.Errorf("event with values out of expected boundaries")
|
||||
}
|
||||
|
||||
return b.lmdbEnv.Update(func(txn *lmdb.Txn) error {
|
||||
nson, err := nson.Marshal(evt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx := b.Serial()
|
||||
// raw event store
|
||||
if err := txn.Put(b.rawEventStore, idx, []byte(nson), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, k := range b.getIndexKeysForEvent(evt) {
|
||||
if err := txn.Put(k.dbi, k.key, idx, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
12
plugins/storage/postgresql/delete.go
Normal file
12
plugins/storage/postgresql/delete.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (b PostgresBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
_, err := b.DB.ExecContext(ctx, "DELETE FROM event WHERE id = $1", evt.ID)
|
||||
return err
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
_ "github.com/lib/pq"
|
||||
@@ -15,8 +14,6 @@ const (
|
||||
queryTagsLimit = 10
|
||||
)
|
||||
|
||||
var _ relayer.Storage = (*PostgresBackend)(nil)
|
||||
|
||||
func (b *PostgresBackend) Init() error {
|
||||
db, err := sqlx.Connect("postgres", b.DatabaseURL)
|
||||
if err != nil {
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (b PostgresBackend) QueryEvents(ctx context.Context, filter *nostr.Filter) (ch chan *nostr.Event, err error) {
|
||||
func (b PostgresBackend) 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)
|
||||
@@ -44,7 +44,7 @@ func (b PostgresBackend) QueryEvents(ctx context.Context, filter *nostr.Filter)
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (b PostgresBackend) CountEvents(ctx context.Context, filter *nostr.Filter) (int64, error) {
|
||||
func (b PostgresBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
|
||||
query, params, err := b.queryEventsSql(filter, true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -57,14 +57,10 @@ func (b PostgresBackend) CountEvents(ctx context.Context, filter *nostr.Filter)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (b PostgresBackend) queryEventsSql(filter *nostr.Filter, doCount bool) (string, []any, error) {
|
||||
func (b PostgresBackend) queryEventsSql(filter nostr.Filter, doCount bool) (string, []any, error) {
|
||||
var conditions []string
|
||||
var params []any
|
||||
|
||||
if filter == nil {
|
||||
return "", nil, fmt.Errorf("filter cannot be null")
|
||||
}
|
||||
|
||||
if filter.IDs != nil {
|
||||
if len(filter.IDs) > b.QueryIDsLimit {
|
||||
// too many ids, fail everything
|
||||
@@ -159,11 +155,11 @@ func (b PostgresBackend) queryEventsSql(filter *nostr.Filter, doCount bool) (str
|
||||
}
|
||||
|
||||
if filter.Since != nil {
|
||||
conditions = append(conditions, "created_at >= ?")
|
||||
conditions = append(conditions, "created_at > ?")
|
||||
params = append(params, filter.Since)
|
||||
}
|
||||
if filter.Until != nil {
|
||||
conditions = append(conditions, "created_at <= ?")
|
||||
conditions = append(conditions, "created_at < ?")
|
||||
params = append(params, filter.Until)
|
||||
}
|
||||
|
||||
54
plugins/storage/postgresql/save.go
Normal file
54
plugins/storage/postgresql/save.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (b *PostgresBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
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 khatru.ErrDupEvent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *PostgresBackend) BeforeSave(ctx context.Context, evt *nostr.Event) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (b *PostgresBackend) AfterSave(evt *nostr.Event) {
|
||||
// delete all but the 100 most recent ones for each key
|
||||
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 100 LIMIT 1
|
||||
)`, evt.PubKey, evt.Kind)
|
||||
}
|
||||
|
||||
func saveEventSql(evt *nostr.Event) (string, []any, error) {
|
||||
const query = `INSERT INTO event (
|
||||
id, pubkey, created_at, kind, tags, content, sig)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING`
|
||||
|
||||
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
|
||||
}
|
||||
64
plugins/storage/postgresql/save_test.go
Normal file
64
plugins/storage/postgresql/save_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSaveEventSql(t *testing.T) {
|
||||
now := nostr.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
event *nostr.Event
|
||||
query string
|
||||
params []any
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
event: &nostr.Event{
|
||||
ID: "id",
|
||||
PubKey: "pk",
|
||||
CreatedAt: now,
|
||||
Kind: nostr.KindTextNote,
|
||||
Content: "test",
|
||||
Sig: "sig",
|
||||
},
|
||||
query: `INSERT INTO event (
|
||||
id, pubkey, created_at, kind, tags, content, sig)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
params: []any{"id", "pk", now, nostr.KindTextNote, []byte("null"), "test", "sig"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
event: &nostr.Event{
|
||||
ID: "id",
|
||||
PubKey: "pk",
|
||||
CreatedAt: now,
|
||||
Kind: nostr.KindTextNote,
|
||||
Tags: nostr.Tags{nostr.Tag{"foo", "bar"}},
|
||||
Content: "test",
|
||||
Sig: "sig",
|
||||
},
|
||||
query: `INSERT INTO event (
|
||||
id, pubkey, created_at, kind, tags, content, sig)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
params: []any{"id", "pk", now, nostr.KindTextNote, []byte("[[\"foo\",\"bar\"]]"), "test", "sig"},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query, params, err := saveEventSql(tt.event)
|
||||
assert.Equal(t, clean(tt.query), clean(query))
|
||||
assert.Equal(t, tt.params, params)
|
||||
assert.Equal(t, tt.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
12
plugins/storage/sqlite3/delete.go
Normal file
12
plugins/storage/sqlite3/delete.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (b SQLite3Backend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
_, err := b.DB.ExecContext(ctx, "DELETE FROM event WHERE id = $1", evt.ID)
|
||||
return err
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var _ relayer.Storage = (*SQLite3Backend)(nil)
|
||||
|
||||
func (b *SQLite3Backend) Init() error {
|
||||
db, err := sqlx.Connect("sqlite3", b.DatabaseURL)
|
||||
if err != nil {
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (b SQLite3Backend) QueryEvents(ctx context.Context, filter *nostr.Filter) (ch chan *nostr.Event, err error) {
|
||||
func (b SQLite3Backend) QueryEvents(ctx context.Context, filter nostr.Filter) (ch chan *nostr.Event, err error) {
|
||||
ch = make(chan *nostr.Event)
|
||||
|
||||
query, params, err := queryEventsSql(filter, false)
|
||||
@@ -44,7 +44,7 @@ func (b SQLite3Backend) QueryEvents(ctx context.Context, filter *nostr.Filter) (
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (b SQLite3Backend) CountEvents(ctx context.Context, filter *nostr.Filter) (int64, error) {
|
||||
func (b SQLite3Backend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
|
||||
query, params, err := queryEventsSql(filter, true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -58,14 +58,10 @@ func (b SQLite3Backend) CountEvents(ctx context.Context, filter *nostr.Filter) (
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func queryEventsSql(filter *nostr.Filter, doCount bool) (string, []any, error) {
|
||||
func queryEventsSql(filter nostr.Filter, doCount bool) (string, []any, error) {
|
||||
var conditions []string
|
||||
var params []any
|
||||
|
||||
if filter == nil {
|
||||
return "", nil, fmt.Errorf("filter cannot be null")
|
||||
}
|
||||
|
||||
if filter.IDs != nil {
|
||||
if len(filter.IDs) > 500 {
|
||||
// too many ids, fail everything
|
||||
@@ -154,11 +150,11 @@ func queryEventsSql(filter *nostr.Filter, doCount bool) (string, []any, error) {
|
||||
}
|
||||
|
||||
if filter.Since != nil {
|
||||
conditions = append(conditions, "created_at >= ?")
|
||||
conditions = append(conditions, "created_at > ?")
|
||||
params = append(params, filter.Since)
|
||||
}
|
||||
if filter.Until != nil {
|
||||
conditions = append(conditions, "created_at <= ?")
|
||||
conditions = append(conditions, "created_at < ?")
|
||||
params = append(params, filter.Until)
|
||||
}
|
||||
if filter.Search != "" {
|
||||
@@ -3,30 +3,12 @@ package sqlite3
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2/storage"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (b *SQLite3Backend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
// react to different kinds of events
|
||||
if evt.Kind == nostr.KindSetMetadata || evt.Kind == nostr.KindContactList || (10000 <= evt.Kind && evt.Kind < 20000) {
|
||||
// delete past events from this user
|
||||
b.DB.ExecContext(ctx, `DELETE FROM event WHERE pubkey = $1 AND kind = $2`, evt.PubKey, evt.Kind)
|
||||
} else if evt.Kind == nostr.KindRecommendServer {
|
||||
// delete past recommend_server events equal to this one
|
||||
b.DB.ExecContext(ctx, `DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND content = $3`,
|
||||
evt.PubKey, evt.Kind, evt.Content)
|
||||
} else if evt.Kind >= 30000 && evt.Kind < 40000 {
|
||||
// NIP-33
|
||||
d := evt.Tags.GetFirst([]string{"d"})
|
||||
if d != nil {
|
||||
tagsLike := fmt.Sprintf(`%%"d","%s"%%`, d.Value())
|
||||
b.DB.ExecContext(ctx, `DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND tags LIKE $3`, evt.PubKey, evt.Kind, tagsLike)
|
||||
}
|
||||
}
|
||||
|
||||
// insert
|
||||
tagsj, _ := json.Marshal(evt.Tags)
|
||||
res, err := b.DB.ExecContext(ctx, `
|
||||
@@ -43,7 +25,7 @@ func (b *SQLite3Backend) SaveEvent(ctx context.Context, evt *nostr.Event) error
|
||||
}
|
||||
|
||||
if nr == 0 {
|
||||
return storage.ErrDupEvent
|
||||
return khatru.ErrDupEvent
|
||||
}
|
||||
|
||||
return nil
|
||||
82
relay.go
Normal file
82
relay.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip11"
|
||||
)
|
||||
|
||||
func NewRelay() *Relay {
|
||||
return &Relay{
|
||||
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
|
||||
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
},
|
||||
|
||||
clients: make(map[*websocket.Conn]struct{}),
|
||||
serveMux: &http.ServeMux{},
|
||||
|
||||
WriteWait: 10 * time.Second,
|
||||
PongWait: 60 * time.Second,
|
||||
PingPeriod: 30 * time.Second,
|
||||
MaxMessageSize: 512000,
|
||||
}
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
Name string
|
||||
Description string
|
||||
PubKey string
|
||||
Contact string
|
||||
ServiceURL string // required for nip-42
|
||||
|
||||
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
|
||||
RejectFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||
RejectCountFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||
OverwriteDeletionOutcome []func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
|
||||
StoreEvent []func(ctx context.Context, event *nostr.Event) error
|
||||
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
|
||||
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
|
||||
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
|
||||
EditInformation []func(ctx context.Context, info *nip11.RelayInformationDocument)
|
||||
OnAuth []func(ctx context.Context, pubkey string)
|
||||
OnConnect []func(ctx context.Context)
|
||||
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
||||
|
||||
// Default logger, as set by NewServer, is a stdlib logger prefixed with "[khatru-relay] ",
|
||||
// outputting to stderr.
|
||||
Log *log.Logger
|
||||
|
||||
// for establishing websockets
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
// keep a connection reference to all connected clients for Server.Shutdown
|
||||
clientsMu sync.Mutex
|
||||
clients map[*websocket.Conn]struct{}
|
||||
|
||||
// in case you call Server.Start
|
||||
Addr string
|
||||
serveMux *http.ServeMux
|
||||
httpServer *http.Server
|
||||
|
||||
// websocket options
|
||||
WriteWait time.Duration // Time allowed to write a message to the peer.
|
||||
PongWait time.Duration // Time allowed to read the next pong message from the peer.
|
||||
PingPeriod time.Duration // Send pings to peer with this period. Must be less than pongWait.
|
||||
MaxMessageSize int64 // Maximum message size allowed from peer.
|
||||
}
|
||||
|
||||
func (rl *Relay) RequestAuth(ctx context.Context) {
|
||||
ws := GetConnection(ctx)
|
||||
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
|
||||
}
|
||||
125
start.go
125
start.go
@@ -1,106 +1,31 @@
|
||||
package relayer
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
// Server is a base for package users to implement nostr relays.
|
||||
// It can serve HTTP requests and websockets, passing control over to a relay implementation.
|
||||
//
|
||||
// To implement a relay, it is enough to satisfy [Relay] interface. Other interfaces are
|
||||
// [Informationer], [CustomWebSocketHandler], [ShutdownAware] and AdvancedXxx types.
|
||||
// See their respective doc comments.
|
||||
//
|
||||
// The basic usage is to call Start or StartConf, which starts serving immediately.
|
||||
// For a more fine-grained control, use NewServer.
|
||||
// See [basic/main.go], [whitelisted/main.go], [expensive/main.go] and [rss-bridge/main.go]
|
||||
// for example implementations.
|
||||
//
|
||||
// The following resource is a good starting point for details on what nostr protocol is
|
||||
// and how it works: https://github.com/nostr-protocol/nostr
|
||||
type Server struct {
|
||||
// Default logger, as set by NewServer, is a stdlib logger prefixed with [Relay.Name],
|
||||
// outputting to stderr.
|
||||
Log Logger
|
||||
|
||||
relay Relay
|
||||
|
||||
// keep a connection reference to all connected clients for Server.Shutdown
|
||||
clientsMu sync.Mutex
|
||||
clients map[*websocket.Conn]struct{}
|
||||
|
||||
// in case you call Server.Start
|
||||
Addr string
|
||||
serveMux *http.ServeMux
|
||||
httpServer *http.Server
|
||||
func (rl *Relay) Router() *http.ServeMux {
|
||||
return rl.serveMux
|
||||
}
|
||||
|
||||
func (s *Server) Router() *http.ServeMux {
|
||||
return s.serveMux
|
||||
}
|
||||
|
||||
// NewServer initializes the relay and its storage using their respective Init methods,
|
||||
// returning any non-nil errors, and returns a Server ready to listen for HTTP requests.
|
||||
func NewServer(relay Relay) (*Server, error) {
|
||||
srv := &Server{
|
||||
Log: defaultLogger(relay.Name() + ": "),
|
||||
relay: relay,
|
||||
clients: make(map[*websocket.Conn]struct{}),
|
||||
serveMux: &http.ServeMux{},
|
||||
}
|
||||
|
||||
// init the relay
|
||||
if err := relay.Init(); err != nil {
|
||||
return nil, fmt.Errorf("relay init: %w", err)
|
||||
}
|
||||
if err := relay.Storage(context.Background()).Init(); err != nil {
|
||||
return nil, fmt.Errorf("storage init: %w", err)
|
||||
}
|
||||
|
||||
// start listening from events from other sources, if any
|
||||
if inj, ok := relay.(Injector); ok {
|
||||
go func() {
|
||||
for event := range inj.InjectEvents() {
|
||||
notifyListeners(&event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler interface.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
s.HandleWebsocket(w, r)
|
||||
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
||||
s.HandleNIP11(w, r)
|
||||
} else {
|
||||
s.serveMux.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start(host string, port int, started ...chan bool) error {
|
||||
// Start creates an http server and starts listening on given host and port.
|
||||
func (rl *Relay) Start(host string, port int, started ...chan bool) error {
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Addr = ln.Addr().String()
|
||||
s.httpServer = &http.Server{
|
||||
Handler: cors.Default().Handler(s),
|
||||
rl.Addr = ln.Addr().String()
|
||||
rl.httpServer = &http.Server{
|
||||
Handler: cors.Default().Handler(rl),
|
||||
Addr: addr,
|
||||
WriteTimeout: 2 * time.Second,
|
||||
ReadTimeout: 2 * time.Second,
|
||||
@@ -112,7 +37,7 @@ func (s *Server) Start(host string, port int, started ...chan bool) error {
|
||||
close(started)
|
||||
}
|
||||
|
||||
if err := s.httpServer.Serve(ln); err == http.ErrServerClosed {
|
||||
if err := rl.httpServer.Serve(ln); err == http.ErrServerClosed {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -122,34 +47,14 @@ func (s *Server) Start(host string, port int, started ...chan bool) error {
|
||||
}
|
||||
|
||||
// Shutdown sends a websocket close control message to all connected clients.
|
||||
//
|
||||
// If the relay is ShutdownAware, Shutdown calls its OnShutdown, passing the context as is.
|
||||
// Note that the HTTP server make some time to shutdown and so the context deadline,
|
||||
// if any, may have been shortened by the time OnShutdown is called.
|
||||
func (s *Server) Shutdown(ctx context.Context) {
|
||||
s.httpServer.Shutdown(ctx)
|
||||
func (rl *Relay) Shutdown(ctx context.Context) {
|
||||
rl.httpServer.Shutdown(ctx)
|
||||
|
||||
s.clientsMu.Lock()
|
||||
defer s.clientsMu.Unlock()
|
||||
for conn := range s.clients {
|
||||
rl.clientsMu.Lock()
|
||||
defer rl.clientsMu.Unlock()
|
||||
for conn := range rl.clients {
|
||||
conn.WriteControl(websocket.CloseMessage, nil, time.Now().Add(time.Second))
|
||||
conn.Close()
|
||||
delete(s.clients, conn)
|
||||
}
|
||||
|
||||
if f, ok := s.relay.(ShutdownAware); ok {
|
||||
f.OnShutdown(ctx)
|
||||
delete(rl.clients, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultLogger(prefix string) Logger {
|
||||
l := log.New(os.Stderr, "", log.LstdFlags|log.Lmsgprefix)
|
||||
l.SetPrefix(prefix)
|
||||
return stdLogger{l}
|
||||
}
|
||||
|
||||
type stdLogger struct{ log *log.Logger }
|
||||
|
||||
func (l stdLogger) Infof(format string, v ...any) { l.log.Printf(format, v...) }
|
||||
func (l stdLogger) Warningf(format string, v ...any) { l.log.Printf(format, v...) }
|
||||
func (l stdLogger) Errorf(format string, v ...any) { l.log.Printf(format, v...) }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package relayer
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -88,6 +88,6 @@ func TestServerShutdownWebsocket(t *testing.T) {
|
||||
err = e
|
||||
}
|
||||
if _, ok := err.(wsutil.ClosedError); !ok {
|
||||
t.Errorf("client.ConnectionError: %v (%T); want wsutil.ClosedError", err, err)
|
||||
t.Errorf("client.ConnextionError: %v (%T); want wsutil.ClosedError", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package storage
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrDupEvent = errors.New("duplicate: event already exists")
|
||||
@@ -1,8 +0,0 @@
|
||||
package postgresql
|
||||
|
||||
import "context"
|
||||
|
||||
func (b PostgresBackend) DeleteEvent(ctx context.Context, id string, pubkey string) error {
|
||||
_, err := b.DB.ExecContext(ctx, "DELETE FROM event WHERE id = $1 AND pubkey = $2", id, pubkey)
|
||||
return err
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2/storage"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (b *PostgresBackend) 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 storage.ErrDupEvent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *PostgresBackend) BeforeSave(ctx context.Context, evt *nostr.Event) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (b *PostgresBackend) AfterSave(evt *nostr.Event) {
|
||||
// delete all but the 100 most recent ones for each key
|
||||
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 100 LIMIT 1
|
||||
)`, evt.PubKey, evt.Kind)
|
||||
}
|
||||
|
||||
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 = $1 AND kind = $2`
|
||||
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 = $1 AND kind = $2 AND content = $3`
|
||||
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 = $1 AND kind = $2 AND tagvalues && ARRAY[$3]`
|
||||
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 ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING`
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteBeforeSave(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
event *nostr.Event
|
||||
query string
|
||||
params []any
|
||||
shouldDelete bool
|
||||
}{
|
||||
{
|
||||
name: "set metadata",
|
||||
event: &nostr.Event{
|
||||
Kind: nostr.KindSetMetadata,
|
||||
PubKey: "pk",
|
||||
},
|
||||
query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2",
|
||||
params: []any{"pk", nostr.KindSetMetadata},
|
||||
shouldDelete: true,
|
||||
},
|
||||
{
|
||||
name: "contact list",
|
||||
event: &nostr.Event{
|
||||
Kind: nostr.KindContactList,
|
||||
PubKey: "pk",
|
||||
},
|
||||
query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2",
|
||||
params: []any{"pk", nostr.KindContactList},
|
||||
shouldDelete: true,
|
||||
},
|
||||
{
|
||||
name: "recommend server",
|
||||
event: &nostr.Event{
|
||||
Kind: nostr.KindRecommendServer,
|
||||
PubKey: "pk",
|
||||
Content: "test",
|
||||
},
|
||||
query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND content = $3",
|
||||
params: []any{"pk", nostr.KindRecommendServer, "test"},
|
||||
shouldDelete: true,
|
||||
},
|
||||
{
|
||||
name: "nip-33",
|
||||
event: &nostr.Event{
|
||||
Kind: 31000,
|
||||
PubKey: "pk",
|
||||
Tags: nostr.Tags{nostr.Tag{"d", "value"}},
|
||||
},
|
||||
query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND tagvalues && ARRAY[$3]",
|
||||
params: []any{"pk", 31000, "value"},
|
||||
shouldDelete: true,
|
||||
},
|
||||
{
|
||||
name: "kind > 10000",
|
||||
event: &nostr.Event{
|
||||
Kind: 10001,
|
||||
PubKey: "pk",
|
||||
},
|
||||
query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2",
|
||||
params: []any{"pk", 10001},
|
||||
shouldDelete: true,
|
||||
},
|
||||
{
|
||||
name: "kind < 20000",
|
||||
event: &nostr.Event{
|
||||
Kind: 19999,
|
||||
PubKey: "pk",
|
||||
},
|
||||
query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2",
|
||||
params: []any{"pk", 19999},
|
||||
shouldDelete: true,
|
||||
},
|
||||
// Should not delete cases
|
||||
{
|
||||
name: "kind < 10000",
|
||||
event: &nostr.Event{
|
||||
Kind: 9999,
|
||||
PubKey: "pk",
|
||||
},
|
||||
query: "",
|
||||
params: nil,
|
||||
shouldDelete: false,
|
||||
},
|
||||
{
|
||||
name: "kind > 21000",
|
||||
event: &nostr.Event{
|
||||
Kind: 21000,
|
||||
PubKey: "pk",
|
||||
},
|
||||
query: "",
|
||||
params: nil,
|
||||
shouldDelete: false,
|
||||
},
|
||||
{
|
||||
name: "kind 1",
|
||||
event: &nostr.Event{
|
||||
Kind: nostr.KindTextNote,
|
||||
PubKey: "pk",
|
||||
},
|
||||
query: "",
|
||||
params: nil,
|
||||
shouldDelete: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query, params, shouldDelete := deleteBeforeSaveSql(tt.event)
|
||||
assert.Equal(t, tt.query, query)
|
||||
assert.Equal(t, tt.params, params)
|
||||
assert.Equal(t, tt.shouldDelete, shouldDelete)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveEventSql(t *testing.T) {
|
||||
now := nostr.Now()
|
||||
var tests = []struct {
|
||||
name string
|
||||
event *nostr.Event
|
||||
query string
|
||||
params []any
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
event: &nostr.Event{
|
||||
ID: "id",
|
||||
PubKey: "pk",
|
||||
CreatedAt: now,
|
||||
Kind: nostr.KindTextNote,
|
||||
Content: "test",
|
||||
Sig: "sig",
|
||||
},
|
||||
query: `INSERT INTO event (
|
||||
id, pubkey, created_at, kind, tags, content, sig)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
params: []any{"id", "pk", now, nostr.KindTextNote, []byte("null"), "test", "sig"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
event: &nostr.Event{
|
||||
ID: "id",
|
||||
PubKey: "pk",
|
||||
CreatedAt: now,
|
||||
Kind: nostr.KindTextNote,
|
||||
Tags: nostr.Tags{nostr.Tag{"foo", "bar"}},
|
||||
Content: "test",
|
||||
Sig: "sig",
|
||||
},
|
||||
query: `INSERT INTO event (
|
||||
id, pubkey, created_at, kind, tags, content, sig)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
params: []any{"id", "pk", now, nostr.KindTextNote, []byte("[[\"foo\",\"bar\"]]"), "test", "sig"},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query, params, err := saveEventSql(tt.event)
|
||||
assert.Equal(t, clean(tt.query), clean(query))
|
||||
assert.Equal(t, tt.params, params)
|
||||
assert.Equal(t, tt.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package sqlite3
|
||||
|
||||
import "context"
|
||||
|
||||
func (b SQLite3Backend) DeleteEvent(ctx context.Context, id string, pubkey string) error {
|
||||
_, err := b.DB.ExecContext(ctx, "DELETE FROM event WHERE id = $1 AND pubkey = $2", id, pubkey)
|
||||
return err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package relayer
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
25
utils.go
Normal file
25
utils.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
AUTH_CONTEXT_KEY = iota
|
||||
WS_KEY = iota
|
||||
)
|
||||
|
||||
var nip20prefixmatcher = regexp.MustCompile(`^\w+: `)
|
||||
|
||||
func GetConnection(ctx context.Context) *WebSocket {
|
||||
return ctx.Value(WS_KEY).(*WebSocket)
|
||||
}
|
||||
|
||||
func GetAuthed(ctx context.Context) string {
|
||||
authedPubkey := ctx.Value(AUTH_CONTEXT_KEY)
|
||||
if authedPubkey == nil {
|
||||
return ""
|
||||
}
|
||||
return authedPubkey.(string)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package relayer
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -11,11 +11,12 @@ type WebSocket struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
// nip42
|
||||
challenge string
|
||||
authed string
|
||||
Challenge string
|
||||
Authed string
|
||||
WaitingForAuth chan struct{}
|
||||
}
|
||||
|
||||
func (ws *WebSocket) WriteJSON(any interface{}) error {
|
||||
func (ws *WebSocket) WriteJSON(any any) error {
|
||||
ws.mutex.Lock()
|
||||
defer ws.mutex.Unlock()
|
||||
return ws.conn.WriteJSON(any)
|
||||
|
||||
Reference in New Issue
Block a user