diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go index 2432cce0f..b20dd2add 100644 --- a/internal/api/client/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -35,6 +35,9 @@ const ( EmojiPath = BasePath + "/custom_emojis" // DomainBlocksPath is used for posting domain blocks. DomainBlocksPath = BasePath + "/domain_blocks" + + // ExportQueryKey is the key to use when requesting a public export of some data. + ExportQueryKey = "export" ) // Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc) @@ -57,5 +60,6 @@ func New(config *config.Config, processor processing.Processor, log *logrus.Logg func (m *Module) Route(r router.Router) error { r.AttachHandler(http.MethodPost, EmojiPath, m.emojiCreatePOSTHandler) r.AttachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler) + r.AttachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler) return nil } diff --git a/internal/api/client/admin/domainblock.go b/internal/api/client/admin/domainblockcreate.go similarity index 99% rename from internal/api/client/admin/domainblock.go rename to internal/api/client/admin/domainblockcreate.go index e129c5e99..e1ff84d82 100644 --- a/internal/api/client/admin/domainblock.go +++ b/internal/api/client/admin/domainblockcreate.go @@ -67,3 +67,5 @@ func validateCreateDomainBlock(form *model.DomainBlockCreateRequest) error { return nil } + + diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go new file mode 100644 index 000000000..4b7844d62 --- /dev/null +++ b/internal/api/client/admin/domainblockget.go @@ -0,0 +1,53 @@ +package admin + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (m *Module) DomainBlocksGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "DomainBlocksPOSTHandler", + "request_uri": c.Request.RequestURI, + "user_agent": c.Request.UserAgent(), + "origin_ip": c.ClientIP(), + }) + + // make sure we're authed with an admin account + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + if !authed.User.Admin { + l.Debugf("user %s not an admin", authed.User.ID) + c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + return + } + + export := false + exportString := c.Query(ExportQueryKey) + if exportString != "" { + i, err := strconv.ParseBool(exportString) + if err != nil { + l.Debugf("error parsing export string: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) + return + } + export = i + } + + domainBlocks, err := m.processor.AdminDomainBlocksGet(authed, export) + if err != nil { + l.Debugf("error getting domain blocks: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, domainBlocks) +} diff --git a/internal/api/model/domainblock.go b/internal/api/model/domainblock.go index ec7eb481d..aaa28f34d 100644 --- a/internal/api/model/domainblock.go +++ b/internal/api/model/domainblock.go @@ -20,14 +20,14 @@ package model // DomainBlock represents a block on one domain type DomainBlock struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Domain string `json:"domain"` - Obfuscate bool `json:"obfuscate"` - PrivateComment string `json:"private_comment"` - PublicComment string `json:"public_comment"` - SubscriptionID string `json:"subscription_id"` - CreatedBy string `json:"created_by"` - CreatedAt string `json:"created_at"` + Obfuscate bool `json:"obfuscate,omitempty"` + PrivateComment string `json:"private_comment,omitempty"` + PublicComment string `json:"public_comment,omitempty"` + SubscriptionID string `json:"subscription_id,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + CreatedAt string `json:"created_at,omitempty"` } // DomainBlockCreateRequest is the form submitted as a POST to /api/v1/admin/domain_blocks to create a new block. diff --git a/internal/db/pg/instance.go b/internal/db/pg/instance.go index f9f50b933..2de0c5366 100644 --- a/internal/db/pg/instance.go +++ b/internal/db/pg/instance.go @@ -53,6 +53,8 @@ func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error) } func (ps *postgresService) GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error) { + ps.log.Debug("GetAccountsForInstance") + accounts := []*gtsmodel.Account{} q := ps.conn.Model(&accounts).Where("domain = ?", domain).Order("id DESC") diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index dffe06ed7..1050f141e 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -512,6 +512,7 @@ func (ps *postgresService) CountStatusesByAccountID(accountID string) (int, erro } func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) { + ps.log.Debugf("getting statuses for account %s", accountID) statuses := []*gtsmodel.Status{} q := ps.conn.Model(&statuses).Order("id DESC") @@ -547,6 +548,12 @@ func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, ex } return nil, err } + + if len(statuses) == 0 { + return nil, db.ErrNoEntries{} + } + + ps.log.Debugf("returning statuses for account %s", accountID) return statuses, nil } diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index a708ce6aa..c31b67352 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -54,7 +54,7 @@ func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error { "username": account.Username, }) - l.Debug("beginning account delete process") + l.Debugf("beginning account delete process for username %s", account.Username) // 1. Delete account's application(s), clients, and oauth tokens // we only need to do this step for local account since remote ones won't have any tokens or applications on our server @@ -85,6 +85,7 @@ func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error { } // 2. Delete account's blocks + l.Debug("deleting account blocks") // first delete any blocks that this account created if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Block{}); err != nil { l.Errorf("error deleting blocks created by account: %s", err) @@ -99,6 +100,7 @@ func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error { // nothing to do here // 4. Delete account's follow requests + l.Debug("deleting account follow requests") // first delete any follow requests that this account created if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil { l.Errorf("error deleting follow requests created by account: %s", err) @@ -110,6 +112,7 @@ func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error { } // 5. Delete account's follows + l.Debug("deleting account follows") // first delete any follows that this account created if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil { l.Errorf("error deleting follows created by account: %s", err) @@ -121,6 +124,7 @@ func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error { } // 6. Delete account's statuses + l.Debug("deleting account statuses") // we'll select statuses 20 at a time so we don't wreck the db, and pass them through to the client api channel // Deleting the statuses in this way also handles 7. Delete account's media attachments, 8. Delete account's mentions, and 9. Delete account's polls, // since these are all attached to statuses. @@ -130,7 +134,7 @@ selectStatusesLoop: statuses, err := p.db.GetStatusesForAccount(account.ID, 20, false, maxID, false, false) if err != nil { if _, ok := err.(db.ErrNoEntries); ok { - // no accounts left for this instance so we're done + // no statuses left for this instance so we're done l.Infof("Delete: done iterating through statuses for account %s", account.Username) break selectStatusesLoop } @@ -142,6 +146,7 @@ selectStatusesLoop: for i, s := range statuses { // pass the status delete through the client api channel for processing s.GTSAuthorAccount = account + l.Debug("putting status in the client api channel") p.fromClientAPI <- gtsmodel.FromClientAPI{ APObjectType: gtsmodel.ActivityStreamsNote, APActivityType: gtsmodel.ActivityStreamsDelete, @@ -158,29 +163,67 @@ selectStatusesLoop: } } + // if there are any boosts of this status, delete them as well + boosts := []*gtsmodel.Status{} + if err := p.db.GetWhere([]db.Where{{Key: "boost_of_id", Value: s.ID}}, &boosts); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // an actual error has occurred + l.Errorf("Delete: db error selecting boosts of status %s for account %s: %s", s.ID, account.Username, err) + break selectStatusesLoop + } + } + + for _, b := range boosts { + oa := >smodel.Account{} + if err := p.db.GetByID(b.AccountID, oa); err == nil { + + l.Debug("putting boost undo in the client api channel") + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsAnnounce, + APActivityType: gtsmodel.ActivityStreamsUndo, + GTSModel: s, + OriginAccount: oa, + TargetAccount: account, + } + } + + if err := p.db.DeleteByID(b.ID, b); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // actual error has occurred + l.Errorf("Delete: db error deleting boost with id %s: %s", b.ID, err) + break selectStatusesLoop + } + } + } + // if this is the last status in the slice, set the maxID appropriately for the next query if i == len(statuses)-1 { maxID = s.ID } } } + l.Debug("done deleting statuses") // 10. Delete account's notifications + l.Debug("deleting account notifications") if err := p.db.DeleteWhere([]db.Where{{Key: "origin_account_id", Value: account.ID}}, &[]*gtsmodel.Notification{}); err != nil { l.Errorf("error deleting notifications created by account: %s", err) } // 11. Delete account's bookmarks + l.Debug("deleting account bookmarks") if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil { l.Errorf("error deleting bookmarks created by account: %s", err) } // 12. Delete account's faves + l.Debug("deleting account faves") if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusFave{}); err != nil { l.Errorf("error deleting faves created by account: %s", err) } // 13. Delete account's mutes + l.Debug("deleting account mutes") if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusMute{}); err != nil { l.Errorf("error deleting status mutes created by account: %s", err) } @@ -191,6 +234,7 @@ selectStatusesLoop: // TODO // 16. Delete account's user + l.Debug("deleting account user") if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, >smodel.User{}); err != nil { return err } @@ -217,5 +261,10 @@ selectStatusesLoop: account.SuspendedAt = time.Now() account.SuspensionOrigin = deletedBy - return p.db.UpdateByID(account.ID, account) + if err := p.db.UpdateByID(account.ID, account); err != nil { + return err + } + + l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain) + return nil } diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go index 9b317aa13..b8ccbc528 100644 --- a/internal/processing/account/getstatuses.go +++ b/internal/processing/account/getstatuses.go @@ -47,10 +47,7 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou for _, s := range statuses { visible, err := p.filter.StatusVisible(s, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err)) - } - if !visible { + if err != nil || !visible { continue } diff --git a/internal/processing/admin.go b/internal/processing/admin.go index 2df106e98..0ffef5a47 100644 --- a/internal/processing/admin.go +++ b/internal/processing/admin.go @@ -31,3 +31,7 @@ func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCre func (p *processor) AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) { return p.adminProcessor.DomainBlockCreate(authed.Account, form) } + +func (p *processor) AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { + return p.adminProcessor.DomainBlocksGet(authed.Account, export) +} diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go index 5f42ff820..af1f8dca1 100644 --- a/internal/processing/admin/admin.go +++ b/internal/processing/admin/admin.go @@ -32,6 +32,7 @@ import ( // Processor wraps a bunch of functions for processing admin actions. type Processor interface { DomainBlockCreate(account *gtsmodel.Account, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) + DomainBlocksGet(account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) EmojiCreate(account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) } diff --git a/internal/processing/admin/domainblock.go b/internal/processing/admin/createdomainblock.go similarity index 93% rename from internal/processing/admin/domainblock.go rename to internal/processing/admin/createdomainblock.go index 0e80ca28c..82a7cac4f 100644 --- a/internal/processing/admin/domainblock.go +++ b/internal/processing/admin/createdomainblock.go @@ -65,10 +65,10 @@ func (p *processor) DomainBlockCreate(account *gtsmodel.Account, form *apimodel. } // process the side effects of the domain block asynchronously since it might take a while - go p.initiateDomainBlockSideEffects(domainBlock) // TODO: add this to a queuing system so it can retry/resume + go p.initiateDomainBlockSideEffects(account, domainBlock) // TODO: add this to a queuing system so it can retry/resume } - mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock) + mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, false) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: error converting domain block to frontend/masto representation %s: %s", form.Domain, err)) } @@ -81,7 +81,7 @@ func (p *processor) DomainBlockCreate(account *gtsmodel.Account, form *apimodel. // 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. -func (p *processor) initiateDomainBlockSideEffects(block *gtsmodel.DomainBlock) { +func (p *processor) initiateDomainBlockSideEffects(account *gtsmodel.Account, block *gtsmodel.DomainBlock) { l := p.log.WithFields(logrus.Fields{ "func": "domainBlockProcessSideEffects", "domain": block.Domain, @@ -134,12 +134,14 @@ 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 p.fromClientAPI <- gtsmodel.FromClientAPI{ APObjectType: gtsmodel.ActivityStreamsPerson, APActivityType: gtsmodel.ActivityStreamsDelete, GTSModel: a, - OriginAccount: a, + OriginAccount: account, TargetAccount: a, } diff --git a/internal/processing/admin/getdomainblocks.go b/internal/processing/admin/getdomainblocks.go new file mode 100644 index 000000000..57e2ca7af --- /dev/null +++ b/internal/processing/admin/getdomainblocks.go @@ -0,0 +1,30 @@ +package admin + +import ( + 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" +) + +func (p *processor) DomainBlocksGet(account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { + domainBlocks := []*gtsmodel.DomainBlock{} + + if err := p.db.GetAll(&domainBlocks); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // something has gone really wrong + return nil, gtserror.NewErrorInternalError(err) + } + } + + mastoDomainBlocks := []*apimodel.DomainBlock{} + for _, b := range domainBlocks { + mastoDomainBlock, err := p.tc.DomainBlockToMasto(b, export) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + mastoDomainBlocks = append(mastoDomainBlocks, mastoDomainBlock) + } + + return mastoDomainBlocks, nil +} diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 9141e3367..3cb21569a 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -185,7 +185,6 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return err } - // delete this status from any and all timelines if err := p.deleteStatusFromTimelines(statusToDelete); err != nil { return err @@ -393,6 +392,11 @@ func (p *processor) federateUnfave(fave *gtsmodel.StatusFave, originAccount *gts } func (p *processor) federateUnannounce(boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { + if originAccount.Domain != "" { + // nothing to do here + return nil + } + asAnnounce, err := p.tc.BoostToAS(boost, originAccount, targetAccount) if err != nil { return fmt.Errorf("federateUnannounce: error converting status to announce: %s", err) diff --git a/internal/processing/processor.go b/internal/processing/processor.go index c0eaad42a..9ecf14911 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -87,6 +87,8 @@ type Processor interface { AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) // AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form. AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) + // AdminDomainBlocksGet returns a list of currently blocked domains. + AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) // AppCreate processes the creation of a new API application AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 5063990eb..30c1c7d2c 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -77,7 +77,7 @@ type TypeConverter interface { // NotificationToMasto converts a gts notification into a mastodon notification NotificationToMasto(n *gtsmodel.Notification) (*model.Notification, error) // DomainBlockTomasto converts a gts model domin block into a mastodon domain block, for serving at /api/v1/admin/domain_blocks - DomainBlockToMasto(b *gtsmodel.DomainBlock) (*model.DomainBlock, error) + DomainBlockToMasto(b *gtsmodel.DomainBlock, export bool) (*model.DomainBlock, error) /* FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index dce753071..d8849a037 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -645,15 +645,22 @@ func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notifi }, nil } -func (c *converter) DomainBlockToMasto(b *gtsmodel.DomainBlock) (*model.DomainBlock, error) { - return &model.DomainBlock{ - ID: b.ID, +func (c *converter) DomainBlockToMasto(b *gtsmodel.DomainBlock, export bool) (*model.DomainBlock, error) { + + domainBlock := &model.DomainBlock{ Domain: b.Domain, - Obfuscate: b.Obfuscate, - PrivateComment: b.PrivateComment, PublicComment: b.PublicComment, - SubscriptionID: b.SubscriptionID, - CreatedBy: b.CreatedByAccountID, - CreatedAt: b.CreatedAt.Format(time.RFC3339), - }, nil + } + + // if we're exporting a domain block, return it with minimal information attached + if !export { + domainBlock.ID = b.ID + domainBlock.Obfuscate = b.Obfuscate + domainBlock.PrivateComment = b.PrivateComment + domainBlock.SubscriptionID = b.SubscriptionID + domainBlock.CreatedBy = b.CreatedByAccountID + domainBlock.CreatedAt = b.CreatedAt.Format(time.RFC3339) + } + + return domainBlock, nil }