domainblock.go (12284B)
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 admin 19 20 import ( 21 "bytes" 22 "context" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "mime/multipart" 28 "strings" 29 "time" 30 31 "codeberg.org/gruf/go-kv" 32 "github.com/superseriousbusiness/gotosocial/internal/ap" 33 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 34 "github.com/superseriousbusiness/gotosocial/internal/db" 35 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 36 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 37 "github.com/superseriousbusiness/gotosocial/internal/id" 38 "github.com/superseriousbusiness/gotosocial/internal/log" 39 "github.com/superseriousbusiness/gotosocial/internal/messages" 40 "github.com/superseriousbusiness/gotosocial/internal/text" 41 ) 42 43 func (p *Processor) DomainBlockCreate(ctx context.Context, account *gtsmodel.Account, domain string, obfuscate bool, publicComment string, privateComment string, subscriptionID string) (*apimodel.DomainBlock, gtserror.WithCode) { 44 // domain blocks will always be lowercase 45 domain = strings.ToLower(domain) 46 47 // first check if we already have a block -- if err == nil we already had a block so we can skip a whole lot of work 48 block, err := p.state.DB.GetDomainBlock(ctx, domain) 49 if err != nil { 50 if !errors.Is(err, db.ErrNoEntries) { 51 // something went wrong in the DB 52 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error checking for existence of domain block %s: %s", domain, err)) 53 } 54 55 // there's no block for this domain yet so create one 56 newBlock := >smodel.DomainBlock{ 57 ID: id.NewULID(), 58 Domain: domain, 59 CreatedByAccountID: account.ID, 60 PrivateComment: text.SanitizePlaintext(privateComment), 61 PublicComment: text.SanitizePlaintext(publicComment), 62 Obfuscate: &obfuscate, 63 SubscriptionID: subscriptionID, 64 } 65 66 // Insert the new block into the database 67 if err := p.state.DB.CreateDomainBlock(ctx, newBlock); err != nil { 68 return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error putting new domain block %s: %s", domain, err)) 69 } 70 71 // Set the newly created block 72 block = newBlock 73 74 // Process the side effects of the domain block asynchronously since it might take a while 75 go func() { 76 p.initiateDomainBlockSideEffects(context.Background(), account, block) 77 }() 78 } 79 80 // Convert our gts model domain block into an API model 81 apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, block, false) 82 if err != nil { 83 return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting domain block to frontend/api representation %s: %s", domain, err)) 84 } 85 86 return apiDomainBlock, nil 87 } 88 89 // initiateDomainBlockSideEffects should be called asynchronously, to process the side effects of a domain block: 90 // 91 // 1. Strip most info away from the instance entry for the domain. 92 // 2. Delete the instance account for that instance if it exists. 93 // 3. Select all accounts from this instance and pass them through the delete functionality of the processor. 94 func (p *Processor) initiateDomainBlockSideEffects(ctx context.Context, account *gtsmodel.Account, block *gtsmodel.DomainBlock) { 95 l := log.WithContext(ctx).WithFields(kv.Fields{{"domain", block.Domain}}...) 96 l.Debug("processing domain block side effects") 97 98 // if we have an instance entry for this domain, update it with the new block ID and clear all fields 99 instance := >smodel.Instance{} 100 if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "domain", Value: block.Domain}}, instance); err == nil { 101 updatingColumns := []string{ 102 "title", 103 "updated_at", 104 "suspended_at", 105 "domain_block_id", 106 "short_description", 107 "description", 108 "terms", 109 "contact_email", 110 "contact_account_username", 111 "contact_account_id", 112 "version", 113 } 114 instance.Title = "" 115 instance.UpdatedAt = time.Now() 116 instance.SuspendedAt = time.Now() 117 instance.DomainBlockID = block.ID 118 instance.ShortDescription = "" 119 instance.Description = "" 120 instance.Terms = "" 121 instance.ContactEmail = "" 122 instance.ContactAccountUsername = "" 123 instance.ContactAccountID = "" 124 instance.Version = "" 125 if err := p.state.DB.UpdateByID(ctx, instance, instance.ID, updatingColumns...); err != nil { 126 l.Errorf("domainBlockProcessSideEffects: db error updating instance: %s", err) 127 } 128 l.Debug("domainBlockProcessSideEffects: instance entry updated") 129 } 130 131 // if we have an instance account for this instance, delete it 132 if instanceAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, block.Domain, block.Domain); err == nil { 133 if err := p.state.DB.DeleteAccount(ctx, instanceAccount.ID); err != nil { 134 l.Errorf("domainBlockProcessSideEffects: db error deleting instance account: %s", err) 135 } 136 } 137 138 // delete accounts through the normal account deletion system (which should also delete media + posts + remove posts from timelines) 139 140 limit := 20 // just select 20 accounts at a time so we don't nuke our DB/mem with one huge query 141 var maxID string // this is initially an empty string so we'll start at the top of accounts list (sorted by ID) 142 143 selectAccountsLoop: 144 for { 145 accounts, err := p.state.DB.GetInstanceAccounts(ctx, block.Domain, maxID, limit) 146 if err != nil { 147 if err == db.ErrNoEntries { 148 // no accounts left for this instance so we're done 149 l.Infof("domainBlockProcessSideEffects: done iterating through accounts for domain %s", block.Domain) 150 break selectAccountsLoop 151 } 152 // an actual error has occurred 153 l.Errorf("domainBlockProcessSideEffects: db error selecting accounts for domain %s: %s", block.Domain, err) 154 break selectAccountsLoop 155 } 156 157 for i, a := range accounts { 158 l.Debugf("putting delete for account %s in the clientAPI channel", a.Username) 159 160 // pass the account delete through the client api channel for processing 161 p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ 162 APObjectType: ap.ActorPerson, 163 APActivityType: ap.ActivityDelete, 164 GTSModel: block, 165 OriginAccount: account, 166 TargetAccount: a, 167 }) 168 169 // if this is the last account in the slice, set the maxID appropriately for the next query 170 if i == len(accounts)-1 { 171 maxID = a.ID 172 } 173 } 174 } 175 } 176 177 // DomainBlocksImport handles the import of a bunch of domain blocks at once, by calling the DomainBlockCreate function for each domain in the provided file. 178 func (p *Processor) DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode) { 179 f, err := domains.Open() 180 if err != nil { 181 return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error opening attachment: %s", err)) 182 } 183 buf := new(bytes.Buffer) 184 size, err := io.Copy(buf, f) 185 if err != nil { 186 return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error reading attachment: %s", err)) 187 } 188 if size == 0 { 189 return nil, gtserror.NewErrorBadRequest(errors.New("DomainBlocksImport: could not read provided attachment: size 0 bytes")) 190 } 191 192 d := []apimodel.DomainBlock{} 193 if err := json.Unmarshal(buf.Bytes(), &d); err != nil { 194 return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: could not read provided attachment: %s", err)) 195 } 196 197 blocks := []*apimodel.DomainBlock{} 198 for _, d := range d { 199 block, err := p.DomainBlockCreate(ctx, account, d.Domain.Domain, false, d.PublicComment, "", "") 200 if err != nil { 201 return nil, err 202 } 203 204 blocks = append(blocks, block) 205 } 206 207 return blocks, nil 208 } 209 210 // DomainBlocksGet returns all existing domain blocks. 211 // If export is true, the format will be suitable for writing out to an export. 212 func (p *Processor) DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { 213 domainBlocks := []*gtsmodel.DomainBlock{} 214 215 if err := p.state.DB.GetAll(ctx, &domainBlocks); err != nil { 216 if !errors.Is(err, db.ErrNoEntries) { 217 // something has gone really wrong 218 return nil, gtserror.NewErrorInternalError(err) 219 } 220 } 221 222 apiDomainBlocks := []*apimodel.DomainBlock{} 223 for _, b := range domainBlocks { 224 apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, b, export) 225 if err != nil { 226 return nil, gtserror.NewErrorInternalError(err) 227 } 228 apiDomainBlocks = append(apiDomainBlocks, apiDomainBlock) 229 } 230 231 return apiDomainBlocks, nil 232 } 233 234 // DomainBlockGet returns one domain block with the given id. 235 // If export is true, the format will be suitable for writing out to an export. 236 func (p *Processor) DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) { 237 domainBlock := >smodel.DomainBlock{} 238 239 if err := p.state.DB.GetByID(ctx, id, domainBlock); err != nil { 240 if !errors.Is(err, db.ErrNoEntries) { 241 // something has gone really wrong 242 return nil, gtserror.NewErrorInternalError(err) 243 } 244 // there are no entries for this ID 245 return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id)) 246 } 247 248 apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, export) 249 if err != nil { 250 return nil, gtserror.NewErrorInternalError(err) 251 } 252 253 return apiDomainBlock, nil 254 } 255 256 // DomainBlockDelete removes one domain block with the given ID. 257 func (p *Processor) DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) { 258 domainBlock := >smodel.DomainBlock{} 259 260 if err := p.state.DB.GetByID(ctx, id, domainBlock); err != nil { 261 if !errors.Is(err, db.ErrNoEntries) { 262 // something has gone really wrong 263 return nil, gtserror.NewErrorInternalError(err) 264 } 265 // there are no entries for this ID 266 return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id)) 267 } 268 269 // prepare the domain block to return 270 apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, false) 271 if err != nil { 272 return nil, gtserror.NewErrorInternalError(err) 273 } 274 275 // Delete the domain block 276 if err := p.state.DB.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil { 277 return nil, gtserror.NewErrorInternalError(err) 278 } 279 280 // remove the domain block reference from the instance, if we have an entry for it 281 i := >smodel.Instance{} 282 if err := p.state.DB.GetWhere(ctx, []db.Where{ 283 {Key: "domain", Value: domainBlock.Domain}, 284 {Key: "domain_block_id", Value: id}, 285 }, i); err == nil { 286 updatingColumns := []string{"suspended_at", "domain_block_id", "updated_at"} 287 i.SuspendedAt = time.Time{} 288 i.DomainBlockID = "" 289 i.UpdatedAt = time.Now() 290 if err := p.state.DB.UpdateByID(ctx, i, i.ID, updatingColumns...); err != nil { 291 return nil, gtserror.NewErrorInternalError(fmt.Errorf("couldn't update database entry for instance %s: %s", domainBlock.Domain, err)) 292 } 293 } 294 295 // unsuspend all accounts whose suspension origin was this domain block 296 // 1. remove the 'suspended_at' entry from their accounts 297 if err := p.state.DB.UpdateWhere(ctx, []db.Where{ 298 {Key: "suspension_origin", Value: domainBlock.ID}, 299 }, "suspended_at", nil, &[]*gtsmodel.Account{}); err != nil { 300 return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspended_at from accounts: %s", err)) 301 } 302 303 // 2. remove the 'suspension_origin' entry from their accounts 304 if err := p.state.DB.UpdateWhere(ctx, []db.Where{ 305 {Key: "suspension_origin", Value: domainBlock.ID}, 306 }, "suspension_origin", nil, &[]*gtsmodel.Account{}); err != nil { 307 return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspension_origin from accounts: %s", err)) 308 } 309 310 return apiDomainBlock, nil 311 }