2023-03-12 15:00:57 +00:00
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2021-05-08 12:25:55 +00:00
2023-01-02 12:10:50 +00:00
package accounts
2021-05-08 12:25:55 +00:00
import (
2022-06-08 18:38:03 +00:00
"errors"
2021-09-11 11:19:06 +00:00
"fmt"
2021-05-08 12:25:55 +00:00
"net/http"
2024-01-17 14:54:30 +00:00
"slices"
2021-09-11 11:19:06 +00:00
"strconv"
2021-05-08 12:25:55 +00:00
"github.com/gin-gonic/gin"
2023-05-12 09:17:31 +00:00
"github.com/gin-gonic/gin/binding"
2023-03-06 09:30:19 +00:00
"github.com/go-playground/form/v4"
2023-01-02 12:10:50 +00:00
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
2022-06-08 18:38:03 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2021-05-08 12:25:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
2021-08-02 17:06:44 +00:00
// AccountUpdateCredentialsPATCHHandler swagger:operation PATCH /api/v1/accounts/update_credentials accountUpdate
2021-07-31 15:49:59 +00:00
//
// Update your account.
//
2022-09-28 17:30:40 +00:00
// ---
// tags:
// - accounts
2021-07-31 15:49:59 +00:00
//
2022-09-28 17:30:40 +00:00
// consumes:
// - multipart/form-data
2023-06-16 09:16:04 +00:00
// - application/x-www-form-urlencoded
2023-05-12 09:17:31 +00:00
// - application/json
2021-07-31 15:49:59 +00:00
//
2022-09-28 17:30:40 +00:00
// produces:
// - application/json
2021-07-31 15:49:59 +00:00
//
2022-09-28 17:30:40 +00:00
// parameters:
// -
// name: discoverable
// in: formData
// description: Account should be made discoverable and shown in the profile directory (if enabled).
// type: boolean
// -
// name: bot
// in: formData
// description: Account is flagged as a bot.
// type: boolean
// -
// name: display_name
// in: formData
// description: The display name to use for the account.
// type: string
// allowEmptyValue: true
// -
// name: note
// in: formData
// description: Bio/description of this account.
// type: string
// allowEmptyValue: true
// -
// name: avatar
// in: formData
// description: Avatar of the user.
// type: file
// -
// name: header
// in: formData
// description: Header of the user.
// type: file
// -
// name: locked
// in: formData
// description: Require manual approval of follow requests.
// type: boolean
// -
// name: source[privacy]
// in: formData
// description: Default post privacy for authored statuses.
// type: string
// -
// name: source[sensitive]
// in: formData
// description: Mark authored statuses as sensitive by default.
// type: boolean
// -
// name: source[language]
// in: formData
// description: Default language to use for authored statuses (ISO 6391).
// type: string
// -
2023-03-02 11:06:40 +00:00
// name: source[status_content_type]
2022-09-28 17:30:40 +00:00
// in: formData
2023-03-02 11:06:40 +00:00
// description: Default content type to use for authored statuses (text/plain or text/markdown).
2022-09-28 17:30:40 +00:00
// type: string
// -
// name: custom_css
// in: formData
// description: >-
// Custom CSS to use when rendering this account's profile or statuses.
// String must be no more than 5,000 characters (~5kb).
// type: string
2022-10-08 12:00:39 +00:00
// -
// name: enable_rss
// in: formData
// description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss`
// type: boolean
2023-03-06 09:30:19 +00:00
// -
2024-03-06 17:05:45 +00:00
// name: fields_attributes[0][name]
2023-03-06 09:30:19 +00:00
// in: formData
2024-03-06 17:05:45 +00:00
// description: Name of 1st profile field to be added to this account's profile.
// (The index may be any string; add more indexes to send more fields.)
// type: string
// -
// name: fields_attributes[0][value]
// in: formData
// description: Value of 1st profile field to be added to this account's profile.
// (The index may be any string; add more indexes to send more fields.)
// type: string
// -
// name: fields_attributes[1][name]
// in: formData
// description: Name of 2nd profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[1][value]
// in: formData
// description: Value of 2nd profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[2][name]
// in: formData
// description: Name of 3rd profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[2][value]
// in: formData
// description: Value of 3rd profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[3][name]
// in: formData
// description: Name of 4th profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[3][value]
// in: formData
// description: Value of 4th profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[4][name]
// in: formData
// description: Name of 5th profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[4][value]
// in: formData
// description: Value of 5th profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[5][name]
// in: formData
// description: Name of 6th profile field to be added to this account's profile.
// type: string
// -
// name: fields_attributes[5][value]
// in: formData
// description: Value of 6th profile field to be added to this account's profile.
// type: string
2021-07-31 15:49:59 +00:00
//
2022-09-28 17:30:40 +00:00
// security:
// - OAuth2 Bearer:
// - write:accounts
2021-07-31 15:49:59 +00:00
//
2022-09-28 17:30:40 +00:00
// responses:
// '200':
// description: "The newly updated account."
// schema:
// "$ref": "#/definitions/account"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
2021-05-08 12:25:55 +00:00
func ( m * Module ) AccountUpdateCredentialsPATCHHandler ( c * gin . Context ) {
2021-07-31 15:49:59 +00:00
authed , err := oauth . Authed ( c , true , true , true , true )
2021-05-08 12:25:55 +00:00
if err != nil {
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorUnauthorized ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
2021-05-08 12:25:55 +00:00
return
}
2023-01-02 12:10:50 +00:00
if _ , err := apiutil . NegotiateAccept ( c , apiutil . JSONAcceptHeaders ... ) ; err != nil {
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorNotAcceptable ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
2021-12-11 16:50:00 +00:00
return
}
2021-09-11 11:19:06 +00:00
form , err := parseUpdateAccountForm ( c )
if err != nil {
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
2021-05-08 12:25:55 +00:00
return
}
2023-02-22 15:05:26 +00:00
acctSensitive , errWithCode := m . processor . Account ( ) . Update ( c . Request . Context ( ) , authed . Account , form )
2022-06-08 18:38:03 +00:00
if errWithCode != nil {
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGetV1 )
2021-05-08 12:25:55 +00:00
return
}
2023-11-27 14:00:57 +00:00
apiutil . JSON ( c , http . StatusOK , acctSensitive )
2021-05-08 12:25:55 +00:00
}
2021-09-11 11:19:06 +00:00
2023-05-12 09:17:31 +00:00
// fieldsAttributesFormBinding satisfies gin's binding.Binding interface.
// Should only be used specifically for multipart/form-data MIME type.
type fieldsAttributesFormBinding struct { }
2023-03-06 09:30:19 +00:00
2023-05-12 09:17:31 +00:00
func ( fieldsAttributesFormBinding ) Name ( ) string {
return "FieldsAttributes"
2023-03-06 09:30:19 +00:00
}
2023-05-12 09:17:31 +00:00
func ( fieldsAttributesFormBinding ) Bind ( req * http . Request , obj any ) error {
2023-03-06 09:30:19 +00:00
if err := req . ParseForm ( ) ; err != nil {
return err
}
2023-05-12 09:17:31 +00:00
// Change default namespace prefix and suffix to
// allow correct parsing of the field attributes.
2023-03-06 09:30:19 +00:00
decoder := form . NewDecoder ( )
decoder . SetNamespacePrefix ( "[" )
decoder . SetNamespaceSuffix ( "]" )
2023-05-12 09:17:31 +00:00
return decoder . Decode ( obj , req . Form )
2023-03-06 09:30:19 +00:00
}
2023-01-02 12:10:50 +00:00
func parseUpdateAccountForm ( c * gin . Context ) ( * apimodel . UpdateCredentialsRequest , error ) {
form := & apimodel . UpdateCredentialsRequest {
Source : & apimodel . UpdateSource { } ,
2021-09-11 11:19:06 +00:00
}
2022-06-08 18:38:03 +00:00
2023-05-12 09:17:31 +00:00
switch ct := c . ContentType ( ) ; ct {
case binding . MIMEJSON :
// Bind with default json binding first.
if err := c . ShouldBindWith ( form , binding . JSON ) ; err != nil {
return nil , err
}
2021-09-11 11:19:06 +00:00
2023-05-12 09:17:31 +00:00
// Now use custom form binding for
// field attributes in the json data.
var err error
form . FieldsAttributes , err = parseFieldsAttributesFromJSON ( form . JSONFieldsAttributes )
2021-09-11 11:19:06 +00:00
if err != nil {
2023-05-12 09:17:31 +00:00
return nil , fmt . Errorf ( "custom json binding failed: %w" , err )
}
2023-06-16 09:16:04 +00:00
case binding . MIMEPOSTForm :
// Bind with default form binding first.
if err := c . ShouldBindWith ( form , binding . FormPost ) ; err != nil {
return nil , err
}
// Now use custom form binding for
// field attributes in the form data.
if err := c . ShouldBindWith ( form , fieldsAttributesFormBinding { } ) ; err != nil {
return nil , fmt . Errorf ( "custom form binding failed: %w" , err )
}
2023-05-12 09:17:31 +00:00
case binding . MIMEMultipartPOSTForm :
// Bind with default form binding first.
if err := c . ShouldBindWith ( form , binding . FormMultipart ) ; err != nil {
return nil , err
2021-09-11 11:19:06 +00:00
}
2023-05-12 09:17:31 +00:00
// Now use custom form binding for
// field attributes in the form data.
if err := c . ShouldBindWith ( form , fieldsAttributesFormBinding { } ) ; err != nil {
return nil , fmt . Errorf ( "custom form binding failed: %w" , err )
}
default :
2023-06-16 09:16:04 +00:00
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 )
2023-05-12 09:17:31 +00:00
return nil , err
2022-08-06 10:09:21 +00:00
}
2022-08-05 10:30:47 +00:00
if form == nil ||
( form . Discoverable == nil &&
form . Bot == nil &&
form . DisplayName == nil &&
form . Note == nil &&
form . Avatar == nil &&
form . Header == nil &&
form . Locked == nil &&
form . Source . Privacy == nil &&
form . Source . Sensitive == nil &&
form . Source . Language == nil &&
2023-03-02 11:06:40 +00:00
form . Source . StatusContentType == nil &&
2022-09-12 11:14:29 +00:00
form . FieldsAttributes == nil &&
2022-10-08 12:00:39 +00:00
form . CustomCSS == nil &&
form . EnableRSS == nil ) {
2022-08-05 10:30:47 +00:00
return nil , errors . New ( "empty form submitted" )
}
2021-09-11 11:19:06 +00:00
return form , nil
}
2023-05-12 09:17:31 +00:00
func parseFieldsAttributesFromJSON ( jsonFieldsAttributes * map [ string ] apimodel . UpdateField ) ( * [ ] apimodel . UpdateField , error ) {
if jsonFieldsAttributes == nil {
// Nothing set, nothing to do.
return nil , nil
}
fieldsAttributes := make ( [ ] apimodel . UpdateField , 0 , len ( * jsonFieldsAttributes ) )
for keyStr , updateField := range * jsonFieldsAttributes {
key , err := strconv . Atoi ( keyStr )
if err != nil {
return nil , fmt . Errorf ( "couldn't parse fieldAttributes key %s to int: %w" , keyStr , err )
}
fieldsAttributes = append ( fieldsAttributes , apimodel . UpdateField {
Key : key ,
Name : updateField . Name ,
Value : updateField . Value ,
} )
}
// Sort slice by the key each field was submitted with.
2024-01-17 14:54:30 +00:00
slices . SortFunc ( fieldsAttributes , func ( a , b apimodel . UpdateField ) int {
const k = + 1
switch {
case a . Key > b . Key :
return + k
case a . Key < b . Key :
return - k
default :
return 0
}
2023-05-12 09:17:31 +00:00
} )
return & fieldsAttributes , nil
}