diff --git a/internal/api/client/admin/domainblock.go b/internal/api/client/admin/domainblock.go index 651a04116..0f944df5b 100644 --- a/internal/api/client/admin/domainblock.go +++ b/internal/api/client/admin/domainblock.go @@ -57,3 +57,8 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { c.JSON(http.StatusOK, domainBlock) } + +func validateCreateDomainBlock(form *model.DomainBlockCreateRequest) error { + // TODO: add some validation here later if necessary + return nil +} diff --git a/internal/api/model/domainblock.go b/internal/api/model/domainblock.go index 837d16d21..ec7eb481d 100644 --- a/internal/api/model/domainblock.go +++ b/internal/api/model/domainblock.go @@ -20,6 +20,7 @@ package model // DomainBlock represents a block on one domain type DomainBlock struct { + ID string `json:"id"` Domain string `json:"domain"` Obfuscate bool `json:"obfuscate"` PrivateComment string `json:"private_comment"` diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index b7e66ce55..b32984e95 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -25,7 +25,7 @@ type DomainBlock struct { // ID of this block in the database ID string `pg:"type:CHAR(26),pk,notnull,unique"` // blocked domain - Domain string `pg:",notnull"` + Domain string `pg:",pk,notnull,unique"` // When was this block created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this block updated diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index c13c35f43..857831ba3 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -7,7 +7,7 @@ type Instance struct { // ID of this instance in the database ID string `pg:"type:CHAR(26),pk,notnull,unique"` // Instance domain eg example.org - Domain string `pg:",notnull,unique"` + Domain string `pg:",pk,notnull,unique"` // Title of this instance as it would like to be displayed. Title string // base URI of this instance eg https://example.org diff --git a/internal/processing/admin.go b/internal/processing/admin.go index 6ee3a059f..2df106e98 100644 --- a/internal/processing/admin.go +++ b/internal/processing/admin.go @@ -19,55 +19,15 @@ package processing import ( - "bytes" - "errors" - "fmt" - "io" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) { - if !authed.User.Admin { - return nil, fmt.Errorf("user %s not an admin", authed.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 + return p.adminProcessor.EmojiCreate(authed.Account, authed.User, form) +} + +func (p *processor) AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) { + return p.adminProcessor.DomainBlockCreate(authed.Account, form) } diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go new file mode 100644 index 000000000..2c5a5148c --- /dev/null +++ b/internal/processing/admin/admin.go @@ -0,0 +1,55 @@ +/* + 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 . +*/ + +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) + 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 + db db.DB + log *logrus.Logger +} + +// New returns a new admin processor. +func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, config *config.Config, log *logrus.Logger) Processor { + return &processor{ + tc: tc, + config: config, + mediaHandler: mediaHandler, + db: db, + log: log, + } +} diff --git a/internal/processing/admin/domainblock.go b/internal/processing/admin/domainblock.go new file mode 100644 index 000000000..42fad563d --- /dev/null +++ b/internal/processing/admin/domainblock.go @@ -0,0 +1,114 @@ +/* + 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 . +*/ + +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 little while + go p.domainBlockProcessSideEffects(domainBlock) // TODO: add this to a queuing system so it can retry/resume + } + + mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock) + 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 +} + +func (p *processor) domainBlockProcessSideEffects(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("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) + } + + aaaaaaaaa + // TODO: delete accounts through the normal account deletion system (which should also delete media + posts + remove posts from timelines) +} 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 . +*/ + +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/processor.go b/internal/processing/processor.go index 566bec8e5..e2106111f 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -32,8 +32,9 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/status" - "github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/streaming" + "github.com/superseriousbusiness/gotosocial/internal/processing/admin" + "github.com/superseriousbusiness/gotosocial/internal/processing/status" + "github.com/superseriousbusiness/gotosocial/internal/processing/streaming" "github.com/superseriousbusiness/gotosocial/internal/timeline" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/visibility" @@ -81,6 +82,8 @@ type Processor interface { // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. 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) // AppCreate processes the creation of a new API application AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) @@ -210,6 +213,7 @@ type processor struct { SUB-PROCESSORS */ + adminProcessor admin.Processor statusProcessor status.Processor streamingProcessor streaming.Processor } @@ -222,6 +226,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f statusProcessor := status.New(db, tc, config, fromClientAPI, log) streamingProcessor := streaming.New(db, tc, oauthServer, config, log) + adminProcessor := admin.New(db, tc, mediaHandler, config, log) return &processor{ fromClientAPI: fromClientAPI, @@ -238,6 +243,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f db: db, filter: visibility.NewFilter(db, log), + adminProcessor: adminProcessor, statusProcessor: statusProcessor, streamingProcessor: streamingProcessor, } diff --git a/internal/processing/synchronous/status/boost.go b/internal/processing/status/boost.go similarity index 100% rename from internal/processing/synchronous/status/boost.go rename to internal/processing/status/boost.go diff --git a/internal/processing/synchronous/status/boostedby.go b/internal/processing/status/boostedby.go similarity index 100% rename from internal/processing/synchronous/status/boostedby.go rename to internal/processing/status/boostedby.go diff --git a/internal/processing/synchronous/status/context.go b/internal/processing/status/context.go similarity index 100% rename from internal/processing/synchronous/status/context.go rename to internal/processing/status/context.go diff --git a/internal/processing/synchronous/status/create.go b/internal/processing/status/create.go similarity index 100% rename from internal/processing/synchronous/status/create.go rename to internal/processing/status/create.go diff --git a/internal/processing/synchronous/status/delete.go b/internal/processing/status/delete.go similarity index 100% rename from internal/processing/synchronous/status/delete.go rename to internal/processing/status/delete.go diff --git a/internal/processing/synchronous/status/fave.go b/internal/processing/status/fave.go similarity index 100% rename from internal/processing/synchronous/status/fave.go rename to internal/processing/status/fave.go diff --git a/internal/processing/synchronous/status/favedby.go b/internal/processing/status/favedby.go similarity index 100% rename from internal/processing/synchronous/status/favedby.go rename to internal/processing/status/favedby.go diff --git a/internal/processing/synchronous/status/get.go b/internal/processing/status/get.go similarity index 100% rename from internal/processing/synchronous/status/get.go rename to internal/processing/status/get.go diff --git a/internal/processing/synchronous/status/status.go b/internal/processing/status/status.go similarity index 100% rename from internal/processing/synchronous/status/status.go rename to internal/processing/status/status.go diff --git a/internal/processing/synchronous/status/unboost.go b/internal/processing/status/unboost.go similarity index 100% rename from internal/processing/synchronous/status/unboost.go rename to internal/processing/status/unboost.go diff --git a/internal/processing/synchronous/status/unfave.go b/internal/processing/status/unfave.go similarity index 100% rename from internal/processing/synchronous/status/unfave.go rename to internal/processing/status/unfave.go diff --git a/internal/processing/synchronous/status/util.go b/internal/processing/status/util.go similarity index 100% rename from internal/processing/synchronous/status/util.go rename to internal/processing/status/util.go diff --git a/internal/processing/synchronous/streaming/authorize.go b/internal/processing/streaming/authorize.go similarity index 100% rename from internal/processing/synchronous/streaming/authorize.go rename to internal/processing/streaming/authorize.go diff --git a/internal/processing/synchronous/streaming/openstream.go b/internal/processing/streaming/openstream.go similarity index 100% rename from internal/processing/synchronous/streaming/openstream.go rename to internal/processing/streaming/openstream.go diff --git a/internal/processing/synchronous/streaming/streamdelete.go b/internal/processing/streaming/streamdelete.go similarity index 100% rename from internal/processing/synchronous/streaming/streamdelete.go rename to internal/processing/streaming/streamdelete.go diff --git a/internal/processing/synchronous/streaming/streaming.go b/internal/processing/streaming/streaming.go similarity index 100% rename from internal/processing/synchronous/streaming/streaming.go rename to internal/processing/streaming/streaming.go diff --git a/internal/processing/synchronous/streaming/streamnotification.go b/internal/processing/streaming/streamnotification.go similarity index 100% rename from internal/processing/synchronous/streaming/streamnotification.go rename to internal/processing/streaming/streamnotification.go diff --git a/internal/processing/synchronous/streaming/streamstatus.go b/internal/processing/streaming/streamstatus.go similarity index 100% rename from internal/processing/synchronous/streaming/streamstatus.go rename to internal/processing/streaming/streamstatus.go diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 80a922635..5063990eb 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -76,6 +76,8 @@ type TypeConverter interface { RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) // 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) /* FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index c2f00c77d..dce753071 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -644,3 +644,16 @@ func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notifi Status: mastoStatus, }, nil } + +func (c *converter) DomainBlockToMasto(b *gtsmodel.DomainBlock) (*model.DomainBlock, error) { + return &model.DomainBlock{ + ID: b.ID, + 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 +}