diff options
Diffstat (limited to 'internal/processing/admin')
-rw-r--r-- | internal/processing/admin/admin.go | 60 | ||||
-rw-r--r-- | internal/processing/admin/createdomainblock.go | 154 | ||||
-rw-r--r-- | internal/processing/admin/deletedomainblock.go | 36 | ||||
-rw-r--r-- | internal/processing/admin/emoji.go | 73 | ||||
-rw-r--r-- | internal/processing/admin/getdomainblock.go | 30 | ||||
-rw-r--r-- | internal/processing/admin/getdomainblocks.go | 30 |
6 files changed, 383 insertions, 0 deletions
diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go new file mode 100644 index 000000000..8cb1d7f78 --- /dev/null +++ b/internal/processing/admin/admin.go @@ -0,0 +1,60 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +package admin + +import ( + "github.com/sirupsen/logrus" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" +) + +// 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) + DomainBlockGet(account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) + DomainBlockDelete(account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) + EmojiCreate(account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) +} + +type processor struct { + tc typeutils.TypeConverter + config *config.Config + mediaHandler media.Handler + fromClientAPI chan gtsmodel.FromClientAPI + db db.DB + log *logrus.Logger +} + +// New returns a new admin processor. +func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, fromClientAPI chan gtsmodel.FromClientAPI, config *config.Config, log *logrus.Logger) Processor { + return &processor{ + tc: tc, + config: config, + mediaHandler: mediaHandler, + fromClientAPI: fromClientAPI, + db: db, + log: log, + } +} diff --git a/internal/processing/admin/createdomainblock.go b/internal/processing/admin/createdomainblock.go new file mode 100644 index 000000000..82a7cac4f --- /dev/null +++ b/internal/processing/admin/createdomainblock.go @@ -0,0 +1,154 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +package admin + +import ( + "fmt" + "time" + + "github.com/sirupsen/logrus" + 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" +) + +func (p *processor) DomainBlockCreate(account *gtsmodel.Account, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) { + // 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 + domainBlock := >smodel.DomainBlock{} + err := p.db.GetWhere([]db.Where{{Key: "domain", Value: form.Domain, CaseInsensitive: true}}, domainBlock) + if err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // something went wrong in the DB + return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error checking for existence of domain block %s: %s", form.Domain, err)) + } + + // there's no block for this domain yet so create one + // note: we take a new ulid from timestamp here in case we need to sort blocks + blockID, err := id.NewULID() + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: error creating id for new domain block %s: %s", form.Domain, err)) + } + + domainBlock = >smodel.DomainBlock{ + ID: blockID, + Domain: form.Domain, + CreatedByAccountID: account.ID, + PrivateComment: form.PrivateComment, + PublicComment: form.PublicComment, + Obfuscate: form.Obfuscate, + } + + // put the new block in the database + if err := p.db.Put(domainBlock); err != nil { + if _, ok := err.(db.ErrAlreadyExists); !ok { + // there's a real error creating the block + return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error putting new domain block %s: %s", form.Domain, err)) + } + } + + // process the side effects of the domain block asynchronously since it might take a while + go p.initiateDomainBlockSideEffects(account, domainBlock) // TODO: add this to a queuing system so it can retry/resume + } + + 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)) + } + + return mastoDomainBlock, nil +} + +// 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. +func (p *processor) initiateDomainBlockSideEffects(account *gtsmodel.Account, block *gtsmodel.DomainBlock) { + l := p.log.WithFields(logrus.Fields{ + "func": "domainBlockProcessSideEffects", + "domain": block.Domain, + }) + + 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 := >smodel.Instance{} + if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: block.Domain, CaseInsensitive: true}}, instance); err == nil { + 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 = "" + if err := p.db.UpdateByID(instance.ID, instance); err != nil { + 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 + if err := p.db.DeleteWhere([]db.Where{{Key: "username", Value: block.Domain, CaseInsensitive: true}}, >smodel.Account{}); err != nil { + l.Errorf("domainBlockProcessSideEffects: db error removing instance account: %s", err) + } + + // 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 { + accounts, err := p.db.GetAccountsForInstance(block.Domain, maxID, limit) + if err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + // 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 + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsPerson, + APActivityType: gtsmodel.ActivityStreamsDelete, + GTSModel: a, + OriginAccount: account, + TargetAccount: a, + } + + // 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 + } + } + } +} diff --git a/internal/processing/admin/deletedomainblock.go b/internal/processing/admin/deletedomainblock.go new file mode 100644 index 000000000..973344dc2 --- /dev/null +++ b/internal/processing/admin/deletedomainblock.go @@ -0,0 +1,36 @@ +package admin + +import ( + "fmt" + + 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) DomainBlockDelete(account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) { + domainBlock := >smodel.DomainBlock{} + + if err := p.db.GetByID(id, domainBlock); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // 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 + mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, false) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + // delete the domain block + if err := p.db.DeleteByID(id, domainBlock); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return mastoDomainBlock, nil +} diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go new file mode 100644 index 000000000..f19e173b5 --- /dev/null +++ b/internal/processing/admin/emoji.go @@ -0,0 +1,73 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +package admin + +import ( + "bytes" + "errors" + "fmt" + "io" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" +) + +func (p *processor) EmojiCreate(account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) { + if user.Admin { + return nil, fmt.Errorf("user %s not an admin", user.ID) + } + + // open the emoji and extract the bytes from it + f, err := form.Image.Open() + if err != nil { + return nil, fmt.Errorf("error opening emoji: %s", err) + } + buf := new(bytes.Buffer) + size, err := io.Copy(buf, f) + if err != nil { + return nil, fmt.Errorf("error reading emoji: %s", err) + } + if size == 0 { + return nil, errors.New("could not read provided emoji: size 0 bytes") + } + + // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using + emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode) + if err != nil { + return nil, fmt.Errorf("error reading emoji: %s", err) + } + + emojiID, err := id.NewULID() + if err != nil { + return nil, err + } + emoji.ID = emojiID + + mastoEmoji, err := p.tc.EmojiToMasto(emoji) + if err != nil { + return nil, fmt.Errorf("error converting emoji to mastotype: %s", err) + } + + if err := p.db.Put(emoji); err != nil { + return nil, fmt.Errorf("database error while processing emoji: %s", err) + } + + return &mastoEmoji, nil +} diff --git a/internal/processing/admin/getdomainblock.go b/internal/processing/admin/getdomainblock.go new file mode 100644 index 000000000..170d82e0b --- /dev/null +++ b/internal/processing/admin/getdomainblock.go @@ -0,0 +1,30 @@ +package admin + +import ( + "fmt" + + 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) DomainBlockGet(account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) { + domainBlock := >smodel.DomainBlock{} + + if err := p.db.GetByID(id, domainBlock); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // 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)) + } + + mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, export) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return mastoDomainBlock, nil +} 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 +} |