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 := >smodel.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 := >smodel.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 }