diff --git a/nip96/nip96.go b/nip96/nip96.go deleted file mode 100644 index 73162c7..0000000 --- a/nip96/nip96.go +++ /dev/null @@ -1,137 +0,0 @@ -package nip96 - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "fmt" - "hash" - "io" - "mime/multipart" - "net/http" - "strconv" - - jsoniter "github.com/json-iterator/go" - "fiatjaf.com/nostr" -) - -// Upload uploads a file to the provided req.Host. -func Upload(ctx context.Context, req UploadRequest) (*UploadResponse, error) { - if err := req.Validate(); err != nil { - return nil, err - } - - client := http.DefaultClient - if req.HTTPClient != nil { - client = req.HTTPClient - } - - var requestBody bytes.Buffer - fileHash := sha256.New() - writer := multipart.NewWriter(&requestBody) - { - // Add the file - fileWriter, err := writer.CreateFormFile("file", req.Filename) - if err != nil { - return nil, fmt.Errorf("multipartWriter.CreateFormFile: %w", err) - } - if _, err := io.Copy(fileWriter, io.TeeReader(req.File, fileHash)); err != nil { - return nil, fmt.Errorf("io.Copy: %w", err) - } - - // Add the other fields - writer.WriteField("caption", req.Caption) - writer.WriteField("alt", req.Alt) - writer.WriteField("media_type", req.MediaType) - writer.WriteField("content_type", req.ContentType) - writer.WriteField("no_transform", fmt.Sprintf("%t", req.NoTransform)) - if req.Expiration == 0 { - writer.WriteField("expiration", "") - } else { - writer.WriteField("expiration", strconv.FormatInt(int64(req.Expiration), 10)) - } - - if err := writer.Close(); err != nil { - return nil, fmt.Errorf("multipartWriter.Close: %w", err) - } - } - - uploadReq, err := http.NewRequest("POST", req.Host, &requestBody) - if err != nil { - return nil, fmt.Errorf("http.NewRequest: %w", err) - } - uploadReq.Header.Set("Content-Type", writer.FormDataContentType()) - - if req.SK != "" { - if !req.SignPayload { - fileHash = nil - } - auth, err := generateAuthHeader(req.SK, req.Host, fileHash) - if err != nil { - return nil, fmt.Errorf("generateAuthHeader: %w", err) - } - uploadReq.Header.Set("Authorization", auth) - } - - resp, err := client.Do(uploadReq) - if err != nil { - return nil, fmt.Errorf("httpclient.Do: %w", err) - } - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusRequestEntityTooLarge: - return nil, fmt.Errorf("File is too large") - - case http.StatusBadRequest: - return nil, fmt.Errorf("Bad request") - - case http.StatusForbidden: - return nil, fmt.Errorf("Unauthorized") - - case http.StatusPaymentRequired: - return nil, fmt.Errorf("Payment required") - - case http.StatusOK, http.StatusCreated, http.StatusAccepted: - var uploadResp UploadResponse - if err := jsoniter.NewDecoder(resp.Body).Decode(&uploadResp); err != nil { - return nil, fmt.Errorf("Error decoding JSON: %w", err) - } - return &uploadResp, nil - - default: - return nil, fmt.Errorf("Unexpected error %v", resp.Status) - } -} - -func generateAuthHeader(sk, host string, fileHash hash.Hash) (string, error) { - pk, err := nostr.GetPublicKey(sk) - if err != nil { - return "", fmt.Errorf("nostr.GetPublicKey: %w", err) - } - - event := nostr.Event{ - Kind: 27235, - PubKey: pk, - CreatedAt: nostr.Now(), - Tags: nostr.Tags{ - nostr.Tag{"u", host}, - nostr.Tag{"method", "POST"}, - }, - } - if fileHash != nil { - event.Tags = append(event.Tags, nostr.Tag{"payload", hex.EncodeToString(fileHash.Sum(nil))}) - } - event.Sign(sk) - - b, err := jsoniter.ConfigFastest.Marshal(event) - if err != nil { - return "", fmt.Errorf("json.Marshal: %w", err) - } - - payload := base64.StdEncoding.EncodeToString(b) - - return fmt.Sprintf("Nostr %s", payload), nil -} diff --git a/nip96/nip96_test.go b/nip96/nip96_test.go deleted file mode 100644 index ca87c33..0000000 --- a/nip96/nip96_test.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build !js - -package nip96 - -import ( - "context" - "os" - "testing" - - "fiatjaf.com/nostr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestUpload(t *testing.T) { - img, err := os.Open("./testdata/image.png") - require.NoError(t, err) - - defer img.Close() - - ctx := context.Background() - resp, err := Upload(ctx, UploadRequest{ - Host: "https://nostr.build/api/v2/nip96/upload", - // Host: "https://nostrcheck.me/api/v2/media", - // Host: "https://nostrage.com/api/v2/media", - SK: nostr.GeneratePrivateKey(), - SignPayload: true, - File: img, - Filename: "ostrich.png", - Caption: "nostr ostrich", - ContentType: "image/png", - NoTransform: true, - }) - assert.NoError(t, err) - - t.Logf("resp: %#v\n", *resp) - // nip96_test.go:28: resp: nip96.UploadResponse{Status:"success", Message:"Upload successful.", ProcessingURL:"", Nip94Event:struct { Tags nostr.Tags "json:\"tags\"" }{Tags:nostr.Tags{nostr.Tag{"url", "https://image.nostr.build/4ece05f1d77c9cb97d334ba9c0301b2960640df89bf5d75d6bffadefc4355673.jpg"}, nostr.Tag{"ox", "4ece05f1d77c9cb97d334ba9c0301b2960640df89bf5d75d6bffadefc4355673"}, nostr.Tag{"x", ""}, nostr.Tag{"m", "image/jpeg"}, nostr.Tag{"dim", "1125x750"}, nostr.Tag{"bh", "LLF=kB-;yH-;-;R#t7xKEZWA#_oM"}, nostr.Tag{"blurhash", "LLF=kB-;yH-;-;R#t7xKEZWA#_oM"}, nostr.Tag{"thumb", "https://image.nostr.build/thumb/4ece05f1d77c9cb97d334ba9c0301b2960640df89bf5d75d6bffadefc4355673.jpg"}}}} -} diff --git a/nip96/testdata/image.png b/nip96/testdata/image.png deleted file mode 100644 index ee155e6..0000000 Binary files a/nip96/testdata/image.png and /dev/null differ diff --git a/nip96/types.go b/nip96/types.go deleted file mode 100644 index 11d86d4..0000000 --- a/nip96/types.go +++ /dev/null @@ -1,73 +0,0 @@ -package nip96 - -import ( - "fmt" - "io" - "net/http" - - "fiatjaf.com/nostr" -) - -// UploadRequest is a NIP96 upload request. -type UploadRequest struct { - // Host is the NIP96 server to upload to. - Host string - - // SK is a private key used to sign the NIP-98 Auth header. If not set - // the auth header will not be included in the upload. - SK string - // Optional signing of payload (file) as described in NIP-98, if enabled - // includes `payload` tag with file's sha256 in signed event / auth header. - SignPayload bool - - // File is the file to upload. - File io.Reader - - // Filename is the name of the file, e.g. image.png - Filename string - - // Caption is a loose description of the file. - Caption string - - // Alt is a strict description text for visibility-impaired users. - Alt string - - // MediaType is "avatar" or "banner". Informs the server if the file will be - // used as an avatar or banner. If absent, the server will interpret it as a - // normal upload, without special treatment. - MediaType string - - // ContentType is the mime type such as "image/jpeg". This is just a value the - // server can use to reject early if the mime type isn't supported. - ContentType string - - // NoTransform set to "true" asks server not to transform the file and serve - // the uploaded file as is, may be rejected. - NoTransform bool - - // Expiration is a UNIX timestamp in seconds. Empty if file should be stored - // forever. The server isn't required to honor this. - Expiration nostr.Timestamp - - // HTTPClient is an option to provide your own HTTP Client. - HTTPClient *http.Client -} - -func (r *UploadRequest) Validate() error { - if r.Host == "" { - return fmt.Errorf("Host must be set") - } - - return nil -} - -// UploadResponse is a NIP96 upload response. -type UploadResponse struct { - Status string `json:"status"` - Message string `json:"message"` - ProcessingURL string `json:"processing_url"` - Nip94Event struct { - Tags nostr.Tags `json:"tags"` - Content string `json:"content"` - } `json:"nip94_event"` -}