statuscreate.go (5420B)
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 statuses 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 "github.com/superseriousbusiness/gotosocial/internal/validate" 32 ) 33 34 // StatusCreatePOSTHandler swagger:operation POST /api/v1/statuses statusCreate 35 // 36 // Create a new status. 37 // 38 // The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'. 39 // The parameters can also be given in the body of the request, as XML, if the content-type is set to 'application/xml'. 40 // 41 // --- 42 // tags: 43 // - statuses 44 // 45 // consumes: 46 // - application/json 47 // - application/xml 48 // - application/x-www-form-urlencoded 49 // 50 // produces: 51 // - application/json 52 // 53 // security: 54 // - OAuth2 Bearer: 55 // - write:statuses 56 // 57 // responses: 58 // '200': 59 // description: "The newly created status." 60 // schema: 61 // "$ref": "#/definitions/status" 62 // '400': 63 // description: bad request 64 // '401': 65 // description: unauthorized 66 // '403': 67 // description: forbidden 68 // '404': 69 // description: not found 70 // '406': 71 // description: not acceptable 72 // '500': 73 // description: internal server error 74 func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { 75 authed, err := oauth.Authed(c, true, true, true, true) 76 if err != nil { 77 apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) 78 return 79 } 80 81 if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { 82 apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) 83 return 84 } 85 86 form := &apimodel.AdvancedStatusCreateForm{} 87 if err := c.ShouldBind(form); err != nil { 88 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) 89 return 90 } 91 92 // DO NOT COMMIT THIS UNCOMMENTED, IT WILL CAUSE MASS CHAOS. 93 // this is being left in as an ode to kim's shitposting. 94 // 95 // user := authed.Account.DisplayName 96 // if user == "" { 97 // user = authed.Account.Username 98 // } 99 // form.Status += "\n\nsent from " + user + "'s iphone\n" 100 101 if err := validateCreateStatus(form); err != nil { 102 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) 103 return 104 } 105 106 apiStatus, errWithCode := m.processor.Status().Create(c.Request.Context(), authed.Account, authed.Application, form) 107 if errWithCode != nil { 108 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 109 return 110 } 111 112 c.JSON(http.StatusOK, apiStatus) 113 } 114 115 func validateCreateStatus(form *apimodel.AdvancedStatusCreateForm) error { 116 hasStatus := form.Status != "" 117 hasMedia := len(form.MediaIDs) != 0 118 hasPoll := form.Poll != nil 119 120 if !hasStatus && !hasMedia && !hasPoll { 121 return errors.New("no status, media, or poll provided") 122 } 123 124 if hasMedia && hasPoll { 125 return errors.New("can't post media + poll in same status") 126 } 127 128 maxChars := config.GetStatusesMaxChars() 129 maxMediaFiles := config.GetStatusesMediaMaxFiles() 130 maxPollOptions := config.GetStatusesPollMaxOptions() 131 maxPollChars := config.GetStatusesPollOptionMaxChars() 132 maxCwChars := config.GetStatusesCWMaxChars() 133 134 if form.Status != "" { 135 if length := len([]rune(form.Status)); length > maxChars { 136 return fmt.Errorf("status too long, %d characters provided but limit is %d", length, maxChars) 137 } 138 } 139 140 if len(form.MediaIDs) > maxMediaFiles { 141 return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles) 142 } 143 144 if form.Poll != nil { 145 if form.Poll.Options == nil { 146 return errors.New("poll with no options") 147 } 148 if len(form.Poll.Options) > maxPollOptions { 149 return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), maxPollOptions) 150 } 151 for _, p := range form.Poll.Options { 152 if length := len([]rune(p)); length > maxPollChars { 153 return fmt.Errorf("poll option too long, %d characters provided but limit is %d", length, maxPollChars) 154 } 155 } 156 } 157 158 if form.SpoilerText != "" { 159 if length := len([]rune(form.SpoilerText)); length > maxCwChars { 160 return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", length, maxCwChars) 161 } 162 } 163 164 if form.Language != "" { 165 if err := validate.Language(form.Language); err != nil { 166 return err 167 } 168 } 169 170 return nil 171 }