2023-03-12 16:00:57 +01: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-07-05 13:23:03 +02:00
package admin
import (
2023-02-22 16:05:26 +01:00
"bytes"
2021-08-25 15:34:33 +02:00
"context"
2023-02-22 16:05:26 +01:00
"encoding/json"
2022-09-02 12:17:46 +02:00
"errors"
2021-07-05 13:23:03 +02:00
"fmt"
2023-02-22 16:05:26 +01:00
"io"
"mime/multipart"
2022-05-02 12:53:46 +02:00
"strings"
2021-07-05 13:23:03 +02:00
"time"
2022-07-19 10:47:55 +02:00
"codeberg.org/gruf/go-kv"
2021-08-31 15:59:12 +02:00
"github.com/superseriousbusiness/gotosocial/internal/ap"
2021-07-05 13:23:03 +02:00
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
2022-07-19 10:47:55 +02:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2021-08-31 15:59:12 +02:00
"github.com/superseriousbusiness/gotosocial/internal/messages"
2021-07-26 20:25:54 +02:00
"github.com/superseriousbusiness/gotosocial/internal/text"
2021-07-05 13:23:03 +02:00
)
2023-02-22 16:05:26 +01:00
func ( p * Processor ) DomainBlockCreate ( ctx context . Context , account * gtsmodel . Account , domain string , obfuscate bool , publicComment string , privateComment string , subscriptionID string ) ( * apimodel . DomainBlock , gtserror . WithCode ) {
2022-05-02 12:53:46 +02:00
// domain blocks will always be lowercase
domain = strings . ToLower ( domain )
2021-07-05 13:23:03 +02:00
// 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
2023-03-01 19:26:53 +01:00
block , err := p . state . DB . GetDomainBlock ( ctx , domain )
2021-07-05 13:23:03 +02:00
if err != nil {
2022-09-02 12:17:46 +02:00
if ! errors . Is ( err , db . ErrNoEntries ) {
2021-07-05 13:23:03 +02:00
// something went wrong in the DB
2022-09-02 12:17:46 +02:00
return nil , gtserror . NewErrorInternalError ( fmt . Errorf ( "db error checking for existence of domain block %s: %s" , domain , err ) )
2021-07-05 13:23:03 +02:00
}
// there's no block for this domain yet so create one
2022-10-06 12:48:17 +02:00
newBlock := & gtsmodel . DomainBlock {
2023-02-03 21:03:05 +01:00
ID : id . NewULID ( ) ,
2021-07-06 13:29:11 +02:00
Domain : domain ,
2021-07-05 13:23:03 +02:00
CreatedByAccountID : account . ID ,
2022-05-26 11:37:13 +02:00
PrivateComment : text . SanitizePlaintext ( privateComment ) ,
PublicComment : text . SanitizePlaintext ( publicComment ) ,
2022-08-15 12:35:05 +02:00
Obfuscate : & obfuscate ,
2021-07-06 13:29:11 +02:00
SubscriptionID : subscriptionID ,
2021-07-05 13:23:03 +02:00
}
2022-09-02 12:17:46 +02:00
// Insert the new block into the database
2023-03-01 19:26:53 +01:00
if err := p . state . DB . CreateDomainBlock ( ctx , newBlock ) ; err != nil {
2022-09-02 12:17:46 +02:00
return nil , gtserror . NewErrorInternalError ( fmt . Errorf ( "db error putting new domain block %s: %s" , domain , err ) )
2021-07-05 13:23:03 +02:00
}
2022-09-02 12:17:46 +02:00
// Set the newly created block
2022-10-06 12:48:17 +02:00
block = newBlock
2022-09-02 12:17:46 +02:00
// Process the side effects of the domain block asynchronously since it might take a while
2022-10-04 17:50:29 +02:00
go func ( ) {
p . initiateDomainBlockSideEffects ( context . Background ( ) , account , block )
} ( )
2021-07-05 13:23:03 +02:00
}
2022-09-02 12:17:46 +02:00
// Convert our gts model domain block into an API model
apiDomainBlock , err := p . tc . DomainBlockToAPIDomainBlock ( ctx , block , false )
2021-07-05 13:23:03 +02:00
if err != nil {
2022-09-02 12:17:46 +02:00
return nil , gtserror . NewErrorInternalError ( fmt . Errorf ( "error converting domain block to frontend/api representation %s: %s" , domain , err ) )
2021-07-05 13:23:03 +02:00
}
2021-10-04 15:24:19 +02:00
return apiDomainBlock , nil
2021-07-05 13:23:03 +02:00
}
// initiateDomainBlockSideEffects should be called asynchronously, to process the side effects of a domain block:
//
// 1. Strip most info away from the instance entry for the domain.
// 2. Delete the instance account for that instance if it exists.
// 3. Select all accounts from this instance and pass them through the delete functionality of the processor.
2023-02-22 16:05:26 +01:00
func ( p * Processor ) initiateDomainBlockSideEffects ( ctx context . Context , account * gtsmodel . Account , block * gtsmodel . DomainBlock ) {
l := log . WithContext ( ctx ) . WithFields ( kv . Fields { { "domain" , block . Domain } } ... )
2021-07-05 13:23:03 +02:00
l . Debug ( "processing domain block side effects" )
// if we have an instance entry for this domain, update it with the new block ID and clear all fields
instance := & gtsmodel . Instance { }
2023-03-01 19:26:53 +01:00
if err := p . state . DB . GetWhere ( ctx , [ ] db . Where { { Key : "domain" , Value : block . Domain } } , instance ) ; err == nil {
2022-08-15 12:35:05 +02:00
updatingColumns := [ ] string {
"title" ,
"updated_at" ,
"suspended_at" ,
"domain_block_id" ,
"short_description" ,
"description" ,
"terms" ,
"contact_email" ,
"contact_account_username" ,
"contact_account_id" ,
"version" ,
}
2021-07-05 13:23:03 +02:00
instance . Title = ""
instance . UpdatedAt = time . Now ( )
instance . SuspendedAt = time . Now ( )
instance . DomainBlockID = block . ID
instance . ShortDescription = ""
instance . Description = ""
instance . Terms = ""
instance . ContactEmail = ""
instance . ContactAccountUsername = ""
instance . ContactAccountID = ""
instance . Version = ""
2023-03-01 19:26:53 +01:00
if err := p . state . DB . UpdateByID ( ctx , instance , instance . ID , updatingColumns ... ) ; err != nil {
2021-07-05 13:23:03 +02:00
l . Errorf ( "domainBlockProcessSideEffects: db error updating instance: %s" , err )
}
l . Debug ( "domainBlockProcessSideEffects: instance entry updated" )
}
// if we have an instance account for this instance, delete it
2023-03-01 19:26:53 +01:00
if instanceAccount , err := p . state . DB . GetAccountByUsernameDomain ( ctx , block . Domain , block . Domain ) ; err == nil {
if err := p . state . DB . DeleteAccount ( ctx , instanceAccount . ID ) ; err != nil {
2022-10-08 13:50:48 +02:00
l . Errorf ( "domainBlockProcessSideEffects: db error deleting instance account: %s" , err )
}
2021-07-05 13:23:03 +02:00
}
// delete accounts through the normal account deletion system (which should also delete media + posts + remove posts from timelines)
limit := 20 // just select 20 accounts at a time so we don't nuke our DB/mem with one huge query
var maxID string // this is initially an empty string so we'll start at the top of accounts list (sorted by ID)
selectAccountsLoop :
for {
2023-03-01 19:26:53 +01:00
accounts , err := p . state . DB . GetInstanceAccounts ( ctx , block . Domain , maxID , limit )
2021-07-05 13:23:03 +02:00
if err != nil {
2021-08-20 12:26:56 +02:00
if err == db . ErrNoEntries {
2021-07-05 13:23:03 +02:00
// no accounts left for this instance so we're done
l . Infof ( "domainBlockProcessSideEffects: done iterating through accounts for domain %s" , block . Domain )
break selectAccountsLoop
}
// an actual error has occurred
l . Errorf ( "domainBlockProcessSideEffects: db error selecting accounts for domain %s: %s" , block . Domain , err )
break selectAccountsLoop
}
for i , a := range accounts {
l . Debugf ( "putting delete for account %s in the clientAPI channel" , a . Username )
// pass the account delete through the client api channel for processing
2023-03-01 19:26:53 +01:00
p . state . Workers . EnqueueClientAPI ( ctx , messages . FromClientAPI {
2021-08-31 15:59:12 +02:00
APObjectType : ap . ActorPerson ,
APActivityType : ap . ActivityDelete ,
2021-07-06 13:29:11 +02:00
GTSModel : block ,
2021-07-05 13:23:03 +02:00
OriginAccount : account ,
TargetAccount : a ,
2022-04-28 14:23:11 +02:00
} )
2021-07-05 13:23:03 +02:00
// if this is the last account in the slice, set the maxID appropriately for the next query
if i == len ( accounts ) - 1 {
maxID = a . ID
}
}
}
}
2023-02-22 16:05:26 +01:00
// DomainBlocksImport handles the import of a bunch of domain blocks at once, by calling the DomainBlockCreate function for each domain in the provided file.
func ( p * Processor ) DomainBlocksImport ( ctx context . Context , account * gtsmodel . Account , domains * multipart . FileHeader ) ( [ ] * apimodel . DomainBlock , gtserror . WithCode ) {
f , err := domains . Open ( )
if err != nil {
return nil , gtserror . NewErrorBadRequest ( fmt . Errorf ( "DomainBlocksImport: error opening attachment: %s" , err ) )
}
buf := new ( bytes . Buffer )
size , err := io . Copy ( buf , f )
if err != nil {
return nil , gtserror . NewErrorBadRequest ( fmt . Errorf ( "DomainBlocksImport: error reading attachment: %s" , err ) )
}
if size == 0 {
return nil , gtserror . NewErrorBadRequest ( errors . New ( "DomainBlocksImport: could not read provided attachment: size 0 bytes" ) )
}
d := [ ] apimodel . DomainBlock { }
if err := json . Unmarshal ( buf . Bytes ( ) , & d ) ; err != nil {
return nil , gtserror . NewErrorBadRequest ( fmt . Errorf ( "DomainBlocksImport: could not read provided attachment: %s" , err ) )
}
blocks := [ ] * apimodel . DomainBlock { }
for _ , d := range d {
block , err := p . DomainBlockCreate ( ctx , account , d . Domain . Domain , false , d . PublicComment , "" , "" )
if err != nil {
return nil , err
}
blocks = append ( blocks , block )
}
return blocks , nil
}
// DomainBlocksGet returns all existing domain blocks.
// If export is true, the format will be suitable for writing out to an export.
func ( p * Processor ) DomainBlocksGet ( ctx context . Context , account * gtsmodel . Account , export bool ) ( [ ] * apimodel . DomainBlock , gtserror . WithCode ) {
domainBlocks := [ ] * gtsmodel . DomainBlock { }
2023-03-01 19:26:53 +01:00
if err := p . state . DB . GetAll ( ctx , & domainBlocks ) ; err != nil {
2023-02-22 16:05:26 +01:00
if ! errors . Is ( err , db . ErrNoEntries ) {
// something has gone really wrong
return nil , gtserror . NewErrorInternalError ( err )
}
}
apiDomainBlocks := [ ] * apimodel . DomainBlock { }
for _ , b := range domainBlocks {
apiDomainBlock , err := p . tc . DomainBlockToAPIDomainBlock ( ctx , b , export )
if err != nil {
return nil , gtserror . NewErrorInternalError ( err )
}
apiDomainBlocks = append ( apiDomainBlocks , apiDomainBlock )
}
return apiDomainBlocks , nil
}
// DomainBlockGet returns one domain block with the given id.
// If export is true, the format will be suitable for writing out to an export.
func ( p * Processor ) DomainBlockGet ( ctx context . Context , account * gtsmodel . Account , id string , export bool ) ( * apimodel . DomainBlock , gtserror . WithCode ) {
domainBlock := & gtsmodel . DomainBlock { }
2023-03-01 19:26:53 +01:00
if err := p . state . DB . GetByID ( ctx , id , domainBlock ) ; err != nil {
2023-02-22 16:05:26 +01:00
if ! errors . Is ( err , db . ErrNoEntries ) {
// something has gone really wrong
return nil , gtserror . NewErrorInternalError ( err )
}
// there are no entries for this ID
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "no entry for ID %s" , id ) )
}
apiDomainBlock , err := p . tc . DomainBlockToAPIDomainBlock ( ctx , domainBlock , export )
if err != nil {
return nil , gtserror . NewErrorInternalError ( err )
}
return apiDomainBlock , nil
}
// DomainBlockDelete removes one domain block with the given ID.
func ( p * Processor ) DomainBlockDelete ( ctx context . Context , account * gtsmodel . Account , id string ) ( * apimodel . DomainBlock , gtserror . WithCode ) {
domainBlock := & gtsmodel . DomainBlock { }
2023-03-01 19:26:53 +01:00
if err := p . state . DB . GetByID ( ctx , id , domainBlock ) ; err != nil {
2023-02-22 16:05:26 +01:00
if ! errors . Is ( err , db . ErrNoEntries ) {
// something has gone really wrong
return nil , gtserror . NewErrorInternalError ( err )
}
// there are no entries for this ID
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "no entry for ID %s" , id ) )
}
// prepare the domain block to return
apiDomainBlock , err := p . tc . DomainBlockToAPIDomainBlock ( ctx , domainBlock , false )
if err != nil {
return nil , gtserror . NewErrorInternalError ( err )
}
// Delete the domain block
2023-03-01 19:26:53 +01:00
if err := p . state . DB . DeleteDomainBlock ( ctx , domainBlock . Domain ) ; err != nil {
2023-02-22 16:05:26 +01:00
return nil , gtserror . NewErrorInternalError ( err )
}
// remove the domain block reference from the instance, if we have an entry for it
i := & gtsmodel . Instance { }
2023-03-01 19:26:53 +01:00
if err := p . state . DB . GetWhere ( ctx , [ ] db . Where {
2023-02-22 16:05:26 +01:00
{ Key : "domain" , Value : domainBlock . Domain } ,
{ Key : "domain_block_id" , Value : id } ,
} , i ) ; err == nil {
updatingColumns := [ ] string { "suspended_at" , "domain_block_id" , "updated_at" }
i . SuspendedAt = time . Time { }
i . DomainBlockID = ""
i . UpdatedAt = time . Now ( )
2023-03-01 19:26:53 +01:00
if err := p . state . DB . UpdateByID ( ctx , i , i . ID , updatingColumns ... ) ; err != nil {
2023-02-22 16:05:26 +01:00
return nil , gtserror . NewErrorInternalError ( fmt . Errorf ( "couldn't update database entry for instance %s: %s" , domainBlock . Domain , err ) )
}
}
// unsuspend all accounts whose suspension origin was this domain block
// 1. remove the 'suspended_at' entry from their accounts
2023-03-01 19:26:53 +01:00
if err := p . state . DB . UpdateWhere ( ctx , [ ] db . Where {
2023-02-22 16:05:26 +01:00
{ Key : "suspension_origin" , Value : domainBlock . ID } ,
} , "suspended_at" , nil , & [ ] * gtsmodel . Account { } ) ; err != nil {
return nil , gtserror . NewErrorInternalError ( fmt . Errorf ( "database error removing suspended_at from accounts: %s" , err ) )
}
// 2. remove the 'suspension_origin' entry from their accounts
2023-03-01 19:26:53 +01:00
if err := p . state . DB . UpdateWhere ( ctx , [ ] db . Where {
2023-02-22 16:05:26 +01:00
{ Key : "suspension_origin" , Value : domainBlock . ID } ,
} , "suspension_origin" , nil , & [ ] * gtsmodel . Account { } ) ; err != nil {
return nil , gtserror . NewErrorInternalError ( fmt . Errorf ( "database error removing suspension_origin from accounts: %s" , err ) )
}
return apiDomainBlock , nil
}