gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

accountupdate.go (8449B)


      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 accounts
     19 
     20 import (
     21 	"errors"
     22 	"fmt"
     23 	"net/http"
     24 	"strconv"
     25 
     26 	"github.com/gin-gonic/gin"
     27 	"github.com/gin-gonic/gin/binding"
     28 	"github.com/go-playground/form/v4"
     29 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
     30 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
     31 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     32 	"github.com/superseriousbusiness/gotosocial/internal/oauth"
     33 	"golang.org/x/exp/slices"
     34 )
     35 
     36 // AccountUpdateCredentialsPATCHHandler swagger:operation PATCH /api/v1/accounts/update_credentials accountUpdate
     37 //
     38 // Update your account.
     39 //
     40 //	---
     41 //	tags:
     42 //	- accounts
     43 //
     44 //	consumes:
     45 //	- multipart/form-data
     46 //	- application/x-www-form-urlencoded
     47 //	- application/json
     48 //
     49 //	produces:
     50 //	- application/json
     51 //
     52 //	parameters:
     53 //	-
     54 //		name: discoverable
     55 //		in: formData
     56 //		description: Account should be made discoverable and shown in the profile directory (if enabled).
     57 //		type: boolean
     58 //	-
     59 //		name: bot
     60 //		in: formData
     61 //		description: Account is flagged as a bot.
     62 //		type: boolean
     63 //	-
     64 //		name: display_name
     65 //		in: formData
     66 //		description: The display name to use for the account.
     67 //		type: string
     68 //		allowEmptyValue: true
     69 //	-
     70 //		name: note
     71 //		in: formData
     72 //		description: Bio/description of this account.
     73 //		type: string
     74 //		allowEmptyValue: true
     75 //	-
     76 //		name: avatar
     77 //		in: formData
     78 //		description: Avatar of the user.
     79 //		type: file
     80 //	-
     81 //		name: header
     82 //		in: formData
     83 //		description: Header of the user.
     84 //		type: file
     85 //	-
     86 //		name: locked
     87 //		in: formData
     88 //		description: Require manual approval of follow requests.
     89 //		type: boolean
     90 //	-
     91 //		name: source[privacy]
     92 //		in: formData
     93 //		description: Default post privacy for authored statuses.
     94 //		type: string
     95 //	-
     96 //		name: source[sensitive]
     97 //		in: formData
     98 //		description: Mark authored statuses as sensitive by default.
     99 //		type: boolean
    100 //	-
    101 //		name: source[language]
    102 //		in: formData
    103 //		description: Default language to use for authored statuses (ISO 6391).
    104 //		type: string
    105 //	-
    106 //		name: source[status_content_type]
    107 //		in: formData
    108 //		description: Default content type to use for authored statuses (text/plain or text/markdown).
    109 //		type: string
    110 //	-
    111 //		name: custom_css
    112 //		in: formData
    113 //		description: >-
    114 //			Custom CSS to use when rendering this account's profile or statuses.
    115 //			String must be no more than 5,000 characters (~5kb).
    116 //		type: string
    117 //	-
    118 //		name: enable_rss
    119 //		in: formData
    120 //		description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss`
    121 //		type: boolean
    122 //	-
    123 //		name: fields_attributes
    124 //		in: formData
    125 //		description: Profile fields to be added to this account's profile
    126 //		type: array
    127 //		items:
    128 //			type: object
    129 //
    130 //	security:
    131 //	- OAuth2 Bearer:
    132 //		- write:accounts
    133 //
    134 //	responses:
    135 //		'200':
    136 //			description: "The newly updated account."
    137 //			schema:
    138 //				"$ref": "#/definitions/account"
    139 //		'400':
    140 //			description: bad request
    141 //		'401':
    142 //			description: unauthorized
    143 //		'404':
    144 //			description: not found
    145 //		'406':
    146 //			description: not acceptable
    147 //		'500':
    148 //			description: internal server error
    149 func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
    150 	authed, err := oauth.Authed(c, true, true, true, true)
    151 	if err != nil {
    152 		apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
    153 		return
    154 	}
    155 
    156 	if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
    157 		apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
    158 		return
    159 	}
    160 
    161 	form, err := parseUpdateAccountForm(c)
    162 	if err != nil {
    163 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
    164 		return
    165 	}
    166 
    167 	acctSensitive, errWithCode := m.processor.Account().Update(c.Request.Context(), authed.Account, form)
    168 	if errWithCode != nil {
    169 		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    170 		return
    171 	}
    172 
    173 	c.JSON(http.StatusOK, acctSensitive)
    174 }
    175 
    176 // fieldsAttributesFormBinding satisfies gin's binding.Binding interface.
    177 // Should only be used specifically for multipart/form-data MIME type.
    178 type fieldsAttributesFormBinding struct{}
    179 
    180 func (fieldsAttributesFormBinding) Name() string {
    181 	return "FieldsAttributes"
    182 }
    183 
    184 func (fieldsAttributesFormBinding) Bind(req *http.Request, obj any) error {
    185 	if err := req.ParseForm(); err != nil {
    186 		return err
    187 	}
    188 
    189 	// Change default namespace prefix and suffix to
    190 	// allow correct parsing of the field attributes.
    191 	decoder := form.NewDecoder()
    192 	decoder.SetNamespacePrefix("[")
    193 	decoder.SetNamespaceSuffix("]")
    194 
    195 	return decoder.Decode(obj, req.Form)
    196 }
    197 
    198 func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, error) {
    199 	form := &apimodel.UpdateCredentialsRequest{
    200 		Source: &apimodel.UpdateSource{},
    201 	}
    202 
    203 	switch ct := c.ContentType(); ct {
    204 	case binding.MIMEJSON:
    205 		// Bind with default json binding first.
    206 		if err := c.ShouldBindWith(form, binding.JSON); err != nil {
    207 			return nil, err
    208 		}
    209 
    210 		// Now use custom form binding for
    211 		// field attributes in the json data.
    212 		var err error
    213 		form.FieldsAttributes, err = parseFieldsAttributesFromJSON(form.JSONFieldsAttributes)
    214 		if err != nil {
    215 			return nil, fmt.Errorf("custom json binding failed: %w", err)
    216 		}
    217 	case binding.MIMEPOSTForm:
    218 		// Bind with default form binding first.
    219 		if err := c.ShouldBindWith(form, binding.FormPost); err != nil {
    220 			return nil, err
    221 		}
    222 
    223 		// Now use custom form binding for
    224 		// field attributes in the form data.
    225 		if err := c.ShouldBindWith(form, fieldsAttributesFormBinding{}); err != nil {
    226 			return nil, fmt.Errorf("custom form binding failed: %w", err)
    227 		}
    228 	case binding.MIMEMultipartPOSTForm:
    229 		// Bind with default form binding first.
    230 		if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil {
    231 			return nil, err
    232 		}
    233 
    234 		// Now use custom form binding for
    235 		// field attributes in the form data.
    236 		if err := c.ShouldBindWith(form, fieldsAttributesFormBinding{}); err != nil {
    237 			return nil, fmt.Errorf("custom form binding failed: %w", err)
    238 		}
    239 	default:
    240 		err := fmt.Errorf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s", ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm)
    241 		return nil, err
    242 	}
    243 
    244 	if form == nil ||
    245 		(form.Discoverable == nil &&
    246 			form.Bot == nil &&
    247 			form.DisplayName == nil &&
    248 			form.Note == nil &&
    249 			form.Avatar == nil &&
    250 			form.Header == nil &&
    251 			form.Locked == nil &&
    252 			form.Source.Privacy == nil &&
    253 			form.Source.Sensitive == nil &&
    254 			form.Source.Language == nil &&
    255 			form.Source.StatusContentType == nil &&
    256 			form.FieldsAttributes == nil &&
    257 			form.CustomCSS == nil &&
    258 			form.EnableRSS == nil) {
    259 		return nil, errors.New("empty form submitted")
    260 	}
    261 
    262 	return form, nil
    263 }
    264 
    265 func parseFieldsAttributesFromJSON(jsonFieldsAttributes *map[string]apimodel.UpdateField) (*[]apimodel.UpdateField, error) {
    266 	if jsonFieldsAttributes == nil {
    267 		// Nothing set, nothing to do.
    268 		return nil, nil
    269 	}
    270 
    271 	fieldsAttributes := make([]apimodel.UpdateField, 0, len(*jsonFieldsAttributes))
    272 	for keyStr, updateField := range *jsonFieldsAttributes {
    273 		key, err := strconv.Atoi(keyStr)
    274 		if err != nil {
    275 			return nil, fmt.Errorf("couldn't parse fieldAttributes key %s to int: %w", keyStr, err)
    276 		}
    277 
    278 		fieldsAttributes = append(fieldsAttributes, apimodel.UpdateField{
    279 			Key:   key,
    280 			Name:  updateField.Name,
    281 			Value: updateField.Value,
    282 		})
    283 	}
    284 
    285 	// Sort slice by the key each field was submitted with.
    286 	slices.SortFunc(fieldsAttributes, func(a, b apimodel.UpdateField) bool {
    287 		return a.Key < b.Key
    288 	})
    289 
    290 	return &fieldsAttributes, nil
    291 }