delete nip96.
This commit is contained in:
137
nip96/nip96.go
137
nip96/nip96.go
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"}}}}
|
|
||||||
}
|
|
||||||
BIN
nip96/testdata/image.png
vendored
BIN
nip96/testdata/image.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
@@ -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"`
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user