mediacreate.go (5363B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package media 19 20 import ( 21 "errors" 22 "fmt" 23 "net/http" 24 25 "github.com/gin-gonic/gin" 26 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 27 apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" 28 "github.com/superseriousbusiness/gotosocial/internal/config" 29 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 30 "github.com/superseriousbusiness/gotosocial/internal/oauth" 31 ) 32 33 // MediaCreatePOSTHandler swagger:operation POST /api/{api_version}/media mediaCreate 34 // 35 // Upload a new media attachment. 36 // 37 // --- 38 // tags: 39 // - media 40 // 41 // consumes: 42 // - multipart/form-data 43 // 44 // produces: 45 // - application/json 46 // 47 // parameters: 48 // - 49 // name: api_version 50 // type: string 51 // in: path 52 // description: Version of the API to use. Must be either `v1` or `v2`. 53 // required: true 54 // - 55 // name: description 56 // in: formData 57 // description: >- 58 // Image or media description to use as alt-text on the attachment. 59 // This is very useful for users of screenreaders! 60 // May or may not be required, depending on your instance settings. 61 // type: string 62 // - 63 // name: focus 64 // in: formData 65 // description: >- 66 // Focus of the media file. 67 // If present, it should be in the form of two comma-separated floats between -1 and 1. 68 // For example: `-0.5,0.25`. 69 // type: string 70 // default: "0,0" 71 // - 72 // name: file 73 // in: formData 74 // description: The media attachment to upload. 75 // type: file 76 // required: true 77 // 78 // security: 79 // - OAuth2 Bearer: 80 // - write:media 81 // 82 // responses: 83 // '200': 84 // description: The newly-created media attachment. 85 // schema: 86 // "$ref": "#/definitions/attachment" 87 // '400': 88 // description: bad request 89 // '401': 90 // description: unauthorized 91 // '422': 92 // description: unprocessable 93 // '500': 94 // description: internal server error 95 func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { 96 apiVersion := c.Param(APIVersionKey) 97 if apiVersion != APIv1 && apiVersion != APIv2 { 98 err := errors.New("api version must be one of v1 or v2 for this path") 99 apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGetV1) 100 return 101 } 102 103 authed, err := oauth.Authed(c, true, true, true, true) 104 if err != nil { 105 apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) 106 return 107 } 108 109 if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { 110 apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) 111 return 112 } 113 114 form := &apimodel.AttachmentRequest{} 115 if err := c.ShouldBind(&form); err != nil { 116 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) 117 return 118 } 119 120 if err := validateCreateMedia(form); err != nil { 121 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) 122 return 123 } 124 125 apiAttachment, errWithCode := m.processor.Media().Create(c.Request.Context(), authed.Account, form) 126 if errWithCode != nil { 127 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 128 return 129 } 130 131 if apiVersion == APIv2 { 132 // the mastodon v2 media API specifies that the URL should be null 133 // and that the client should call /api/v1/media/:id to get the URL 134 // 135 // so even though we have the URL already, remove it now to comply 136 // with the api 137 apiAttachment.URL = nil 138 } 139 140 c.JSON(http.StatusOK, apiAttachment) 141 } 142 143 func validateCreateMedia(form *apimodel.AttachmentRequest) error { 144 // check there actually is a file attached and it's not size 0 145 if form.File == nil { 146 return errors.New("no attachment given") 147 } 148 149 maxVideoSize := config.GetMediaVideoMaxSize() 150 maxImageSize := config.GetMediaImageMaxSize() 151 minDescriptionChars := config.GetMediaDescriptionMinChars() 152 maxDescriptionChars := config.GetMediaDescriptionMaxChars() 153 154 // a very superficial check to see if no size limits are exceeded 155 // we still don't actually know which media types we're dealing with but the other handlers will go into more detail there 156 maxSize := maxVideoSize 157 if maxImageSize > maxSize { 158 maxSize = maxImageSize 159 } 160 161 if form.File.Size > int64(maxSize) { 162 return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size) 163 } 164 165 if length := len([]rune(form.Description)); length > maxDescriptionChars { 166 return fmt.Errorf("image description length must be between %d and %d characters (inclusive), but provided image description was %d chars", minDescriptionChars, maxDescriptionChars, length) 167 } 168 169 return nil 170 }