gtsocial-umbx

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

update.go (10746B)


      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 account
     19 
     20 import (
     21 	"context"
     22 	"fmt"
     23 	"io"
     24 	"mime/multipart"
     25 
     26 	"github.com/superseriousbusiness/gotosocial/internal/ap"
     27 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
     28 	"github.com/superseriousbusiness/gotosocial/internal/config"
     29 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     30 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     31 	"github.com/superseriousbusiness/gotosocial/internal/log"
     32 	"github.com/superseriousbusiness/gotosocial/internal/media"
     33 	"github.com/superseriousbusiness/gotosocial/internal/messages"
     34 	"github.com/superseriousbusiness/gotosocial/internal/text"
     35 	"github.com/superseriousbusiness/gotosocial/internal/typeutils"
     36 	"github.com/superseriousbusiness/gotosocial/internal/validate"
     37 )
     38 
     39 func (p *Processor) selectNoteFormatter(contentType string) text.FormatFunc {
     40 	if contentType == "text/markdown" {
     41 		return p.formatter.FromMarkdown
     42 	}
     43 
     44 	return p.formatter.FromPlain
     45 }
     46 
     47 // Update processes the update of an account with the given form.
     48 func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
     49 	if form.Discoverable != nil {
     50 		account.Discoverable = form.Discoverable
     51 	}
     52 
     53 	if form.Bot != nil {
     54 		account.Bot = form.Bot
     55 	}
     56 
     57 	// Via the process of updating the account,
     58 	// it is possible that the emojis used by
     59 	// that account in note/display name/fields
     60 	// may change; we need to keep track of this.
     61 	var emojisChanged bool
     62 
     63 	if form.DisplayName != nil {
     64 		displayName := *form.DisplayName
     65 		if err := validate.DisplayName(displayName); err != nil {
     66 			return nil, gtserror.NewErrorBadRequest(err, err.Error())
     67 		}
     68 
     69 		// Parse new display name (always from plaintext).
     70 		account.DisplayName = text.SanitizePlaintext(displayName)
     71 
     72 		// If display name has changed, account emojis may have also changed.
     73 		emojisChanged = true
     74 	}
     75 
     76 	if form.Note != nil {
     77 		note := *form.Note
     78 		if err := validate.Note(note); err != nil {
     79 			return nil, gtserror.NewErrorBadRequest(err, err.Error())
     80 		}
     81 
     82 		// Store raw version of the note for now,
     83 		// we'll process the proper version later.
     84 		account.NoteRaw = note
     85 
     86 		// If note has changed, account emojis may have also changed.
     87 		emojisChanged = true
     88 	}
     89 
     90 	if form.FieldsAttributes != nil {
     91 		var (
     92 			fieldsAttributes = *form.FieldsAttributes
     93 			fieldsLen        = len(fieldsAttributes)
     94 			fieldsRaw        = make([]*gtsmodel.Field, 0, fieldsLen)
     95 		)
     96 
     97 		for _, updateField := range fieldsAttributes {
     98 			if updateField.Name == nil || updateField.Value == nil {
     99 				continue
    100 			}
    101 
    102 			var (
    103 				name  string = *updateField.Name
    104 				value string = *updateField.Value
    105 			)
    106 
    107 			if name == "" || value == "" {
    108 				continue
    109 			}
    110 
    111 			// Sanitize raw field values.
    112 			fieldRaw := &gtsmodel.Field{
    113 				Name:  text.SanitizePlaintext(name),
    114 				Value: text.SanitizePlaintext(value),
    115 			}
    116 			fieldsRaw = append(fieldsRaw, fieldRaw)
    117 		}
    118 
    119 		// Check length of parsed raw fields.
    120 		if err := validate.ProfileFields(fieldsRaw); err != nil {
    121 			return nil, gtserror.NewErrorBadRequest(err, err.Error())
    122 		}
    123 
    124 		// OK, new raw fields are valid.
    125 		account.FieldsRaw = fieldsRaw
    126 		account.Fields = make([]*gtsmodel.Field, 0, fieldsLen) // process these in a sec
    127 
    128 		// If fields have changed, account emojis may also have changed.
    129 		emojisChanged = true
    130 	}
    131 
    132 	if emojisChanged {
    133 		// Use map to deduplicate emojis by their ID.
    134 		emojis := make(map[string]*gtsmodel.Emoji)
    135 
    136 		// Retrieve display name emojis.
    137 		for _, emoji := range p.formatter.FromPlainEmojiOnly(
    138 			ctx,
    139 			p.parseMention,
    140 			account.ID,
    141 			"",
    142 			account.DisplayName,
    143 		).Emojis {
    144 			emojis[emoji.ID] = emoji
    145 		}
    146 
    147 		// Format + set note according to user prefs.
    148 		f := p.selectNoteFormatter(account.StatusContentType)
    149 		formatNoteResult := f(ctx, p.parseMention, account.ID, "", account.NoteRaw)
    150 		account.Note = formatNoteResult.HTML
    151 
    152 		// Retrieve note emojis.
    153 		for _, emoji := range formatNoteResult.Emojis {
    154 			emojis[emoji.ID] = emoji
    155 		}
    156 
    157 		// Process the raw fields we stored earlier.
    158 		account.Fields = make([]*gtsmodel.Field, 0, len(account.FieldsRaw))
    159 		for _, fieldRaw := range account.FieldsRaw {
    160 			field := &gtsmodel.Field{}
    161 
    162 			// Name stays plain, but we still need to
    163 			// see if there are any emojis set in it.
    164 			field.Name = fieldRaw.Name
    165 			for _, emoji := range p.formatter.FromPlainEmojiOnly(
    166 				ctx,
    167 				p.parseMention,
    168 				account.ID,
    169 				"",
    170 				fieldRaw.Name,
    171 			).Emojis {
    172 				emojis[emoji.ID] = emoji
    173 			}
    174 
    175 			// Value can be HTML, but we don't want
    176 			// to wrap the result in <p> tags.
    177 			fieldFormatValueResult := p.formatter.FromPlainNoParagraph(ctx, p.parseMention, account.ID, "", fieldRaw.Value)
    178 			field.Value = fieldFormatValueResult.HTML
    179 
    180 			// Retrieve field emojis.
    181 			for _, emoji := range fieldFormatValueResult.Emojis {
    182 				emojis[emoji.ID] = emoji
    183 			}
    184 
    185 			// We're done, append the shiny new field.
    186 			account.Fields = append(account.Fields, field)
    187 		}
    188 
    189 		emojisCount := len(emojis)
    190 		account.Emojis = make([]*gtsmodel.Emoji, 0, emojisCount)
    191 		account.EmojiIDs = make([]string, 0, emojisCount)
    192 
    193 		for id, emoji := range emojis {
    194 			account.Emojis = append(account.Emojis, emoji)
    195 			account.EmojiIDs = append(account.EmojiIDs, id)
    196 		}
    197 	}
    198 
    199 	if form.Avatar != nil && form.Avatar.Size != 0 {
    200 		avatarInfo, err := p.UpdateAvatar(ctx, form.Avatar, nil, account.ID)
    201 		if err != nil {
    202 			return nil, gtserror.NewErrorBadRequest(err)
    203 		}
    204 		account.AvatarMediaAttachmentID = avatarInfo.ID
    205 		account.AvatarMediaAttachment = avatarInfo
    206 		log.Tracef(ctx, "new avatar info for account %s is %+v", account.ID, avatarInfo)
    207 	}
    208 
    209 	if form.Header != nil && form.Header.Size != 0 {
    210 		headerInfo, err := p.UpdateHeader(ctx, form.Header, nil, account.ID)
    211 		if err != nil {
    212 			return nil, gtserror.NewErrorBadRequest(err)
    213 		}
    214 		account.HeaderMediaAttachmentID = headerInfo.ID
    215 		account.HeaderMediaAttachment = headerInfo
    216 		log.Tracef(ctx, "new header info for account %s is %+v", account.ID, headerInfo)
    217 	}
    218 
    219 	if form.Locked != nil {
    220 		account.Locked = form.Locked
    221 	}
    222 
    223 	if form.Source != nil {
    224 		if form.Source.Language != nil {
    225 			if err := validate.Language(*form.Source.Language); err != nil {
    226 				return nil, gtserror.NewErrorBadRequest(err)
    227 			}
    228 			account.Language = *form.Source.Language
    229 		}
    230 
    231 		if form.Source.Sensitive != nil {
    232 			account.Sensitive = form.Source.Sensitive
    233 		}
    234 
    235 		if form.Source.Privacy != nil {
    236 			if err := validate.Privacy(*form.Source.Privacy); err != nil {
    237 				return nil, gtserror.NewErrorBadRequest(err)
    238 			}
    239 			privacy := typeutils.APIVisToVis(apimodel.Visibility(*form.Source.Privacy))
    240 			account.Privacy = privacy
    241 		}
    242 
    243 		if form.Source.StatusContentType != nil {
    244 			if err := validate.StatusContentType(*form.Source.StatusContentType); err != nil {
    245 				return nil, gtserror.NewErrorBadRequest(err, err.Error())
    246 			}
    247 
    248 			account.StatusContentType = *form.Source.StatusContentType
    249 		}
    250 	}
    251 
    252 	if form.CustomCSS != nil {
    253 		customCSS := *form.CustomCSS
    254 		if err := validate.CustomCSS(customCSS); err != nil {
    255 			return nil, gtserror.NewErrorBadRequest(err, err.Error())
    256 		}
    257 		account.CustomCSS = text.SanitizePlaintext(customCSS)
    258 	}
    259 
    260 	if form.EnableRSS != nil {
    261 		account.EnableRSS = form.EnableRSS
    262 	}
    263 
    264 	err := p.state.DB.UpdateAccount(ctx, account)
    265 	if err != nil {
    266 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err))
    267 	}
    268 
    269 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
    270 		APObjectType:   ap.ObjectProfile,
    271 		APActivityType: ap.ActivityUpdate,
    272 		GTSModel:       account,
    273 		OriginAccount:  account,
    274 	})
    275 
    276 	acctSensitive, err := p.tc.AccountToAPIAccountSensitive(ctx, account)
    277 	if err != nil {
    278 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not convert account into apisensitive account: %s", err))
    279 	}
    280 	return acctSensitive, nil
    281 }
    282 
    283 // UpdateAvatar does the dirty work of checking the avatar part of an account update form,
    284 // parsing and checking the image, and doing the necessary updates in the database for this to become
    285 // the account's new avatar image.
    286 func (p *Processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) {
    287 	maxImageSize := config.GetMediaImageMaxSize()
    288 	if avatar.Size > int64(maxImageSize) {
    289 		return nil, fmt.Errorf("UpdateAvatar: avatar with size %d exceeded max image size of %d bytes", avatar.Size, maxImageSize)
    290 	}
    291 
    292 	dataFunc := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
    293 		f, err := avatar.Open()
    294 		return f, avatar.Size, err
    295 	}
    296 
    297 	isAvatar := true
    298 	ai := &media.AdditionalMediaInfo{
    299 		Avatar:      &isAvatar,
    300 		Description: description,
    301 	}
    302 
    303 	processingMedia, err := p.mediaManager.PreProcessMedia(ctx, dataFunc, accountID, ai)
    304 	if err != nil {
    305 		return nil, fmt.Errorf("UpdateAvatar: error processing avatar: %s", err)
    306 	}
    307 
    308 	return processingMedia.LoadAttachment(ctx)
    309 }
    310 
    311 // UpdateHeader does the dirty work of checking the header part of an account update form,
    312 // parsing and checking the image, and doing the necessary updates in the database for this to become
    313 // the account's new header image.
    314 func (p *Processor) UpdateHeader(ctx context.Context, header *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) {
    315 	maxImageSize := config.GetMediaImageMaxSize()
    316 	if header.Size > int64(maxImageSize) {
    317 		return nil, fmt.Errorf("UpdateHeader: header with size %d exceeded max image size of %d bytes", header.Size, maxImageSize)
    318 	}
    319 
    320 	dataFunc := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
    321 		f, err := header.Open()
    322 		return f, header.Size, err
    323 	}
    324 
    325 	isHeader := true
    326 	ai := &media.AdditionalMediaInfo{
    327 		Header: &isHeader,
    328 	}
    329 
    330 	processingMedia, err := p.mediaManager.PreProcessMedia(ctx, dataFunc, accountID, ai)
    331 	if err != nil {
    332 		return nil, fmt.Errorf("UpdateHeader: error processing header: %s", err)
    333 	}
    334 
    335 	return processingMedia.LoadAttachment(ctx)
    336 }