mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-06-08 13:49:41 +02:00
refactor framework interface, simplify basic and whitelisted, bring expensive on and rewrite it.
This commit is contained in:
1
expensive/.gitignore
vendored
Normal file
1
expensive/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
relayer-expensive
|
||||
2
expensive/Makefile
Normal file
2
expensive/Makefile
Normal file
@@ -0,0 +1,2 @@
|
||||
relayer-expensive: $(shell find .. -name "*.go")
|
||||
CC=$$(which musl-gcc) go build -ldflags='-s -w -linkmode external -extldflags "-static"' -o ./relayer-expensive
|
||||
31
expensive/README.md
Normal file
31
expensive/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
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.
|
||||
68
expensive/handler.go
Normal file
68
expensive/handler.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleWebpage(w http.ResponseWriter, r *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 = invoice
|
||||
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, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
invoice, err := generateInvoice(r.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})
|
||||
}
|
||||
}
|
||||
78
expensive/lightning.go
Normal file
78
expensive/lightning.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
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(pubkey string) (string, error) {
|
||||
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": generateLabel(pubkey),
|
||||
})
|
||||
result, _ := cln.Rpc(r.CLNRune, "listinvoices", string(jparams))
|
||||
if gjson.Get(result, "invoices.#").Int() == 1 {
|
||||
return gjson.Get(result, "invoices.1.bolt11").String(), nil
|
||||
}
|
||||
|
||||
// otherwise generate an invoice
|
||||
jparams, _ = json.Marshal(map[string]any{
|
||||
"amount_msat": r.TicketPriceSats * 1000,
|
||||
"label": generateLabel(pubkey),
|
||||
"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, "invoices.0.paid").String() == "paid"
|
||||
}
|
||||
84
expensive/main.go
Normal file
84
expensive/main.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/go-nostr"
|
||||
"github.com/fiatjaf/relayer"
|
||||
"github.com/fiatjaf/relayer/storage/postgresql"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
var r = &Relay{}
|
||||
|
||||
func (r *Relay) Name() string {
|
||||
return "ExpensiveRelay"
|
||||
}
|
||||
|
||||
func (r *Relay) Storage() relayer.Storage {
|
||||
return &postgresql.PostgresBackend{DatabaseURL: r.PostgresDatabase}
|
||||
}
|
||||
|
||||
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().(*postgresql.PostgresBackend)
|
||||
|
||||
for {
|
||||
time.Sleep(60 * time.Minute)
|
||||
db.DB.Exec(`DELETE FROM event WHERE created_at < $1`, time.Now().AddDate(0, -6, 0)) // 6 months
|
||||
}
|
||||
}()
|
||||
|
||||
// special handlers
|
||||
relayer.Router.Path("/").HandlerFunc(handleWebpage)
|
||||
relayer.Router.Path("/invoice").HandlerFunc(handleInvoice)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(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 (r *Relay) BeforeSave(evt *nostr.Event) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (r *Relay) AfterSave(evt *nostr.Event) {
|
||||
// delete all but the 1000 most recent ones for each key
|
||||
r.Storage().(*postgresql.PostgresBackend).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 1000 LIMIT 1
|
||||
)`, evt.PubKey, evt.Kind)
|
||||
}
|
||||
|
||||
func main() {
|
||||
relayer.Start(r)
|
||||
}
|
||||
Reference in New Issue
Block a user