package nostr import ( "context" b64 "encoding/base64" "encoding/json" "errors" "fmt" "html/template" "net/http" "strings" "git.minhas.io/asara/well-goknown/lnd" "git.minhas.io/asara/well-goknown/logger" "git.minhas.io/asara/well-goknown/redis" "github.com/nbd-wtf/go-nostr/nip19" "github.com/skip2/go-qrcode" ) type nostrWellKnown struct { Names map[string]string `json:"names"` Relays map[string][]string `json:"relays,omitempty"` } type jsonErrorMessage struct { Error string `json:"error"` } type NostrRequest struct { Name string `json:"name"` Key string `json:"key"` Hostname string Relays []string `json:"relays,omitempty"` } type NostrResponse struct { RequestKey string `json:"request_key"` QRCode string `json:"QRCode"` } func RequestNostrAddr(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() l := logger.Get() switch r.Method { case "GET": http.ServeFile(w, r, "html/nostr_form.html") case "POST": r.ParseForm() redisCli := redis.NostrRedisConn.Client // check if the user already exists rKey := getRkey("verified", r.FormValue("Name"), getHostname(r.Host)) exists := redisCli.Exists(ctx, rKey) if exists.Val() == 1 { w.WriteHeader(http.StatusConflict) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "username already registered", }) return } rKey = getRkey("requested", r.FormValue("Name"), getHostname(r.Host)) exists = redisCli.Exists(ctx, rKey) if exists.Val() == 1 { w.WriteHeader(http.StatusConflict) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "username already requested, try again in a few minutes", }) return } // get the hexkey hexKey, err := convertNpubToHex(r.FormValue("Key")) if err != nil { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: err.Error(), }) return } // create the struct relays := make(map[string][]string) names := map[string]string{r.FormValue("Name"): hexKey} user := nostrWellKnown{} if r.FormValue("Relays") != "" { relays = map[string][]string{ hexKey: strings.Split(r.FormValue("Relays"), ","), } user = nostrWellKnown{Names: names, Relays: relays} } else { user = nostrWellKnown{Names: names} } jsonUser, _ := json.Marshal(user) enc := b64.StdEncoding.EncodeToString([]byte(jsonUser)) // generate the payment request paymentReq, err := lnd.Request(rKey) if err != nil { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "service unavailable", }) return } // generate qr png png, err := qrcode.Encode(paymentReq, qrcode.Medium, 256) pngB64 := b64.StdEncoding.EncodeToString(png) // write request to redis err = redisCli.Set(ctx, rKey, enc, 0).Err() if err != nil { l.Error().Msg("unable to connect to redis") w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "service unavailable", }) return } // return qr code response := NostrResponse{ RequestKey: rKey, QRCode: pngB64, } tmplt, err := template.ParseFiles("html/nostr_qr_response.html") if err != nil { l.Error().Msg("unable to parse template") w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "service unavailable", }) return } err = tmplt.Execute(w, response) if err != nil { l.Error().Msg("unable to connect to render template") w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "service unavailable", }) return } return } } func GetNostrAddr(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() // get query string for username r.ParseForm() requestedName := r.FormValue("name") // connect to redis redisCli := redis.NostrRedisConn.Client // search for user@domain search := getRkey("verified", requestedName, getHostname(r.Host)) addr, err := redisCli.Get(ctx, search).Result() if err != nil { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "username not registered", }) return } dec, err := b64.StdEncoding.DecodeString(addr) if err != nil { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(&jsonErrorMessage{ Error: "internal server error", }) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(dec) } func AddNostrAddr(n NostrRequest) { ctx := context.TODO() l := logger.Get() // transform request to well known struct relays := make(map[string][]string) user := nostrWellKnown{} hexKey, err := convertNpubToHex(n.Key) if err != nil { l.Error().Msg("unable to convert npub to hex") } names := map[string]string{n.Name: hexKey} if n.Relays != nil { relays = map[string][]string{hexKey: n.Relays} user = nostrWellKnown{Names: names, Relays: relays} } else { user = nostrWellKnown{Names: names} } jsonUser, _ := json.Marshal(user) redisCli := redis.NostrRedisConn.Client nameKey := getRkey("verified", n.Name, n.Hostname) enc := b64.StdEncoding.EncodeToString([]byte(jsonUser)) err = redisCli.Set(ctx, nameKey, enc, 0).Err() if err != nil { l.Error().Msg("unable to connect to redis") } } func getHostname(hostname string) string { // remove port for local testing return hostname } func convertNpubToHex(npub string) (string, error) { if !strings.HasPrefix(npub, "npub1") { return "", errors.New("key does not start with npub1 prefix") } _, hex, err := nip19.Decode(npub) if err != nil { return "", errors.New("unable to decode npub key") } return hex.(string), nil } func getRkey(prefix string, user string, hostname string) string { if strings.Contains(hostname, ":") { hostname = strings.Split(hostname, ":")[0] } return fmt.Sprintf("%s:%s@%s", prefix, user, hostname) }