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
+}