diff options
Diffstat (limited to 'internal/api')
-rw-r--r-- | internal/api/client/media/media.go | 29 | ||||
-rw-r--r-- | internal/api/client/media/mediacreate.go | 23 | ||||
-rw-r--r-- | internal/api/client/media/mediacreate_test.go | 121 | ||||
-rw-r--r-- | internal/api/client/media/mediaupdate_test.go | 8 | ||||
-rw-r--r-- | internal/api/model/attachment.go | 8 |
5 files changed, 154 insertions, 35 deletions
diff --git a/internal/api/client/media/media.go b/internal/api/client/media/media.go index c9aee64ca..87cc2f091 100644 --- a/internal/api/client/media/media.go +++ b/internal/api/client/media/media.go @@ -26,20 +26,12 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/router" ) -// BasePathV1 is the base API path for making media requests through v1 of the api (for mastodon API compatibility) -const BasePathV1 = "/api/v1/media" - -// BasePathV2 is the base API path for making media requests through v2 of the api (for mastodon API compatibility) -const BasePathV2 = "/api/v2/media" - -// IDKey is the key for media attachment IDs -const IDKey = "id" - -// BasePathWithIDV1 corresponds to a media attachment with the given ID -const BasePathWithIDV1 = BasePathV1 + "/:" + IDKey - -// BasePathWithIDV2 corresponds to a media attachment with the given ID -const BasePathWithIDV2 = BasePathV2 + "/:" + IDKey +const ( + IDKey = "id" // IDKey is the key for media attachment IDs + APIVersionKey = "api_version" // APIVersionKey is the key for which version of the API to use (v1 or v2) + BasePathWithAPIVersion = "/api/:" + APIVersionKey + "/media" // BasePathWithAPIVersion is the base API path for making media requests through v1 or v2 of the api (for mastodon API compatibility) + BasePathWithIDV1 = "/api/v1/media/:" + IDKey // BasePathWithID corresponds to a media attachment with the given ID +) // Module implements the ClientAPIModule interface for media type Module struct { @@ -55,15 +47,8 @@ func New(processor processing.Processor) api.ClientModule { // Route satisfies the RESTAPIModule interface func (m *Module) Route(s router.Router) error { - // v1 handlers - s.AttachHandler(http.MethodPost, BasePathV1, m.MediaCreatePOSTHandler) + s.AttachHandler(http.MethodPost, BasePathWithAPIVersion, m.MediaCreatePOSTHandler) s.AttachHandler(http.MethodGet, BasePathWithIDV1, m.MediaGETHandler) s.AttachHandler(http.MethodPut, BasePathWithIDV1, m.MediaPUTHandler) - - // v2 handlers - s.AttachHandler(http.MethodPost, BasePathV2, m.MediaCreatePOSTHandler) - s.AttachHandler(http.MethodGet, BasePathWithIDV2, m.MediaGETHandler) - s.AttachHandler(http.MethodPut, BasePathWithIDV2, m.MediaPUTHandler) - return nil } diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go index f51d272b6..5a040b26c 100644 --- a/internal/api/client/media/mediacreate.go +++ b/internal/api/client/media/mediacreate.go @@ -31,7 +31,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -// MediaCreatePOSTHandler swagger:operation POST /api/v1/media mediaCreate +// MediaCreatePOSTHandler swagger:operation POST /api/{api_version}/media mediaCreate // // Upload a new media attachment. // @@ -46,6 +46,11 @@ import ( // - application/json // // parameters: +// - name: api version +// type: string +// in: path +// description: Version of the API to use. Must be one of v1 or v2. +// required: true // - name: description // in: formData // description: |- @@ -95,6 +100,13 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { return } + apiVersion := c.Param(APIVersionKey) + if apiVersion != "v1" && apiVersion != "v2" { + err := errors.New("api version must be one of v1 or v2") + api.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) + return + } + form := &model.AttachmentRequest{} if err := c.ShouldBind(&form); err != nil { api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) @@ -112,6 +124,15 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { return } + if apiVersion == "v2" { + // the mastodon v2 media API specifies that the URL should be null + // and that the client should call /api/v1/media/:id to get the URL + // + // so even though we have the URL already, remove it now to comply + // with the api + apiAttachment.URL = nil + } + c.JSON(http.StatusOK, apiAttachment) } diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index 595edb73d..8651fd982 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -30,6 +30,7 @@ import ( "net/http/httptest" "testing" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" mediamodule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -154,9 +155,15 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() { if err != nil { panic(err) } - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePathV1), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") + ctx.Params = gin.Params{ + gin.Param{ + Key: mediamodule.APIVersionKey, + Value: "v1", + }, + } // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) @@ -185,7 +192,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() { err = json.Unmarshal(b, attachmentReply) suite.NoError(err) - suite.Equal("this is a test image -- a cool background from somewhere", attachmentReply.Description) + suite.Equal("this is a test image -- a cool background from somewhere", *attachmentReply.Description) suite.Equal("image", attachmentReply.Type) suite.EqualValues(model.MediaMeta{ Original: model.MediaDimensions{ @@ -212,6 +219,100 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() { suite.Equal(len(storageKeysBeforeRequest)+2, len(storageKeysAfterRequest)) // 2 images should be added to storage: the original and the thumbnail } +func (suite *MediaCreateTestSuite) TestMediaCreateSuccessfulV2() { + // set up the context for the request + t := suite.testTokens["local_account_1"] + oauthToken := oauth.DBTokenToToken(t) + recorder := httptest.NewRecorder() + ctx, _ := testrig.CreateGinTestContext(recorder, nil) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + ctx.Set(oauth.SessionAuthorizedToken, oauthToken) + ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) + ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) + + // see what's in storage *before* the request + storageKeysBeforeRequest := []string{} + iter, err := suite.storage.KVStore.Iterator(nil) + if err != nil { + panic(err) + } + for iter.Next() { + storageKeysBeforeRequest = append(storageKeysBeforeRequest, iter.Key()) + } + iter.Release() + + // create the request + buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{ + "description": "this is a test image -- a cool background from somewhere", + "focus": "-0.5,0.5", + }) + if err != nil { + panic(err) + } + ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v2/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting + ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") + ctx.Params = gin.Params{ + gin.Param{ + Key: mediamodule.APIVersionKey, + Value: "v2", + }, + } + + // do the actual request + suite.mediaModule.MediaCreatePOSTHandler(ctx) + + // check what's in storage *after* the request + storageKeysAfterRequest := []string{} + iter, err = suite.storage.KVStore.Iterator(nil) + if err != nil { + panic(err) + } + for iter.Next() { + storageKeysAfterRequest = append(storageKeysAfterRequest, iter.Key()) + } + iter.Release() + + // check response + suite.EqualValues(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + suite.NoError(err) + fmt.Println(string(b)) + + attachmentReply := &model.Attachment{} + err = json.Unmarshal(b, attachmentReply) + suite.NoError(err) + + suite.Equal("this is a test image -- a cool background from somewhere", *attachmentReply.Description) + suite.Equal("image", attachmentReply.Type) + suite.EqualValues(model.MediaMeta{ + Original: model.MediaDimensions{ + Width: 1920, + Height: 1080, + Size: "1920x1080", + Aspect: 1.7777778, + }, + Small: model.MediaDimensions{ + Width: 512, + Height: 288, + Size: "512x288", + Aspect: 1.7777778, + }, + Focus: model.MediaFocus{ + X: -0.5, + Y: 0.5, + }, + }, attachmentReply.Meta) + suite.Equal("LjBzUo#6RQR._NvzRjWF?urqV@a$", attachmentReply.Blurhash) + suite.NotEmpty(attachmentReply.ID) + suite.Nil(attachmentReply.URL) + suite.NotEmpty(attachmentReply.PreviewURL) + suite.Equal(len(storageKeysBeforeRequest)+2, len(storageKeysAfterRequest)) // 2 images should be added to storage: the original and the thumbnail +} + func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() { // set up the context for the request t := suite.testTokens["local_account_1"] @@ -238,9 +339,15 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() { if err != nil { panic(err) } - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePathV1), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") + ctx.Params = gin.Params{ + gin.Param{ + Key: mediamodule.APIVersionKey, + Value: "v1", + }, + } // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) @@ -278,9 +385,15 @@ func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() { if err != nil { panic(err) } - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePathV1), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") + ctx.Params = gin.Params{ + gin.Param{ + Key: mediamodule.APIVersionKey, + Value: "v1", + }, + } // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go index 8081a5c15..607f4c31c 100644 --- a/internal/api/client/media/mediaupdate_test.go +++ b/internal/api/client/media/mediaupdate_test.go @@ -145,7 +145,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImage() { if err != nil { panic(err) } - ctx.Request = httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost:8080/%s/%s", mediamodule.BasePathV1, toUpdate.ID), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost:8080/api/v1/media/%s", toUpdate.ID), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") ctx.Params = gin.Params{ @@ -172,7 +172,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImage() { suite.NoError(err) // the reply should contain the updated fields - suite.Equal("new description!", attachmentReply.Description) + suite.Equal("new description!", *attachmentReply.Description) suite.EqualValues("gif", attachmentReply.Type) suite.EqualValues(model.MediaMeta{ Original: model.MediaDimensions{Width: 800, Height: 450, FrameRate: "", Duration: 0, Bitrate: 0, Size: "800x450", Aspect: 1.7777778}, @@ -181,7 +181,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImage() { }, attachmentReply.Meta) suite.Equal(toUpdate.Blurhash, attachmentReply.Blurhash) suite.Equal(toUpdate.ID, attachmentReply.ID) - suite.Equal(toUpdate.URL, attachmentReply.URL) + suite.Equal(toUpdate.URL, *attachmentReply.URL) suite.NotEmpty(toUpdate.Thumbnail.URL, attachmentReply.PreviewURL) } @@ -210,7 +210,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() { if err != nil { panic(err) } - ctx.Request = httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost:8080/%s/%s", mediamodule.BasePathV1, toUpdate.ID), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost:8080/api/v1/media/%s", toUpdate.ID), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") ctx.Params = gin.Params{ diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go index 3ab29d2fb..aafa554d8 100644 --- a/internal/api/model/attachment.go +++ b/internal/api/model/attachment.go @@ -68,7 +68,7 @@ type Attachment struct { Type string `json:"type"` // The location of the original full-size attachment. // example: https://example.org/fileserver/some_id/attachments/some_id/original/attachment.jpeg - URL string `json:"url"` + URL *string `json:"url"` // A shorter URL for the attachment. // In our case, we just give the URL again since we don't create smaller URLs. TextURL string `json:"text_url"` @@ -78,16 +78,16 @@ type Attachment struct { // The location of the full-size original attachment on the remote server. // Only defined for instances other than our own. // example: https://some-other-server.org/attachments/original/ahhhhh.jpeg - RemoteURL string `json:"remote_url"` + RemoteURL *string `json:"remote_url"` // The location of a scaled-down preview of the attachment on the remote server. // Only defined for instances other than our own. // example: https://some-other-server.org/attachments/small/ahhhhh.jpeg - PreviewRemoteURL string `json:"preview_remote_url"` + PreviewRemoteURL *string `json:"preview_remote_url"` // Metadata for this attachment. Meta MediaMeta `json:"meta,omitempty"` // Alt text that describes what is in the media attachment. // example: This is a picture of a kitten. - Description string `json:"description"` + Description *string `json:"description"` // A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet. // See https://github.com/woltapp/blurhash Blurhash string `json:"blurhash,omitempty"` |