diff --git a/nip94/nip94.go b/nip94/nip94.go index 15ef6a3..75a5efb 100644 --- a/nip94/nip94.go +++ b/nip94/nip94.go @@ -51,6 +51,7 @@ type FileMetadata struct { TorrentInfoHash string Blurhash string Thumb string + Content string } func (fm FileMetadata) IsVideo() bool { return strings.Split(fm.M, "/")[0] == "video" } diff --git a/nip96/nip96.go b/nip96/nip96.go index 0515afa..f940b3f 100644 --- a/nip96/nip96.go +++ b/nip96/nip96.go @@ -3,9 +3,12 @@ package nip96 import ( "bytes" "context" + "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" + "hash" "io" "mime/multipart" "net/http" @@ -26,6 +29,7 @@ func Upload(ctx context.Context, req UploadRequest) (*UploadResponse, error) { } var requestBody bytes.Buffer + fileHash := sha256.New() writer := multipart.NewWriter(&requestBody) { // Add the file @@ -33,7 +37,7 @@ func Upload(ctx context.Context, req UploadRequest) (*UploadResponse, error) { if err != nil { return nil, fmt.Errorf("multipartWriter.CreateFormFile: %w", err) } - if _, err := io.Copy(fileWriter, req.File); err != nil { + if _, err := io.Copy(fileWriter, io.TeeReader(req.File, fileHash)); err != nil { return nil, fmt.Errorf("io.Copy: %w", err) } @@ -61,7 +65,10 @@ func Upload(ctx context.Context, req UploadRequest) (*UploadResponse, error) { uploadReq.Header.Set("Content-Type", writer.FormDataContentType()) if req.SK != "" { - auth, err := generateAuthHeader(req.SK, req.Host) + if !req.SignPayload { + fileHash = nil + } + auth, err := generateAuthHeader(req.SK, req.Host, fileHash) if err != nil { return nil, fmt.Errorf("generateAuthHeader: %w", err) } @@ -99,7 +106,7 @@ func Upload(ctx context.Context, req UploadRequest) (*UploadResponse, error) { } } -func generateAuthHeader(sk, host string) (string, error) { +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) @@ -114,6 +121,9 @@ func generateAuthHeader(sk, host string) (string, error) { 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 := json.Marshal(event) diff --git a/nip96/nip96_test.go b/nip96/nip96_test.go index 4d1c412..dc95e0e 100644 --- a/nip96/nip96_test.go +++ b/nip96/nip96_test.go @@ -22,6 +22,7 @@ func TestUpload(t *testing.T) { //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", diff --git a/nip96/types.go b/nip96/types.go index 376825b..47382e5 100644 --- a/nip96/types.go +++ b/nip96/types.go @@ -16,6 +16,9 @@ type UploadRequest struct { // 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 @@ -64,6 +67,7 @@ type UploadResponse struct { Message string `json:"message"` ProcessingURL string `json:"processing_url"` Nip94Event struct { - Tags nostr.Tags `json:"tags"` + Tags nostr.Tags `json:"tags"` + Content string `json:"content"` } `json:"nip94_event"` }