added nostr bot capabilities

This commit is contained in:
Believethehype
2023-03-27 23:38:46 +02:00
parent 6f4ee0a58a
commit d1199c323a
7 changed files with 173 additions and 39 deletions
+2 -2
View File
@@ -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
+7 -4
View File
@@ -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(
+19 -1
View File
@@ -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"
/>
</div>
@@ -190,6 +190,24 @@
id="npub"
placeholder="npub123..."
/>
<div class="field">
<label style="float: right">
Get notifed on Nostr</label>
<label style="float: left">
Zaps with Comments
<input type="checkbox" id="notifycomments" name="notifycomments" v-model="notifycomments" checked />
</label>
<label style="float: left">
Zaps without Comments
<input type="checkbox" id="notifyzaps" name="notifyzaps" v-model="notifyzaps" checked />
</label>
<label style="float: left">
Non Zaps
<input type="checkbox" id="notifynonzaps" name="notifynonzaps" v-model="notifynonzaps" checked/>
</label>
</label>
</div>
</div>
<button class="submit">Submit</button>
</form>
+24 -2
View File
@@ -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
}
+41 -20
View File
@@ -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)
+68 -10
View File
@@ -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)
+12
View File
@@ -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