package main import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "strings" "sync" "time" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" "github.com/nbd-wtf/go-nostr/nip19" ) type Tag []string type Tags []Tag type NostrEvent struct { ID string `json:"id"` PubKey string `json:"pubkey"` CreatedAt time.Time `json:"created_at"` Kind int `json:"kind"` Tags Tags `json:"tags"` Content string `json:"content"` Sig string `json:"sig"` } var nip57Receipt nostr.Event var zapEventSerializedStr string var nip57ReceiptRelays []string func Nip57DescriptionHash(zapEventSerialized string) string { hash := sha256.Sum256([]byte(zapEventSerialized)) hashString := hex.EncodeToString(hash[:]) return hashString } func DecodeBench32(key string) string { if _, v, err := nip19.Decode(key); err == nil { 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 EncodeBench32Note(key string) string { if v, err := nip19.EncodeNote(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 var allusers []Params allusers, err = GetAllUsers(s.Domain) firstpartstring := "{\n \"names\": {\n" finalpartstring := " \t}\n}" var middlestring = "" for _, user := range allusers { nostrnpubHex := DecodeBench32(user.Npub) if user.Npub != "" { //do some more validation checks middlestring = middlestring + "\t\"" + user.Name + "\"" + ": " + "\"" + nostrnpubHex + "\"" + ",\n" } } if s.Nip05 { //Remove ',' from last entry if len(middlestring) > 2 { middlestringtrim := middlestring[:len(middlestring)-2] middlestringtrim += "\n" response = firstpartstring + middlestringtrim + finalpartstring } w.Header().Set("Content-Type", "application/json; charset=utf-8") fmt.Fprintf(w, response) } else { return } if err != nil { return } } func GetNostrProfileMetaData(npub string) (nostr.ProfileMetadata, error) { ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) var metadata *nostr.ProfileMetadata // connect to any relay url := "wss://relay.damus.io" relay, err := nostr.RelayConnect(ctx, url) if err != nil { return *metadata, err } // create filters var filters nostr.Filters if _, v, err := nip19.Decode(npub); err == nil { t := make(map[string][]string) t["p"] = []string{v.(string)} filters = []nostr.Filter{{ Authors: []string{v.(string)}, Kinds: []int{0}, // limit = 3, get the three most recent notes Limit: 1, }} } else { return *metadata, err } sub, err := relay.Subscribe(ctx, filters) evs := make([]nostr.Event, 0) go func() { <-sub.EndOfStoredEvents }() for ev := range sub.Events { evs = append(evs, *ev) } relay.Close() if len(evs) > 0 { metadata, err = nostr.ParseMetadata(evs[0]) } else { err = fmt.Errorf("no profile found for npub %s on relay %s", npub, url) } return *metadata, err } func publishNostrEvent(ev nostr.Event, relays []string) { // Add more relays, remove trailing slashes, and ensure unique relays relays = uniqueSlice(cleanUrls(append(relays, Relays...))) ev.Sign(s.NostrPrivateKey) var wg sync.WaitGroup wg.Add(len(relays)) // Publish the event to relays for _, url := range relays { go func(url string) { defer wg.Done() ctx := context.WithValue(context.Background(), "url", url) relay, err := nostr.RelayConnect(ctx, url) if err != nil { log.Printf("Error connecting to relay %s: %v", url, err) return } defer relay.Close() time.Sleep(3 * time.Second) status, err := relay.Publish(ctx, ev) if err != nil { log.Printf("Error publishing to relay %s: %v", url, err) } else { log.Printf("[NOSTR] published to %s: %s", url, status) } time.Sleep(3 * time.Second) }(url) } wg.Wait() } func ExtractNostrRelays(zapEvent nostr.Event) []string { relaysTag := zapEvent.Tags.GetFirst([]string{"relays"}) if relaysTag == nil || len(*relaysTag) == 0 { return []string{} } relaysStr := strings.TrimPrefix((*relaysTag)[0], "relays") relays := strings.Split(strings.TrimSpace(relaysStr), " ") return relays } func CreateNostrReceipt(zapEvent nostr.Event, invoice string) (nostr.Event, error) { pub, err := nostr.GetPublicKey(nostrPrivkeyHex) if err != nil { return nostr.Event{}, err } zapEventSerialized, err := json.Marshal(zapEvent) if err != nil { return nostr.Event{}, err } nip57Receipt := nostr.Event{ PubKey: pub, CreatedAt: time.Now(), Kind: 9735, Tags: nostr.Tags{ *zapEvent.Tags.GetFirst([]string{"p"}), []string{"bolt11", invoice}, []string{"description", string(zapEventSerialized)}, }, } if eTag := zapEvent.Tags.GetFirst([]string{"e"}); eTag != nil { nip57Receipt.Tags = nip57Receipt.Tags.AppendUnique(*eTag) } err = nip57Receipt.Sign(nostrPrivkeyHex) if err != nil { return nostr.Event{}, err } return nip57Receipt, nil } func uniqueSlice(slice []string) []string { keys := make(map[string]bool) list := make([]string, 0, len(slice)) for _, entry := range slice { if _, exists := keys[entry]; !exists { keys[entry] = true list = append(list, entry) } } return list } func cleanUrls(slice []string) []string { list := make([]string, 0, len(slice)) for _, entry := range slice { if strings.HasSuffix(entry, "/") { entry = entry[:len(entry)-1] } list = append(list, entry) } return list }