summaryrefslogtreecommitdiff
path: root/internal/processing/admin
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/admin')
-rw-r--r--internal/processing/admin/admin.go60
-rw-r--r--internal/processing/admin/createdomainblock.go154
-rw-r--r--internal/processing/admin/deletedomainblock.go36
-rw-r--r--internal/processing/admin/emoji.go73
-rw-r--r--internal/processing/admin/getdomainblock.go30
-rw-r--r--internal/processing/admin/getdomainblocks.go30
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 := &gtsmodel.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 = &gtsmodel.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 := &gtsmodel.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}}, &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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
+}