instance.go (11111B)
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 processing 19 20 import ( 21 "context" 22 "fmt" 23 "sort" 24 25 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 26 "github.com/superseriousbusiness/gotosocial/internal/config" 27 "github.com/superseriousbusiness/gotosocial/internal/db" 28 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 29 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 30 "github.com/superseriousbusiness/gotosocial/internal/log" 31 "github.com/superseriousbusiness/gotosocial/internal/text" 32 "github.com/superseriousbusiness/gotosocial/internal/util" 33 "github.com/superseriousbusiness/gotosocial/internal/validate" 34 ) 35 36 func (p *Processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) { 37 i := >smodel.Instance{} 38 if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "domain", Value: config.GetHost()}}, i); err != nil { 39 return nil, err 40 } 41 return i, nil 42 } 43 44 func (p *Processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { 45 i, err := p.getThisInstance(ctx) 46 if err != nil { 47 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err)) 48 } 49 50 ai, err := p.tc.InstanceToAPIV1Instance(ctx, i) 51 if err != nil { 52 return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) 53 } 54 55 return ai, nil 56 } 57 58 func (p *Processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gtserror.WithCode) { 59 i, err := p.getThisInstance(ctx) 60 if err != nil { 61 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err)) 62 } 63 64 ai, err := p.tc.InstanceToAPIV2Instance(ctx, i) 65 if err != nil { 66 return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) 67 } 68 69 return ai, nil 70 } 71 72 func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) { 73 domains := []*apimodel.Domain{} 74 75 if includeOpen { 76 instances, err := p.state.DB.GetInstancePeers(ctx, false) 77 if err != nil && err != db.ErrNoEntries { 78 err = fmt.Errorf("error selecting instance peers: %s", err) 79 return nil, gtserror.NewErrorInternalError(err) 80 } 81 82 for _, i := range instances { 83 // Domain may be in Punycode, 84 // de-punify it just in case. 85 d, err := util.DePunify(i.Domain) 86 if err != nil { 87 log.Errorf(ctx, "couldn't depunify domain %s: %s", i.Domain, err) 88 continue 89 } 90 91 domains = append(domains, &apimodel.Domain{Domain: d}) 92 } 93 } 94 95 if includeSuspended { 96 domainBlocks := []*gtsmodel.DomainBlock{} 97 if err := p.state.DB.GetAll(ctx, &domainBlocks); err != nil && err != db.ErrNoEntries { 98 return nil, gtserror.NewErrorInternalError(err) 99 } 100 101 for _, domainBlock := range domainBlocks { 102 // Domain may be in Punycode, 103 // de-punify it just in case. 104 d, err := util.DePunify(domainBlock.Domain) 105 if err != nil { 106 log.Errorf(ctx, "couldn't depunify domain %s: %s", domainBlock.Domain, err) 107 continue 108 } 109 110 if *domainBlock.Obfuscate { 111 // Obfuscate the de-punified version. 112 d = obfuscate(d) 113 } 114 115 domains = append(domains, &apimodel.Domain{ 116 Domain: d, 117 SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt), 118 PublicComment: domainBlock.PublicComment, 119 }) 120 } 121 } 122 123 sort.Slice(domains, func(i, j int) bool { 124 return domains[i].Domain < domains[j].Domain 125 }) 126 127 if flat { 128 flattened := []string{} 129 for _, d := range domains { 130 flattened = append(flattened, d.Domain) 131 } 132 return flattened, nil 133 } 134 135 return domains, nil 136 } 137 138 func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) { 139 // fetch the instance entry from the db for processing 140 i := >smodel.Instance{} 141 host := config.GetHost() 142 if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, i); err != nil { 143 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", host, err)) 144 } 145 146 // fetch the instance account from the db for processing 147 ia, err := p.state.DB.GetInstanceAccount(ctx, "") 148 if err != nil { 149 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance account %s: %s", host, err)) 150 } 151 152 updatingColumns := []string{} 153 154 // validate & update site title if it's set on the form 155 if form.Title != nil { 156 if err := validate.SiteTitle(*form.Title); err != nil { 157 return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("site title invalid: %s", err)) 158 } 159 updatingColumns = append(updatingColumns, "title") 160 i.Title = text.SanitizePlaintext(*form.Title) // don't allow html in site title 161 } 162 163 // validate & update site contact account if it's set on the form 164 if form.ContactUsername != nil { 165 // make sure the account with the given username exists in the db 166 contactAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, *form.ContactUsername, "") 167 if err != nil { 168 return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("account with username %s not retrievable", *form.ContactUsername)) 169 } 170 // make sure it has a user associated with it 171 contactUser, err := p.state.DB.GetUserByAccountID(ctx, contactAccount.ID) 172 if err != nil { 173 return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("user for account with username %s not retrievable", *form.ContactUsername)) 174 } 175 // suspended accounts cannot be contact accounts 176 if !contactAccount.SuspendedAt.IsZero() { 177 err := fmt.Errorf("selected contact account %s is suspended", contactAccount.Username) 178 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 179 } 180 // unconfirmed or unapproved users cannot be contacts 181 if contactUser.ConfirmedAt.IsZero() { 182 err := fmt.Errorf("user of selected contact account %s is not confirmed", contactAccount.Username) 183 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 184 } 185 if !*contactUser.Approved { 186 err := fmt.Errorf("user of selected contact account %s is not approved", contactAccount.Username) 187 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 188 } 189 // contact account user must be admin or moderator otherwise what's the point of contacting them 190 if !*contactUser.Admin && !*contactUser.Moderator { 191 err := fmt.Errorf("user of selected contact account %s is neither admin nor moderator", contactAccount.Username) 192 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 193 } 194 updatingColumns = append(updatingColumns, "contact_account_id") 195 i.ContactAccountID = contactAccount.ID 196 } 197 198 // validate & update site contact email if it's set on the form 199 if form.ContactEmail != nil { 200 contactEmail := *form.ContactEmail 201 if contactEmail != "" { 202 if err := validate.Email(contactEmail); err != nil { 203 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 204 } 205 } 206 updatingColumns = append(updatingColumns, "contact_email") 207 i.ContactEmail = contactEmail 208 } 209 210 // validate & update site short description if it's set on the form 211 if form.ShortDescription != nil { 212 if err := validate.SiteShortDescription(*form.ShortDescription); err != nil { 213 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 214 } 215 updatingColumns = append(updatingColumns, "short_description") 216 i.ShortDescription = text.SanitizeHTML(*form.ShortDescription) // html is OK in site description, but we should sanitize it 217 } 218 219 // validate & update site description if it's set on the form 220 if form.Description != nil { 221 if err := validate.SiteDescription(*form.Description); err != nil { 222 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 223 } 224 updatingColumns = append(updatingColumns, "description") 225 i.Description = text.SanitizeHTML(*form.Description) // html is OK in site description, but we should sanitize it 226 } 227 228 // validate & update site terms if it's set on the form 229 if form.Terms != nil { 230 if err := validate.SiteTerms(*form.Terms); err != nil { 231 return nil, gtserror.NewErrorBadRequest(err, err.Error()) 232 } 233 updatingColumns = append(updatingColumns, "terms") 234 i.Terms = text.SanitizeHTML(*form.Terms) // html is OK in site terms, but we should sanitize it 235 } 236 237 var updateInstanceAccount bool 238 239 if form.Avatar != nil && form.Avatar.Size != 0 { 240 // process instance avatar image + description 241 avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, ia.ID) 242 if err != nil { 243 return nil, gtserror.NewErrorBadRequest(err, "error processing avatar") 244 } 245 ia.AvatarMediaAttachmentID = avatarInfo.ID 246 ia.AvatarMediaAttachment = avatarInfo 247 updateInstanceAccount = true 248 } else if form.AvatarDescription != nil && ia.AvatarMediaAttachment != nil { 249 // process just the description for the existing avatar 250 ia.AvatarMediaAttachment.Description = *form.AvatarDescription 251 if err := p.state.DB.UpdateAttachment(ctx, ia.AvatarMediaAttachment, "description"); err != nil { 252 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance avatar description: %s", err)) 253 } 254 } 255 256 if form.Header != nil && form.Header.Size != 0 { 257 // process instance header image 258 headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, ia.ID) 259 if err != nil { 260 return nil, gtserror.NewErrorBadRequest(err, "error processing header") 261 } 262 ia.HeaderMediaAttachmentID = headerInfo.ID 263 ia.HeaderMediaAttachment = headerInfo 264 updateInstanceAccount = true 265 } 266 267 if updateInstanceAccount { 268 // if either avatar or header is updated, we need 269 // to update the instance account that stores them 270 if err := p.state.DB.UpdateAccount(ctx, ia); err != nil { 271 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance account: %s", err)) 272 } 273 } 274 275 if len(updatingColumns) != 0 { 276 if err := p.state.DB.UpdateByID(ctx, i, i.ID, updatingColumns...); err != nil { 277 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance %s: %s", host, err)) 278 } 279 } 280 281 ai, err := p.tc.InstanceToAPIV1Instance(ctx, i) 282 if err != nil { 283 return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) 284 } 285 286 return ai, nil 287 } 288 289 func obfuscate(domain string) string { 290 obfuscated := make([]rune, len(domain)) 291 for i, r := range domain { 292 if i%3 == 1 || i%5 == 1 { 293 obfuscated[i] = '*' 294 } else { 295 obfuscated[i] = r 296 } 297 } 298 return string(obfuscated) 299 }