From d1199c323ac7219060da51749bee2f0e9c2bd70e Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 27 Mar 2023 23:38:46 +0200 Subject: [PATCH] added nostr bot capabilities --- README.md | 4 +-- db.go | 11 ++++--- index.html | 20 +++++++++++- lnurl.go | 26 ++++++++++++++-- main.go | 61 ++++++++++++++++++++++++------------ nostr.go | 78 +++++++++++++++++++++++++++++++++++++++++------ waitforinvoice.go | 12 ++++++++ 7 files changed, 173 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index af88e4e..47e28f2 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ NOSTR_PRIVATE_KEY="nsec123" FORWARD_URL="/" NIP05=true GET_NOSTR_PROFILE=false -LND_PRIVATE_ONLY=false ``` 3. Start the app with `./satdress` or `nohup ./satdress &` for a background task @@ -58,7 +57,8 @@ Maybe ask for help on https://t.me/lnurl if you're in trouble. ## Status of the Fork: - NIP57 for Nostr ("Zaps") work when using an LNBits or LND backend, other backends (sparko, lnpay, eclair, commando) still need verification of payments in waitforinvoice.go by API calls in order to sign the zap on Nostr. (Help appreciated, because I can't test them) - NIP05 support: If user added a npub, they can use lnaddress for Nostr NIP05 verificaton -- Downloads Profile pictures when given npub key (for supported wallets, e.g. blue wallet) +- Acts as a Bot that sends Nostr messages to users when they receive a LN Payment (if set in options for Zaps with/without comments and non Zaps (lnaddress payments)) +- Downloads Profile pictures when given npub key (for supported wallets, e.g. blue wallet) and GET_NOSTR_PROFILE=true - Addded possibility to forward lightning addresses to existing ones (e.g. Wallet of Satoshi) - Added possibility to add a forward main page, go to /lnaddress to add new users - Added an alternative API '/api/easy' that deletes users and creates new name and pin for them diff --git a/db.go b/db.go index 18fb0a3..378f42a 100644 --- a/db.go +++ b/db.go @@ -24,10 +24,13 @@ type Params struct { NodeId string `json:"nodeid"` Rune string `json:"rune"` - Pin string `json:"pin"` - MinSendable string `json:"minSendable"` - MaxSendable string `json:"maxSendable"` - Npub string `json:"npub"` + Pin string `json:"pin"` + MinSendable string `json:"minSendable"` + MaxSendable string `json:"maxSendable"` + Npub string `json:"npub"` + NotifyZaps bool `json:"notifyzaps"` + NotifyZapComment bool `json:"notifycomments"` + NotifyNonZap bool `json:"notifynonzaps"` } func SaveName( diff --git a/index.html b/index.html index 300c0d4..0ecb01c 100644 --- a/index.html +++ b/index.html @@ -70,7 +70,7 @@ class="input full-width" name="host" id="host" - placeholder="https://walletofsatoshi.com/.well-known/lnurlp/ninthtractor31" + placeholder="https://walletofsatoshi.com/.well-known/lnurlp/satoshinakamoto58k" /> @@ -190,6 +190,24 @@ id="npub" placeholder="npub123..." /> +
+ + + + + + +
diff --git a/lnurl.go b/lnurl.go index 042162f..9d0df28 100644 --- a/lnurl.go +++ b/lnurl.go @@ -51,6 +51,7 @@ type LNURLPayValuesCustom struct { Nip57Receipt nostr.Event `json:"nip57Receipt"` Nip57ReceiptRelays []string `json:"nip57ReceiptRelays"` AwaitInvoicePaid bool `json:"awaitInvoicePaid"` + Sender string `json:"sender"` } func handleLNURL(w http.ResponseWriter, r *http.Request) { @@ -143,6 +144,7 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) { } var comment = "" + var payerData lnurl.PayerDataValues // nostr NIP-57 // the "nostr" query param has a zap request which is a nostr event // that specifies which nostr note has been zapped. @@ -172,8 +174,9 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) { } } + //We can't handle comments and payerdata in NIP57 at the same time... - //If a comment is send with the Invoice, always use it (?) + // If a comment is send with the Invoice, always use it (?) regularcomment := r.FormValue("comment") if len(regularcomment) > CommentAllowed { log.Error().Err(err).Str("Comment is too long", err.Error()) @@ -185,7 +188,7 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) { } // payer data, not used currently payerdata := r.FormValue("payerdata") - var payerData lnurl.PayerDataValues + if len(payerdata) > 0 { err = json.Unmarshal([]byte(payerdata), &payerData) if err != nil { @@ -213,6 +216,18 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) { //in order to submit the zap on nostr if allowNostr && payvaluescustom.AwaitInvoicePaid { go WaitForInvoicePaid(payvaluescustom, params) + } else if params.Npub != "" && params.NotifyNonZap { + var amount = payvaluescustom.ParsedInvoice.MSatoshi / 1000 + var satsr = "Sats" + if amount == 1 { + satsr = "Sat" + } + if payvaluescustom.Comment != "" { + go sendMessage(params.Npub, "Received Non-Zap! Amount: "+strconv.FormatInt(amount, 10)+" "+satsr+" ⚡️. Comment: "+payvaluescustom.Comment) + + } else { + go sendMessage(params.Npub, "Received Non-Zap! Amount: "+strconv.FormatInt(amount, 10)+" "+satsr+" ⚡️.") + } } } } @@ -231,6 +246,7 @@ func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, a // NIP57 ZAPs // for nip57 use the nostr event as the descriptionHash if zapEvent.Sig != "" { + // we calculate the descriptionHash here, create an invoice with it // and store the invoice in the zap receipt later down the line zapEventSerialized, err := json.Marshal(zapEvent) @@ -265,12 +281,17 @@ func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, a //Check invoice paid only if we actually have a NIP57 event var awaitPaid = false + var sender = "" // nip57 - we need to store the newly created invoice in the zap receipt if zapEvent.Sig != "" { nip57Receipt = CreateNostrReceipt(zapEvent, invoice) awaitPaid = true + sender = "@" + EncodeBench32Public(zapEvent.PubKey) + log.Debug().Str("Zap from", sender).Msg("Nostr") } + //var sender = zapEvent.Tags.GetFirst([]string{"pubkey"}) + decoded_invoice, _ := decodepay.Decodepay(invoice) return LNURLPayValuesCustom{ LNURLResponse: lnurl.LNURLResponse{Status: "OK"}, @@ -284,6 +305,7 @@ func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, a Nip57Receipt: nip57Receipt, Nip57ReceiptRelays: nip57ReceiptRelays, AwaitInvoicePaid: awaitPaid, + Sender: sender, }, nil } diff --git a/main.go b/main.go index 6f63980..ecee14d 100644 --- a/main.go +++ b/main.go @@ -25,18 +25,20 @@ type Settings struct { Domain string `envconfig:"DOMAIN" required:"true"` // GlobalUsers means that user@ part is globally unique across all domains // WARNING: if you toggle this existing users won't work anymore for safety reasons! - GlobalUsers bool `envconfig:"GLOBAL_USERS" default:"false"` - Secret string `envconfig:"SECRET" required:"true"` - SiteOwnerName string `envconfig:"SITE_OWNER_NAME" required:"true"` - SiteOwnerURL string `envconfig:"SITE_OWNER_URL" required:"true"` - SiteName string `envconfig:"SITE_NAME" required:"true"` - NostrPrivateKey string `envconfig:"NOSTR_PRIVATE_KEY" required:"false" default:""` - ForwardMainPageUrl string `envconfig:"FORWARD_URL" required:"false"` - Nip05 bool `envconfig:"NIP05" default:"false" required:"false"` - GetNostrProfile bool `envconfig:"GET_NOSTR_PROFILE" required:"false" default:"false"` - ForceMigrate bool `envconfig:"FORCE_MIGRATE" default:"false"` - TorProxyURL string `envconfig:"TOR_PROXY_URL"` - LNDprivateOnly bool `envconfig:"LND_PRIVATE_ONLY" required:"false" default:"false"` + GlobalUsers bool `envconfig:"GLOBAL_USERS" default:"false"` + Secret string `envconfig:"SECRET" required:"true"` + SiteOwnerName string `envconfig:"SITE_OWNER_NAME" required:"true"` + SiteOwnerURL string `envconfig:"SITE_OWNER_URL" required:"true"` + SiteName string `envconfig:"SITE_NAME" required:"true"` + NostrPrivateKey string `envconfig:"NOSTR_PRIVATE_KEY" required:"false" default:""` + ForwardMainPageUrl string `envconfig:"FORWARD_URL" required:"false"` + Nip05 bool `envconfig:"NIP05" default:"false" required:"false"` + GetNostrProfile bool `envconfig:"GET_NOSTR_PROFILE" required:"false" default:"false"` + ForceMigrate bool `envconfig:"FORCE_MIGRATE" default:"false"` + TorProxyURL string `envconfig:"TOR_PROXY_URL"` + NotifyNostrUsersCommentOnly bool `envconfig:"NOTIFY_NOSTR_USERS_COMMENT" required:"false" default:"false"` + NotifyNostrUsers bool `envconfig:"NOTIFY_NOSTR_USERS" required:"false" default:"false"` + LNDprivateOnly bool `envconfig:"LND_PRIVATE_ONLY" required:"false" default:"false"` } var ( @@ -128,15 +130,34 @@ func main() { } } + r.ParseForm() + v1 := r.FormValue("notifyzaps") + var notifyZaps = false + if v1 == "on" { + notifyZaps = true + } + v2 := r.FormValue("notifycomments") + var notifyComments = false + if v2 == "on" { + notifyComments = true + } + v3 := r.FormValue("notifynonzaps") + var notifyNonZaps = false + if v3 == "on" { + notifyNonZaps = true + } pin, inv, err := SaveName(name, domain, &Params{ - Kind: r.FormValue("kind"), - Host: r.FormValue("host"), - Key: r.FormValue("key"), - Pak: r.FormValue("pak"), - Waki: r.FormValue("waki"), - NodeId: r.FormValue("nodeid"), - Rune: r.FormValue("rune"), - Npub: r.FormValue("npub"), + Kind: r.FormValue("kind"), + Host: r.FormValue("host"), + Key: r.FormValue("key"), + Pak: r.FormValue("pak"), + Waki: r.FormValue("waki"), + NodeId: r.FormValue("nodeid"), + Rune: r.FormValue("rune"), + Npub: r.FormValue("npub"), + NotifyZaps: notifyZaps, + NotifyZapComment: notifyComments, + NotifyNonZap: notifyNonZaps, }, r.FormValue("pin"), false, "") if err != nil { w.WriteHeader(500) diff --git a/nostr.go b/nostr.go index dc8d667..d1e0360 100644 --- a/nostr.go +++ b/nostr.go @@ -11,6 +11,7 @@ import ( "time" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" "github.com/nbd-wtf/go-nostr/nip19" ) @@ -38,14 +39,69 @@ func Nip57DescriptionHash(zapEventSerialized string) string { func DecodeBench32(key string) string { if _, v, err := nip19.Decode(key); err == nil { - privatekeyhex := v.(string) - nostrPrivkeyHex = privatekeyhex - return nostrPrivkeyHex + return v.(string) } return key } +func EncodeBench32Public(key string) string { + if v, err := nip19.EncodePublicKey(key); err == nil { + return v + } + return key +} + +func EncodeBench32Private(key string) string { + if v, err := nip19.EncodePrivateKey(key); err == nil { + return v + } + return key +} + +func sendMessage(receiverKey string, message string) { + + var relays []string + var tags nostr.Tags + reckey := DecodeBench32(receiverKey) + tags = append(tags, nostr.Tag{"p", reckey}) + + //references, err := optSlice(opts, "--reference") + //if err != nil { + // return + //} + //for _, ref := range references { + //tags = append(tags, nostr.Tag{"e", reckey}) + //} + + // parse and encrypt content + privkeyhex := DecodeBench32(s.NostrPrivateKey) + pubkey, _ := nostr.GetPublicKey(privkeyhex) + + sharedSecret, err := nip04.ComputeSharedSecret(reckey, privkeyhex) + if err != nil { + log.Printf("Error computing shared key: %s. x\n", err.Error()) + return + } + + encryptedMessage, err := nip04.Encrypt(message, sharedSecret) + if err != nil { + log.Printf("Error encrypting message: %s. \n", err.Error()) + return + } + + event := nostr.Event{ + PubKey: pubkey, + CreatedAt: time.Now(), + Kind: nostr.KindEncryptedDirectMessage, + Tags: tags, + Content: encryptedMessage, + } + event.Sign(privkeyhex) + publishNostrEvent(event, relays) + log.Printf("%+v\n", event) +} + func handleNip05(w http.ResponseWriter, r *http.Request) { var err error var response string @@ -57,8 +113,9 @@ func handleNip05(w http.ResponseWriter, r *http.Request) { var middlestring = "" for _, user := range allusers { + nostrnpubHex := DecodeBench32(user.Npub) if user.Npub != "" { //do some more validation checks - middlestring = middlestring + "\t\"" + user.Name + "\"" + ": " + "\"" + DecodeBench32(user.Npub) + "\"" + ",\n" + middlestring = middlestring + "\t\"" + user.Name + "\"" + ": " + "\"" + nostrnpubHex + "\"" + ",\n" } } @@ -131,9 +188,7 @@ func GetNostrProfileMetaData(npub string) (nostr.ProfileMetadata, error) { } func publishNostrEvent(ev nostr.Event, relays []string) { - pk := s.NostrPrivateKey - ev.Sign(pk) - log.Debug().Str("publishing nostr event %s", ev.ID) + // more relays relays = append(relays, "wss://relay.nostr.ch", "wss://eden.nostr.land", "wss://nostr.btcmp.com", "wss://nostr.relayer.se", "wss://relay.current.fyi", "wss://nos.lol", "wss://nostr.mom", "wss://relay.nostr.info", "wss://nostr.zebedee.cloud", "wss://nostr-pub.wellorder.net", "wss://relay.snort.social/", "wss://relay.damus.io/", "wss://nostr.oxtr.dev/", "wss://nostr.fmt.wiz.biz/", "wss://brb.io") // remove trailing / @@ -141,18 +196,21 @@ func publishNostrEvent(ev nostr.Event, relays []string) { // unique relays relays = uniqueSlice(relays) + ev.Sign(s.NostrPrivateKey) + + //log.Printf("publishing nostr event %s", ev.ID) // publish the event to relays for _, url := range relays { go func(url string) { - // remove trailing / - relay, e := nostr.RelayConnect(context.Background(), url) + ctx := context.WithValue(context.Background(), "url", url) + relay, e := nostr.RelayConnect(ctx, url) if e != nil { log.Error().Str(e.Error(), e.Error()) return } time.Sleep(3 * time.Second) - status := relay.Publish(context.Background(), ev) + status := relay.Publish(ctx, ev) log.Info().Str("[NOSTR] published to %s:", status.String()) time.Sleep(3 * time.Second) diff --git a/waitforinvoice.go b/waitforinvoice.go index cea79ce..5e11519 100644 --- a/waitforinvoice.go +++ b/waitforinvoice.go @@ -168,9 +168,21 @@ func WaitForInvoicePaid(payvalues LNURLPayValuesCustom, params *Params) { //If invoice is paid and DescriptionHash matches Nip57 DescriptionHash, publish Zap Nostr Event. This is rather a sanity check. if payvalues.Paid { + var amount = bolt11.MSatoshi / 1000 var descriptionTag = *payvalues.Nip57Receipt.Tags.GetFirst([]string{"description"}) if bolt11.DescriptionHash == Nip57DescriptionHash(descriptionTag.Value()) { publishNostrEvent(payvalues.Nip57Receipt, payvalues.Nip57ReceiptRelays) + var satsr = "Sats" + if amount == 1 { + satsr = "Sat" + } + + if params.Npub != "" && params.NotifyZapComment && payvalues.Comment != "" { + go sendMessage(params.Npub, "Received Zap from "+payvalues.Sender+" with amount: "+strconv.FormatInt(amount, 10)+" "+satsr+" ⚡️. Comment: "+payvalues.Comment) + } else if params.Npub != "" && params.NotifyZaps { + go sendMessage(params.Npub, "Received Zap from "+payvalues.Sender+" with amount: "+strconv.FormatInt(amount, 10)+" "+satsr+" ⚡️.") + } + log.Debug().Str("ZAPPED ⚡️", "Published zap on Nostr").Msg("Nostr") close(quit) return