relayer, second attempt, now much better.

This commit is contained in:
fiatjaf
2023-08-10 14:32:11 -03:00
parent e4fe82dd7f
commit 8968982b9a
93 changed files with 2212 additions and 2674 deletions

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View File

@@ -1 +0,0 @@
relayer-basic

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)
}
}

View 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() {
}

View File

@@ -1 +0,0 @@
relayer-expensive

View File

@@ -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

View File

@@ -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.

View File

@@ -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})
}
}

View File

@@ -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"
}

View File

@@ -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)
}
}

View File

@@ -1,4 +0,0 @@
DOMAIN=dev2.hazlitt.fiatjaf.com
SECRET=ie32uyg48o72iv
HOST=0.0.0.0
PORT=3002

View File

@@ -1,2 +0,0 @@
relayer-rss-bridge
db

View File

@@ -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

View File

@@ -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.
![](screenshot.png)
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.

View File

@@ -1 +0,0 @@
MANIFEST-000164

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1 +0,0 @@
search

View File

@@ -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`.

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -1 +0,0 @@
relayer-whitelisted

View File

@@ -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

View File

@@ -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.

View File

@@ -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)
}
}