mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-04-28 12:04:43 +00:00
Domain block (#76)
* start work on admin domain blocking * move stuff around + further work on domain blocks * move + restructure processor * prep work for deleting account * tidy * go fmt * formatting * domain blocking more work * check domain blocks way earlier on * progress on delete account * delete more stuff when an account is gone * and more... * domain blocky block block * get individual domain block, delete a block
This commit is contained in:
parent
cf19aaf0df
commit
d389e7b150
100 changed files with 3447 additions and 1419 deletions
internal
api
client
account
admin
fileserver
instance
media
status
model
s2s
user
webfinger
security
cliactions
db
federation
gtsmodel
oauth
processing
account.go
account
account.gocreate.gocreatefollow.godelete.goget.gogetfollowers.gogetfollowing.gogetrelationship.gogetstatuses.goremovefollow.goupdate.go
admin.goadmin
federation.gofromclientapi.gofromfederator.goinstance.gomedia.gomedia
processor.gosearch.gostatus
boost.goboostedby.gocontext.gocreate.godelete.gofave.gofavedby.goget.gostatus.gounboost.gounfave.goutil.go
streaming
util.gotypeutils
util
visibility
testrig
|
@ -53,7 +53,7 @@ func (suite *AccountUpdateTestSuite) SetupTest() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.accountModule = account.New(suite.config, suite.processor, suite.log).(*account.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
|
|
@ -82,7 +82,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
maxID = maxIDString
|
||||
}
|
||||
|
||||
pinned := false
|
||||
pinnedOnly := false
|
||||
pinnedString := c.Query(PinnedKey)
|
||||
if pinnedString != "" {
|
||||
i, err := strconv.ParseBool(pinnedString)
|
||||
|
@ -91,7 +91,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"})
|
||||
return
|
||||
}
|
||||
pinned = i
|
||||
pinnedOnly = i
|
||||
}
|
||||
|
||||
mediaOnly := false
|
||||
|
@ -106,7 +106,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
mediaOnly = i
|
||||
}
|
||||
|
||||
statuses, errWithCode := m.processor.AccountStatusesGet(authed, targetAcctID, limit, excludeReplies, maxID, pinned, mediaOnly)
|
||||
statuses, errWithCode := m.processor.AccountStatusesGet(authed, targetAcctID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error from processor account statuses get: %s", errWithCode)
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
|
|
|
@ -29,10 +29,19 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// BasePath is the base API path for this module
|
||||
// BasePath is the base API path for this module.
|
||||
BasePath = "/api/v1/admin"
|
||||
// EmojiPath is used for posting/deleting custom emojis
|
||||
// EmojiPath is used for posting/deleting custom emojis.
|
||||
EmojiPath = BasePath + "/custom_emojis"
|
||||
// DomainBlocksPath is used for posting domain blocks.
|
||||
DomainBlocksPath = BasePath + "/domain_blocks"
|
||||
// DomainBlockPath is used for interacting with a single domain block.
|
||||
DomainBlockPath = DomainBlocksPath + "/:" + IDKey
|
||||
|
||||
// ExportQueryKey is for requesting a public export of some data.
|
||||
ExportQueryKey = "export"
|
||||
// IDKey specifies the ID of a single item being interacted with.
|
||||
IDKey = "id"
|
||||
)
|
||||
|
||||
// Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc)
|
||||
|
@ -54,5 +63,9 @@ func New(config *config.Config, processor processing.Processor, log *logrus.Logg
|
|||
// Route attaches all routes from this module to the given router
|
||||
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)
|
||||
r.AttachHandler(http.MethodGet, DomainBlockPath, m.DomainBlockGETHandler)
|
||||
r.AttachHandler(http.MethodDelete, DomainBlockPath, m.DomainBlockDELETEHandler)
|
||||
return nil
|
||||
}
|
||||
|
|
70
internal/api/client/admin/domainblockcreate.go
Normal file
70
internal/api/client/admin/domainblockcreate.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainBlocksPOSTHandler deals with the creation of a new domain block.
|
||||
func (m *Module) DomainBlocksPOSTHandler(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
|
||||
}
|
||||
|
||||
// extract the media create form from the request context
|
||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
||||
form := &model.DomainBlockCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateCreateDomainBlock(form); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
domainBlock, err := m.processor.AdminDomainBlockCreate(authed, form)
|
||||
if err != nil {
|
||||
l.Debugf("error creating domain block: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, domainBlock)
|
||||
}
|
||||
|
||||
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest) error {
|
||||
// add some more validation here later if necessary
|
||||
if form.Domain == "" {
|
||||
return errors.New("empty domain provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
47
internal/api/client/admin/domainblockdelete.go
Normal file
47
internal/api/client/admin/domainblockdelete.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainBlockDELETEHandler deals with the delete of an existing domain block.
|
||||
func (m *Module) DomainBlockDELETEHandler(c *gin.Context) {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "DomainBlockDELETEHandler",
|
||||
"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
|
||||
}
|
||||
|
||||
domainBlockID := c.Param(IDKey)
|
||||
if domainBlockID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
|
||||
return
|
||||
}
|
||||
|
||||
domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(authed, domainBlockID)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error deleting domain block: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, domainBlock)
|
||||
}
|
60
internal/api/client/admin/domainblockget.go
Normal file
60
internal/api/client/admin/domainblockget.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainBlockGETHandler returns one existing domain block, identified by its id.
|
||||
func (m *Module) DomainBlockGETHandler(c *gin.Context) {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "DomainBlockGETHandler",
|
||||
"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
|
||||
}
|
||||
|
||||
domainBlockID := c.Param(IDKey)
|
||||
if domainBlockID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
|
||||
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
|
||||
}
|
||||
|
||||
domainBlock, err := m.processor.AdminDomainBlockGet(authed, domainBlockID, export)
|
||||
if err != nil {
|
||||
l.Debugf("error getting domain block: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, domainBlock)
|
||||
}
|
54
internal/api/client/admin/domainblocksget.go
Normal file
54
internal/api/client/admin/domainblocksget.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainBlocksGETHandler returns a list of all existing domain blocks.
|
||||
func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "DomainBlocksGETHandler",
|
||||
"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)
|
||||
}
|
|
@ -78,7 +78,7 @@ func (suite *ServeFileTestSuite) SetupSuite() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||
suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// InstanceUpdatePATCHHandler allows an admin to update the instance information served at /api/v1/instance
|
||||
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
|
||||
l := m.log.WithField("func", "InstanceUpdatePATCHHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
|
|
|
@ -84,7 +84,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() {
|
|||
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||
suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
|
||||
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
|
||||
// setup module being tested
|
||||
|
|
|
@ -52,7 +52,7 @@ func (suite *StatusBoostTestSuite) SetupTest() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
|
|
@ -57,7 +57,7 @@ func (suite *StatusCreateTestSuite) SetupTest() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
|
|
@ -55,7 +55,7 @@ func (suite *StatusFaveTestSuite) SetupTest() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
|
|
@ -55,7 +55,7 @@ func (suite *StatusFavedByTestSuite) SetupTest() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
|
|
@ -45,7 +45,7 @@ func (suite *StatusGetTestSuite) SetupTest() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
|
|
@ -55,7 +55,7 @@ func (suite *StatusUnfaveTestSuite) SetupTest() {
|
|||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
|
43
internal/api/model/domainblock.go
Normal file
43
internal/api/model/domainblock.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
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 model
|
||||
|
||||
// DomainBlock represents a block on one domain
|
||||
type DomainBlock struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Domain string `json:"domain"`
|
||||
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.
|
||||
type DomainBlockCreateRequest struct {
|
||||
// hostname/domain to block
|
||||
Domain string `form:"domain" json:"domain" xml:"domain" validation:"required"`
|
||||
// whether the domain should be obfuscated when being displayed publicly
|
||||
Obfuscate bool `form:"obfuscate" json:"obfuscate" xml:"obfuscate"`
|
||||
// private comment for other admins on why the domain was blocked
|
||||
PrivateComment string `form:"private_comment" json:"private_comment" xml:"private_comment"`
|
||||
// public comment on the reason for the domain block
|
||||
PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"`
|
||||
}
|
|
@ -19,10 +19,12 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
|
||||
|
@ -46,9 +48,14 @@ func (m *Module) FollowersGETHandler(c *gin.Context) {
|
|||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
// make a copy of the context to pass along so we don't break anything
|
||||
cp := c.Copy()
|
||||
user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well
|
||||
// transfer the signature verifier from the gin context to the request context
|
||||
ctx := c.Request.Context()
|
||||
verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier))
|
||||
if signed {
|
||||
ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier)
|
||||
}
|
||||
|
||||
user, err := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well
|
||||
if err != nil {
|
||||
l.Info(err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
|
||||
|
@ -46,9 +48,14 @@ func (m *Module) FollowingGETHandler(c *gin.Context) {
|
|||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
// make a copy of the context to pass along so we don't break anything
|
||||
cp := c.Copy()
|
||||
user, err := m.processor.GetFediFollowing(requestedUsername, cp.Request) // handles auth as well
|
||||
// transfer the signature verifier from the gin context to the request context
|
||||
ctx := c.Request.Context()
|
||||
verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier))
|
||||
if signed {
|
||||
ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier)
|
||||
}
|
||||
|
||||
user, err := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL) // handles auth as well
|
||||
if err != nil {
|
||||
l.Info(err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
|
|
|
@ -19,11 +19,13 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
||||
|
@ -40,7 +42,14 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request)
|
||||
// transfer the signature verifier from the gin context to the request context
|
||||
ctx := c.Request.Context()
|
||||
verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier))
|
||||
if signed {
|
||||
ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier)
|
||||
}
|
||||
|
||||
posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request)
|
||||
if err != nil {
|
||||
if withCode, ok := err.(gtserror.WithCode); ok {
|
||||
l.Debug(withCode.Error())
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key.
|
||||
|
@ -32,9 +34,14 @@ func (m *Module) PublicKeyGETHandler(c *gin.Context) {
|
|||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
// make a copy of the context to pass along so we don't break anything
|
||||
cp := c.Copy()
|
||||
user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetFediUser handles auth as well
|
||||
// transfer the signature verifier from the gin context to the request context
|
||||
ctx := c.Request.Context()
|
||||
verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier))
|
||||
if signed {
|
||||
ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier)
|
||||
}
|
||||
|
||||
user, err := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well
|
||||
if err != nil {
|
||||
l.Info(err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it.
|
||||
|
@ -34,9 +36,14 @@ func (m *Module) StatusGETHandler(c *gin.Context) {
|
|||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
// make a copy of the context to pass along so we don't break anything
|
||||
cp := c.Copy()
|
||||
status, err := m.processor.GetFediStatus(requestedUsername, requestedStatusID, cp.Request) // handles auth as well
|
||||
// transfer the signature verifier from the gin context to the request context
|
||||
ctx := c.Request.Context()
|
||||
verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier))
|
||||
if signed {
|
||||
ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier)
|
||||
}
|
||||
|
||||
status, err := m.processor.GetFediStatus(ctx, requestedUsername, requestedStatusID, c.Request.URL) // handles auth as well
|
||||
if err != nil {
|
||||
l.Info(err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// UsersGETHandler should be served at https://example.org/users/:username.
|
||||
|
@ -54,9 +56,14 @@ func (m *Module) UsersGETHandler(c *gin.Context) {
|
|||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
// make a copy of the context to pass along so we don't break anything
|
||||
cp := c.Copy()
|
||||
user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetFediUser handles auth as well
|
||||
// transfer the signature verifier from the gin context to the request context
|
||||
ctx := c.Request.Context()
|
||||
verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier))
|
||||
if signed {
|
||||
ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier)
|
||||
}
|
||||
|
||||
user, err := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well
|
||||
if err != nil {
|
||||
l.Info(err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
|
|
|
@ -42,7 +42,7 @@ func (suite *UserGetTestSuite) SetupTest() {
|
|||
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
||||
suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module)
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
@ -98,7 +98,7 @@ func (suite *UserGetTestSuite) TestGetUser() {
|
|||
}, nil
|
||||
}))
|
||||
// get this transport controller embedded right in the user module we're testing
|
||||
federator := testrig.NewTestFederator(suite.db, tc)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator)
|
||||
userModule := user.New(suite.config, processor, suite.log).(*user.Module)
|
||||
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
package webfinger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org
|
||||
|
@ -68,7 +70,14 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
resp, err := m.processor.GetWebfingerAccount(username, c.Request)
|
||||
// transfer the signature verifier from the gin context to the request context
|
||||
ctx := c.Request.Context()
|
||||
verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier))
|
||||
if signed {
|
||||
ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier)
|
||||
}
|
||||
|
||||
resp, err := m.processor.GetWebfingerAccount(ctx, username, c.Request.URL)
|
||||
if err != nil {
|
||||
l.Debugf("aborting request with an error: %s", err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||
)
|
||||
|
||||
|
@ -33,18 +34,21 @@ const robotsPath = "/robots.txt"
|
|||
type Module struct {
|
||||
config *config.Config
|
||||
log *logrus.Logger
|
||||
db db.DB
|
||||
}
|
||||
|
||||
// New returns a new security module
|
||||
func New(config *config.Config, log *logrus.Logger) api.ClientModule {
|
||||
func New(config *config.Config, db db.DB, log *logrus.Logger) api.ClientModule {
|
||||
return &Module{
|
||||
config: config,
|
||||
log: log,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Route attaches security middleware to the given router
|
||||
func (m *Module) Route(s router.Router) error {
|
||||
s.AttachMiddleware(m.SignatureCheck)
|
||||
s.AttachMiddleware(m.FlocBlock)
|
||||
s.AttachMiddleware(m.ExtraHeaders)
|
||||
s.AttachMiddleware(m.UserAgentBlock)
|
||||
|
|
69
internal/api/security/signaturecheck.go
Normal file
69
internal/api/security/signaturecheck.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// SignatureCheck checks whether an incoming http request has been signed. If so, it will check if the domain
|
||||
// that signed the request is permitted to access the server. If it is permitted, the handler will set the key
|
||||
// verifier in the gin context for use down the line.
|
||||
func (m *Module) SignatureCheck(c *gin.Context) {
|
||||
l := m.log.WithField("func", "DomainBlockChecker")
|
||||
|
||||
// set this extra field for signature validation
|
||||
c.Request.Header.Set("host", m.config.Host)
|
||||
|
||||
// create the verifier from the request
|
||||
// if the request is signed, it will have a signature header
|
||||
verifier, err := httpsig.NewVerifier(c.Request)
|
||||
if err == nil {
|
||||
// the request was signed!
|
||||
|
||||
// The key ID should be given in the signature so that we know where to fetch it from the remote server.
|
||||
// This will be something like https://example.org/users/whatever_requesting_user#main-key
|
||||
requestingPublicKeyID, err := url.Parse(verifier.KeyId())
|
||||
if err == nil && requestingPublicKeyID != nil {
|
||||
// we managed to parse the url!
|
||||
|
||||
// if the domain is blocked we want to bail as early as possible
|
||||
blockedDomain, err := m.blockedDomain(requestingPublicKeyID.Host)
|
||||
if err != nil {
|
||||
l.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if blockedDomain {
|
||||
l.Infof("domain %s is blocked", requestingPublicKeyID.Host)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// set the verifier on the context here to save some work further down the line
|
||||
c.Set(string(util.APRequestingPublicKeyVerifier), verifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) blockedDomain(host string) (bool, error) {
|
||||
b := >smodel.DomainBlock{}
|
||||
err := m.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b)
|
||||
if err == nil {
|
||||
// block exists
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// there are no entries so there's no block
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// there's an actual error
|
||||
return false, err
|
||||
}
|
|
@ -112,7 +112,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
|
|||
mediaHandler := media.New(c, dbService, storageBackend, log)
|
||||
oauthServer := oauth.New(dbService, log)
|
||||
transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
|
||||
federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
|
||||
federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter, mediaHandler)
|
||||
processor := processing.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, timelineManager, dbService, log)
|
||||
if err := processor.Start(); err != nil {
|
||||
return fmt.Errorf("error starting processor: %s", err)
|
||||
|
@ -138,7 +138,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
|
|||
fileServerModule := fileserver.New(c, processor, log)
|
||||
adminModule := admin.New(c, processor, log)
|
||||
statusModule := status.New(c, processor, log)
|
||||
securityModule := security.New(c, log)
|
||||
securityModule := security.New(c, dbService, log)
|
||||
streamingModule := streaming.New(c, processor, log)
|
||||
|
||||
apis := []api.ClientModule{
|
||||
|
|
|
@ -57,7 +57,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log
|
|||
Body: r,
|
||||
}, nil
|
||||
}))
|
||||
federator := testrig.NewTestFederator(dbService, transportController)
|
||||
federator := testrig.NewTestFederator(dbService, transportController, storageBackend)
|
||||
|
||||
processor := testrig.NewTestProcessor(dbService, storageBackend, federator)
|
||||
if err := processor.Start(); err != nil {
|
||||
|
@ -84,7 +84,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log
|
|||
fileServerModule := fileserver.New(c, processor, log)
|
||||
adminModule := admin.New(c, processor, log)
|
||||
statusModule := status.New(c, processor, log)
|
||||
securityModule := security.New(c, log)
|
||||
securityModule := security.New(c, dbService, log)
|
||||
streamingModule := streaming.New(c, processor, log)
|
||||
|
||||
apis := []api.ClientModule{
|
||||
|
|
|
@ -65,11 +65,6 @@ type DB interface {
|
|||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetWhere(where []Where, i interface{}) error
|
||||
|
||||
// // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where".
|
||||
// // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second
|
||||
// // being Key domain and Value example.org, only entries will be returned where BOTH conditions are true.
|
||||
// GetWhereMany(i interface{}, where ...model.Where) error
|
||||
|
||||
// GetAll will try to get all entries of type i.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
|
@ -155,11 +150,11 @@ type DB interface {
|
|||
// CountStatusesByAccountID is a shortcut for the common action of counting statuses produced by accountID.
|
||||
CountStatusesByAccountID(accountID string) (int, error)
|
||||
|
||||
// GetStatusesByTimeDescending is a shortcut for getting the most recent statuses. accountID is optional, if not provided
|
||||
// GetStatusesForAccount is a shortcut for getting the most recent statuses. accountID is optional, if not provided
|
||||
// then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can
|
||||
// be very memory intensive so you probably shouldn't do this!
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) error
|
||||
GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetLastStatusForAccountID simply gets the most recent status by the given account.
|
||||
// The given slice 'status' pointer will be set to the result of the query, whatever it is.
|
||||
|
@ -261,6 +256,10 @@ type DB interface {
|
|||
|
||||
// GetDomainCountForInstance returns the number of known instances known that the given domain federates with.
|
||||
GetDomainCountForInstance(domain string) (int, error)
|
||||
|
||||
// GetAccountsForInstance returns a slice of accounts from the given instance, arranged by ID.
|
||||
GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error)
|
||||
|
||||
/*
|
||||
USEFUL CONVERSION FUNCTIONS
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@ package pg
|
|||
|
||||
import (
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
|
@ -50,3 +51,33 @@ func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error)
|
|||
|
||||
return q.Count()
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("id < ?", maxID)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
err := q.Select()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(accounts) == 0 {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
|
@ -511,39 +511,50 @@ func (ps *postgresService) CountStatusesByAccountID(accountID string) (int, erro
|
|||
return count, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) error {
|
||||
q := ps.conn.Model(statuses).Order("created_at DESC")
|
||||
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")
|
||||
if accountID != "" {
|
||||
q = q.Where("account_id = ?", accountID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if excludeReplies {
|
||||
q = q.Where("? IS NULL", pg.Ident("in_reply_to_id"))
|
||||
}
|
||||
if pinned {
|
||||
|
||||
if pinnedOnly {
|
||||
q = q.Where("pinned = ?", true)
|
||||
}
|
||||
|
||||
if mediaOnly {
|
||||
q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) {
|
||||
return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil
|
||||
})
|
||||
}
|
||||
|
||||
if maxID != "" {
|
||||
s := >smodel.Status{}
|
||||
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
|
||||
return err
|
||||
}
|
||||
q = q.Where("status.created_at < ?", s.CreatedAt)
|
||||
q = q.Where("id < ?", maxID)
|
||||
}
|
||||
|
||||
if err := q.Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
|
||||
if len(statuses) == 0 {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
ps.log.Debugf("returning statuses for account %s", accountID)
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error {
|
||||
|
|
|
@ -25,7 +25,6 @@ import (
|
|||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
|
@ -35,6 +34,7 @@ import (
|
|||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
/*
|
||||
|
@ -115,34 +115,30 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca
|
|||
//
|
||||
// Also note that this function *does not* dereference the remote account that the signature key is associated with.
|
||||
// Other functions should use the returned URL to dereference the remote account, if required.
|
||||
func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *http.Request) (*url.URL, error) {
|
||||
func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*url.URL, bool, error) {
|
||||
l := f.log.WithField("func", "AuthenticateFederatedRequest")
|
||||
|
||||
var publicKey interface{}
|
||||
var pkOwnerURI *url.URL
|
||||
var err error
|
||||
|
||||
// set this extra field for signature validation
|
||||
r.Header.Set("host", f.config.Host)
|
||||
|
||||
verifier, err := httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create http sig verifier: %s", err)
|
||||
// thanks to signaturecheck.go in the security package, we should already have a signature verifier set on the context
|
||||
vi := ctx.Value(util.APRequestingPublicKeyVerifier)
|
||||
if vi == nil {
|
||||
l.Debug("request wasn't signed")
|
||||
return nil, false, nil // request wasn't signed
|
||||
}
|
||||
|
||||
verifier, ok := vi.(httpsig.Verifier)
|
||||
if !ok {
|
||||
l.Debug("couldn't extract sig verifier")
|
||||
return nil, false, nil // couldn't extract the verifier
|
||||
}
|
||||
|
||||
// The key ID should be given in the signature so that we know where to fetch it from the remote server.
|
||||
// This will be something like https://example.org/users/whatever_requesting_user#main-key
|
||||
requestingPublicKeyID, err := url.Parse(verifier.KeyId())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
|
||||
}
|
||||
|
||||
// if the domain is blocked we want to make as few calls towards it as possible, so already bail here if that's the case!
|
||||
blockedDomain, err := f.blockedDomain(requestingPublicKeyID.Host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err)
|
||||
}
|
||||
if blockedDomain {
|
||||
return nil, fmt.Errorf("host %s was domain blocked, aborting auth", requestingPublicKeyID.Host)
|
||||
l.Debug("couldn't parse public key URL")
|
||||
return nil, false, nil // couldn't parse the public key ID url
|
||||
}
|
||||
|
||||
requestingRemoteAccount := >smodel.Account{}
|
||||
|
@ -152,12 +148,12 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
|
|||
// LOCAL ACCOUNT REQUEST
|
||||
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil {
|
||||
return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
|
||||
return nil, false, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
|
||||
}
|
||||
publicKey = requestingLocalAccount.PublicKey
|
||||
pkOwnerURI, err = url.Parse(requestingLocalAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
|
||||
return nil, false, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
|
||||
}
|
||||
} else if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingRemoteAccount); err == nil {
|
||||
// REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY
|
||||
|
@ -165,7 +161,7 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
|
|||
publicKey = requestingRemoteAccount.PublicKey
|
||||
pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing url %s: %s", requestingRemoteAccount.URI, err)
|
||||
return nil, false, fmt.Errorf("error parsing url %s: %s", requestingRemoteAccount.URI, err)
|
||||
}
|
||||
} else {
|
||||
// REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY
|
||||
|
@ -173,72 +169,55 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
|
|||
// so we need to authenticate the request properly by dereferencing the remote key
|
||||
transport, err := f.GetTransportForUser(requestedUsername)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transport err: %s", err)
|
||||
return nil, false, fmt.Errorf("transport err: %s", err)
|
||||
}
|
||||
|
||||
// The actual http call to the remote server is made right here in the Dereference function.
|
||||
b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
|
||||
return nil, false, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
|
||||
}
|
||||
|
||||
// if the key isn't in the response, we can't authenticate the request
|
||||
requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
|
||||
return nil, false, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
|
||||
}
|
||||
|
||||
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
|
||||
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
|
||||
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
|
||||
return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
|
||||
return nil, false, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
|
||||
}
|
||||
|
||||
// and decode the PEM so that we can parse it as a golang public key
|
||||
pubKeyPem := pkPemProp.Get()
|
||||
block, _ := pem.Decode([]byte(pubKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
return nil, false, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
}
|
||||
|
||||
publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
||||
return nil, false, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
||||
}
|
||||
|
||||
// all good! we just need the URI of the key owner to return
|
||||
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
|
||||
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
|
||||
return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
|
||||
return nil, false, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
|
||||
}
|
||||
pkOwnerURI = pkOwnerProp.GetIRI()
|
||||
}
|
||||
if publicKey == nil {
|
||||
return nil, errors.New("returned public key was empty")
|
||||
return nil, false, errors.New("returned public key was empty")
|
||||
}
|
||||
|
||||
// do the actual authentication here!
|
||||
algo := httpsig.RSA_SHA256 // TODO: make this more robust
|
||||
if err := verifier.Verify(publicKey, algo); err != nil {
|
||||
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
return pkOwnerURI, nil
|
||||
}
|
||||
|
||||
func (f *federator) blockedDomain(host string) (bool, error) {
|
||||
b := >smodel.DomainBlock{}
|
||||
err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b)
|
||||
if err == nil {
|
||||
// block exists
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// there are no entries so there's no block
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// there's an actual error
|
||||
return false, err
|
||||
return pkOwnerURI, true, nil
|
||||
}
|
||||
|
|
|
@ -9,7 +9,11 @@ import (
|
|||
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
|
@ -17,6 +21,10 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u
|
|||
f.startHandshake(username, remoteAccountID)
|
||||
defer f.stopHandshake(username, remoteAccountID)
|
||||
|
||||
if blocked, err := f.blockedDomain(remoteAccountID.Host); blocked || err != nil {
|
||||
return nil, fmt.Errorf("DereferenceRemoteAccount: domain %s is blocked", remoteAccountID.Host)
|
||||
}
|
||||
|
||||
transport, err := f.GetTransportForUser(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transport err: %s", err)
|
||||
|
@ -62,6 +70,10 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u
|
|||
}
|
||||
|
||||
func (f *federator) DereferenceRemoteStatus(username string, remoteStatusID *url.URL) (typeutils.Statusable, error) {
|
||||
if blocked, err := f.blockedDomain(remoteStatusID.Host); blocked || err != nil {
|
||||
return nil, fmt.Errorf("DereferenceRemoteStatus: domain %s is blocked", remoteStatusID.Host)
|
||||
}
|
||||
|
||||
transport, err := f.GetTransportForUser(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transport err: %s", err)
|
||||
|
@ -144,6 +156,10 @@ func (f *federator) DereferenceRemoteStatus(username string, remoteStatusID *url
|
|||
}
|
||||
|
||||
func (f *federator) DereferenceRemoteInstance(username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error) {
|
||||
if blocked, err := f.blockedDomain(remoteInstanceURI.Host); blocked || err != nil {
|
||||
return nil, fmt.Errorf("DereferenceRemoteInstance: domain %s is blocked", remoteInstanceURI.Host)
|
||||
}
|
||||
|
||||
transport, err := f.GetTransportForUser(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transport err: %s", err)
|
||||
|
@ -151,3 +167,358 @@ func (f *federator) DereferenceRemoteInstance(username string, remoteInstanceURI
|
|||
|
||||
return transport.DereferenceInstance(context.Background(), remoteInstanceURI)
|
||||
}
|
||||
|
||||
// dereferenceStatusFields fetches all the information we temporarily pinned to an incoming
|
||||
// federated status, back in the federating db's Create function.
|
||||
//
|
||||
// When a status comes in from the federation API, there are certain fields that
|
||||
// haven't been dereferenced yet, because we needed to provide a snappy synchronous
|
||||
// response to the caller. By the time it reaches this function though, it's being
|
||||
// processed asynchronously, so we have all the time in the world to fetch the various
|
||||
// bits and bobs that are attached to the status, and properly flesh it out, before we
|
||||
// send the status to any timelines and notify people.
|
||||
//
|
||||
// Things to dereference and fetch here:
|
||||
//
|
||||
// 1. Media attachments.
|
||||
// 2. Hashtags.
|
||||
// 3. Emojis.
|
||||
// 4. Mentions.
|
||||
// 5. Posting account.
|
||||
// 6. Replied-to-status.
|
||||
//
|
||||
// SIDE EFFECTS:
|
||||
// This function will deference all of the above, insert them in the database as necessary,
|
||||
// and attach them to the status. The status itself will not be added to the database yet,
|
||||
// that's up the caller to do.
|
||||
func (f *federator) DereferenceStatusFields(status *gtsmodel.Status, requestingUsername string) error {
|
||||
l := f.log.WithFields(logrus.Fields{
|
||||
"func": "dereferenceStatusFields",
|
||||
"status": fmt.Sprintf("%+v", status),
|
||||
})
|
||||
l.Debug("entering function")
|
||||
|
||||
statusURI, err := url.Parse(status.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DereferenceStatusFields: couldn't parse status URI %s: %s", status.URI, err)
|
||||
}
|
||||
if blocked, err := f.blockedDomain(statusURI.Host); blocked || err != nil {
|
||||
return fmt.Errorf("DereferenceStatusFields: domain %s is blocked", statusURI.Host)
|
||||
}
|
||||
|
||||
t, err := f.GetTransportForUser(requestingUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating transport: %s", err)
|
||||
}
|
||||
|
||||
// the status should have an ID by now, but just in case it doesn't let's generate one here
|
||||
// because we'll need it further down
|
||||
if status.ID == "" {
|
||||
newID, err := id.NewULIDFromTime(status.CreatedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status.ID = newID
|
||||
}
|
||||
|
||||
// 1. Media attachments.
|
||||
//
|
||||
// At this point we should know:
|
||||
// * the media type of the file we're looking for (a.File.ContentType)
|
||||
// * the blurhash (a.Blurhash)
|
||||
// * the file type (a.Type)
|
||||
// * the remote URL (a.RemoteURL)
|
||||
// This should be enough to pass along to the media processor.
|
||||
attachmentIDs := []string{}
|
||||
for _, a := range status.GTSMediaAttachments {
|
||||
l.Debugf("dereferencing attachment: %+v", a)
|
||||
|
||||
// it might have been processed elsewhere so check first if it's already in the database or not
|
||||
maybeAttachment := >smodel.MediaAttachment{}
|
||||
err := f.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
|
||||
if err == nil {
|
||||
// we already have it in the db, dereferenced, no need to do it again
|
||||
l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
|
||||
attachmentIDs = append(attachmentIDs, maybeAttachment.ID)
|
||||
continue
|
||||
}
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
// we have a real error
|
||||
return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err)
|
||||
}
|
||||
// it just doesn't exist yet so carry on
|
||||
l.Debug("attachment doesn't exist yet, calling ProcessRemoteAttachment", a)
|
||||
deferencedAttachment, err := f.mediaHandler.ProcessRemoteAttachment(t, a, status.AccountID)
|
||||
if err != nil {
|
||||
l.Errorf("error dereferencing status attachment: %s", err)
|
||||
continue
|
||||
}
|
||||
l.Debugf("dereferenced attachment: %+v", deferencedAttachment)
|
||||
deferencedAttachment.StatusID = status.ID
|
||||
deferencedAttachment.Description = a.Description
|
||||
if err := f.db.Put(deferencedAttachment); err != nil {
|
||||
return fmt.Errorf("error inserting dereferenced attachment with remote url %s: %s", a.RemoteURL, err)
|
||||
}
|
||||
attachmentIDs = append(attachmentIDs, deferencedAttachment.ID)
|
||||
}
|
||||
status.Attachments = attachmentIDs
|
||||
|
||||
// 2. Hashtags
|
||||
|
||||
// 3. Emojis
|
||||
|
||||
// 4. Mentions
|
||||
// At this point, mentions should have the namestring and mentionedAccountURI set on them.
|
||||
//
|
||||
// We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
|
||||
mentions := []string{}
|
||||
for _, m := range status.GTSMentions {
|
||||
if m.ID == "" {
|
||||
mID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.ID = mID
|
||||
}
|
||||
|
||||
uri, err := url.Parse(m.MentionedAccountURI)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
|
||||
continue
|
||||
}
|
||||
|
||||
m.StatusID = status.ID
|
||||
m.OriginAccountID = status.GTSAuthorAccount.ID
|
||||
m.OriginAccountURI = status.GTSAuthorAccount.URI
|
||||
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
|
||||
// proper error
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
return fmt.Errorf("db error checking for account with uri %s", uri.String())
|
||||
}
|
||||
|
||||
// we just don't have it yet, so we should go get it....
|
||||
accountable, err := f.DereferenceRemoteAccount(requestingUsername, uri)
|
||||
if err != nil {
|
||||
// we can't dereference it so just skip it
|
||||
l.Debugf("error dereferencing remote account with uri %s: %s", uri.String(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
targetAccount, err = f.typeConverter.ASRepresentationToAccount(accountable, false)
|
||||
if err != nil {
|
||||
l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
targetAccountID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetAccount.ID = targetAccountID
|
||||
|
||||
if err := f.db.Put(targetAccount); err != nil {
|
||||
return fmt.Errorf("db error inserting account with uri %s", uri.String())
|
||||
}
|
||||
}
|
||||
|
||||
// by this point, we know the targetAccount exists in our database with an ID :)
|
||||
m.TargetAccountID = targetAccount.ID
|
||||
if err := f.db.Put(m); err != nil {
|
||||
return fmt.Errorf("error creating mention: %s", err)
|
||||
}
|
||||
mentions = append(mentions, m.ID)
|
||||
}
|
||||
status.Mentions = mentions
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federator) DereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error {
|
||||
l := f.log.WithFields(logrus.Fields{
|
||||
"func": "dereferenceAccountFields",
|
||||
"requestingUsername": requestingUsername,
|
||||
})
|
||||
|
||||
accountURI, err := url.Parse(account.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DereferenceAccountFields: couldn't parse account URI %s: %s", account.URI, err)
|
||||
}
|
||||
if blocked, err := f.blockedDomain(accountURI.Host); blocked || err != nil {
|
||||
return fmt.Errorf("DereferenceAccountFields: domain %s is blocked", accountURI.Host)
|
||||
}
|
||||
|
||||
t, err := f.GetTransportForUser(requestingUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting transport for user: %s", err)
|
||||
}
|
||||
|
||||
// fetch the header and avatar
|
||||
if err := f.fetchHeaderAndAviForAccount(account, t, refresh); err != nil {
|
||||
// if this doesn't work, just skip it -- we can do it later
|
||||
l.Debugf("error fetching header/avi for account: %s", err)
|
||||
}
|
||||
|
||||
if err := f.db.UpdateByID(account.ID, account); err != nil {
|
||||
return fmt.Errorf("error updating account in database: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error {
|
||||
if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" {
|
||||
// we can't do anything unfortunately
|
||||
return errors.New("DereferenceAnnounce: no URI to dereference")
|
||||
}
|
||||
|
||||
boostedStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DereferenceAnnounce: couldn't parse boosted status URI %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
if blocked, err := f.blockedDomain(boostedStatusURI.Host); blocked || err != nil {
|
||||
return fmt.Errorf("DereferenceAnnounce: domain %s is blocked", boostedStatusURI.Host)
|
||||
}
|
||||
|
||||
// check if we already have the boosted status in the database
|
||||
boostedStatus := >smodel.Status{}
|
||||
err = f.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
|
||||
if err == nil {
|
||||
// nice, we already have it so we don't actually need to dereference it from remote
|
||||
announce.Content = boostedStatus.Content
|
||||
announce.ContentWarning = boostedStatus.ContentWarning
|
||||
announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
|
||||
announce.Sensitive = boostedStatus.Sensitive
|
||||
announce.Language = boostedStatus.Language
|
||||
announce.Text = boostedStatus.Text
|
||||
announce.BoostOfID = boostedStatus.ID
|
||||
announce.Visibility = boostedStatus.Visibility
|
||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||
announce.GTSBoostedStatus = boostedStatus
|
||||
return nil
|
||||
}
|
||||
|
||||
// we don't have it so we need to dereference it
|
||||
statusable, err := f.DereferenceRemoteStatus(requestingUsername, boostedStatusURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
// make sure we have the author account in the db
|
||||
attributedToProp := statusable.GetActivityStreamsAttributedTo()
|
||||
for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
|
||||
accountURI := iter.GetIRI()
|
||||
if accountURI == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: accountURI.String()}}, >smodel.Account{}); err == nil {
|
||||
// we already have it, fine
|
||||
continue
|
||||
}
|
||||
|
||||
// we don't have the boosted status author account yet so dereference it
|
||||
accountable, err := f.DereferenceRemoteAccount(requestingUsername, accountURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote account with id %s: %s", accountURI.String(), err)
|
||||
}
|
||||
account, err := f.typeConverter.ASRepresentationToAccount(accountable, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
|
||||
}
|
||||
|
||||
accountID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.ID = accountID
|
||||
|
||||
if err := f.db.Put(account); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
|
||||
}
|
||||
|
||||
if err := f.DereferenceAccountFields(account, requestingUsername, false); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing fields on account with id %s : %s", accountURI.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// now convert the statusable into something we can understand
|
||||
boostedStatus, err = f.typeConverter.ASStatusToStatus(statusable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
boostedStatusID, err := id.NewULIDFromTime(boostedStatus.CreatedAt)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
boostedStatus.ID = boostedStatusID
|
||||
|
||||
if err := f.db.Put(boostedStatus); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
// now dereference additional fields straight away (we're already async here so we have time)
|
||||
if err := f.DereferenceStatusFields(boostedStatus, requestingUsername); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing status fields for status with id %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
// update with the newly dereferenced fields
|
||||
if err := f.db.UpdateByID(boostedStatus.ID, boostedStatus); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error updating dereferenced status in the db: %s", err)
|
||||
}
|
||||
|
||||
// we have everything we need!
|
||||
announce.Content = boostedStatus.Content
|
||||
announce.ContentWarning = boostedStatus.ContentWarning
|
||||
announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
|
||||
announce.Sensitive = boostedStatus.Sensitive
|
||||
announce.Language = boostedStatus.Language
|
||||
announce.Text = boostedStatus.Text
|
||||
announce.BoostOfID = boostedStatus.ID
|
||||
announce.Visibility = boostedStatus.Visibility
|
||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||
announce.GTSBoostedStatus = boostedStatus
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
|
||||
// on behalf of requestingUsername.
|
||||
//
|
||||
// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
|
||||
//
|
||||
// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
|
||||
// to reflect the creation of these new attachments.
|
||||
func (f *federator) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
|
||||
accountURI, err := url.Parse(targetAccount.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetchHeaderAndAviForAccount: couldn't parse account URI %s: %s", targetAccount.URI, err)
|
||||
}
|
||||
if blocked, err := f.blockedDomain(accountURI.Host); blocked || err != nil {
|
||||
return fmt.Errorf("fetchHeaderAndAviForAccount: domain %s is blocked", accountURI.Host)
|
||||
}
|
||||
|
||||
if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
|
||||
a, err := f.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.AvatarRemoteURL,
|
||||
Avatar: true,
|
||||
}, targetAccount.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing avatar for user: %s", err)
|
||||
}
|
||||
targetAccount.AvatarMediaAttachmentID = a.ID
|
||||
}
|
||||
|
||||
if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
|
||||
a, err := f.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.HeaderRemoteURL,
|
||||
Header: true,
|
||||
}, targetAccount.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing header for user: %s", err)
|
||||
}
|
||||
targetAccount.HeaderMediaAttachmentID = a.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -119,10 +119,15 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
|||
return nil, false, fmt.Errorf("could not fetch requested account with username %s: %s", username, err)
|
||||
}
|
||||
|
||||
publicKeyOwnerURI, err := f.AuthenticateFederatedRequest(requestedAccount.Username, r)
|
||||
publicKeyOwnerURI, authenticated, err := f.AuthenticateFederatedRequest(ctx, requestedAccount.Username)
|
||||
if err != nil {
|
||||
l.Debugf("request not authenticated: %s", err)
|
||||
return ctx, false, fmt.Errorf("not authenticated: %s", err)
|
||||
return ctx, false, err
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return ctx, false, nil
|
||||
}
|
||||
|
||||
// authentication has passed, so add an instance entry for this instance if it hasn't been done already
|
||||
|
@ -230,6 +235,14 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
|||
}
|
||||
|
||||
for _, uri := range actorIRIs {
|
||||
blockedDomain, err := f.blockedDomain(uri.Host)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error checking domain block: %s", err)
|
||||
}
|
||||
if blockedDomain {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil {
|
||||
_, ok := err.(db.ErrNoEntries)
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
package federation
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"context"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
@ -41,7 +42,13 @@ type Federator interface {
|
|||
FederatingDB() federatingdb.DB
|
||||
// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources.
|
||||
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
|
||||
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)
|
||||
//
|
||||
// If the request is valid and passes authentication, the URL of the key owner ID will be returned, as well as true, and nil.
|
||||
//
|
||||
// If the request does not pass authentication, or there's a domain block, nil, false, nil will be returned.
|
||||
//
|
||||
// If something goes wrong during authentication, nil, false, and an error will be returned.
|
||||
AuthenticateFederatedRequest(ctx context.Context, username string) (*url.URL, bool, error)
|
||||
// FingerRemoteAccount performs a webfinger lookup for a remote account, using the .well-known path. It will return the ActivityPub URI for that
|
||||
// account, or an error if it doesn't exist or can't be retrieved.
|
||||
FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error)
|
||||
|
@ -54,6 +61,12 @@ type Federator interface {
|
|||
// DereferenceRemoteInstance takes the URL of a remote instance, and a username (optional) to spin up a transport with. It then
|
||||
// does its damnedest to get some kind of information back about the instance, trying /api/v1/instance, then /.well-known/nodeinfo
|
||||
DereferenceRemoteInstance(username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error)
|
||||
// DereferenceStatusFields does further dereferencing on a status.
|
||||
DereferenceStatusFields(status *gtsmodel.Status, requestingUsername string) error
|
||||
// DereferenceAccountFields does further dereferencing on an account.
|
||||
DereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error
|
||||
// DereferenceAnnounce does further dereferencing on an announce.
|
||||
DereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error
|
||||
// GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username.
|
||||
// This can be used for making signed http requests.
|
||||
//
|
||||
|
@ -72,6 +85,7 @@ type federator struct {
|
|||
clock pub.Clock
|
||||
typeConverter typeutils.TypeConverter
|
||||
transportController transport.Controller
|
||||
mediaHandler media.Handler
|
||||
actor pub.FederatingActor
|
||||
log *logrus.Logger
|
||||
handshakes map[string][]*url.URL
|
||||
|
@ -79,7 +93,7 @@ type federator struct {
|
|||
}
|
||||
|
||||
// NewFederator returns a new federator
|
||||
func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator {
|
||||
func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter, mediaHandler media.Handler) Federator {
|
||||
|
||||
clock := &Clock{}
|
||||
f := &federator{
|
||||
|
@ -89,6 +103,7 @@ func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController tr
|
|||
clock: &Clock{},
|
||||
typeConverter: typeConverter,
|
||||
transportController: transportController,
|
||||
mediaHandler: mediaHandler,
|
||||
log: log,
|
||||
handshakeSync: &sync.Mutex{},
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() {
|
|||
return nil, nil
|
||||
}))
|
||||
// setup module being tested
|
||||
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter)
|
||||
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter, testrig.NewTestMediaHandler(suite.db, suite.storage))
|
||||
|
||||
// setup request
|
||||
ctx := context.Background()
|
||||
|
@ -155,7 +155,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
|
|||
}))
|
||||
|
||||
// now setup module being tested, with the mock transport controller
|
||||
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter)
|
||||
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter, testrig.NewTestMediaHandler(suite.db, suite.storage))
|
||||
|
||||
// setup request
|
||||
ctx := context.Background()
|
||||
|
|
|
@ -30,6 +30,9 @@ import (
|
|||
)
|
||||
|
||||
func (f *federator) FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error) {
|
||||
if blocked, err := f.blockedDomain(targetDomain); blocked || err != nil {
|
||||
return nil, fmt.Errorf("FingerRemoteAccount: domain %s is blocked", targetDomain)
|
||||
}
|
||||
|
||||
t, err := f.GetTransportForUser(requestingUsername)
|
||||
if err != nil {
|
||||
|
|
23
internal/federation/util.go
Normal file
23
internal/federation/util.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package federation
|
||||
|
||||
import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (f *federator) blockedDomain(host string) (bool, error) {
|
||||
b := >smodel.DomainBlock{}
|
||||
err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b)
|
||||
if err == nil {
|
||||
// block exists
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// there are no entries so there's no block
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// there's an actual error
|
||||
return false, err
|
||||
}
|
|
@ -20,28 +20,24 @@ package gtsmodel
|
|||
|
||||
import "time"
|
||||
|
||||
// DomainBlock represents a federation block against a particular domain, of varying severity.
|
||||
// DomainBlock represents a federation block against a particular domain
|
||||
type DomainBlock struct {
|
||||
// ID of this block in the database
|
||||
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||
// Domain to block. If ANY PART of the candidate domain contains this string, it will be blocked.
|
||||
// For example: 'example.org' also blocks 'gts.example.org'. '.com' blocks *any* '.com' domains.
|
||||
// TODO: implement wildcards here
|
||||
Domain string `pg:",notnull"`
|
||||
// blocked domain
|
||||
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
|
||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||
// Account ID of the creator of this block
|
||||
CreatedByAccountID string `pg:"type:CHAR(26),notnull"`
|
||||
// TODO: define this
|
||||
Severity int
|
||||
// Reject media from this domain?
|
||||
RejectMedia bool
|
||||
// Reject reports from this domain?
|
||||
RejectReports bool
|
||||
// Private comment on this block, viewable to admins
|
||||
PrivateComment string
|
||||
// Public comment on this block, viewable (optionally) by everyone
|
||||
PublicComment string
|
||||
// whether the domain name should appear obfuscated when displaying it publicly
|
||||
Obfuscate bool
|
||||
// if this block was created through a subscription, what's the subscription ID?
|
||||
SubscriptionID string `pg:"type:CHAR(26)"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -73,14 +73,28 @@ func Authed(c *gin.Context, requireToken bool, requireApp bool, requireUser bool
|
|||
if requireToken && a.Token == nil {
|
||||
return nil, errors.New("token not supplied")
|
||||
}
|
||||
|
||||
if requireApp && a.Application == nil {
|
||||
return nil, errors.New("application not supplied")
|
||||
}
|
||||
if requireUser && a.User == nil {
|
||||
return nil, errors.New("user not supplied")
|
||||
|
||||
if requireUser {
|
||||
if a.User == nil {
|
||||
return nil, errors.New("user not supplied")
|
||||
}
|
||||
if a.User.Disabled || !a.User.Approved {
|
||||
return nil, errors.New("user disabled or not approved")
|
||||
}
|
||||
}
|
||||
if requireAccount && a.Account == nil {
|
||||
return nil, errors.New("account not supplied")
|
||||
|
||||
if requireAccount {
|
||||
if a.Account == nil {
|
||||
return nil, errors.New("account not supplied")
|
||||
}
|
||||
if !a.Account.SuspendedAt.IsZero() {
|
||||
return nil, errors.New("account suspended")
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
|
|
@ -19,512 +19,43 @@
|
|||
package processing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// accountCreate does the dirty work of making an account and user in the database.
|
||||
// It then returns a token to the caller, for use with the new account, as per the
|
||||
// spec here: https://docs.joinmastodon.org/methods/accounts/
|
||||
func (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
|
||||
l := p.log.WithField("func", "accountCreate")
|
||||
|
||||
if err := p.db.IsEmailAvailable(form.Email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.db.IsUsernameAvailable(form.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// don't store a reason if we don't require one
|
||||
reason := form.Reason
|
||||
if !p.config.AccountsConfig.ReasonRequired {
|
||||
reason = ""
|
||||
}
|
||||
|
||||
l.Trace("creating new username and account")
|
||||
user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, authed.Application.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating new signup in the database: %s", err)
|
||||
}
|
||||
|
||||
l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, authed.Application.ID)
|
||||
accessToken, err := p.oauthServer.GenerateUserAccessToken(authed.Token, authed.Application.ClientSecret, user.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
|
||||
}
|
||||
|
||||
return &apimodel.Token{
|
||||
AccessToken: accessToken.GetAccess(),
|
||||
TokenType: "Bearer",
|
||||
Scope: accessToken.GetScope(),
|
||||
CreatedAt: accessToken.GetAccessCreateAt().Unix(),
|
||||
}, nil
|
||||
return p.accountProcessor.Create(authed.Token, authed.Application, form)
|
||||
}
|
||||
|
||||
func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, errors.New("account not found")
|
||||
}
|
||||
return nil, fmt.Errorf("db error: %s", err)
|
||||
}
|
||||
|
||||
// lazily dereference things on the account if it hasn't been done yet
|
||||
var requestingUsername string
|
||||
if authed.Account != nil {
|
||||
requestingUsername = authed.Account.Username
|
||||
}
|
||||
if err := p.dereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
|
||||
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
|
||||
}
|
||||
|
||||
var mastoAccount *apimodel.Account
|
||||
var err error
|
||||
if authed.Account != nil && targetAccount.ID == authed.Account.ID {
|
||||
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
|
||||
} else {
|
||||
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting account: %s", err)
|
||||
}
|
||||
return mastoAccount, nil
|
||||
return p.accountProcessor.Get(authed.Account, targetAccountID)
|
||||
}
|
||||
|
||||
func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
|
||||
l := p.log.WithField("func", "AccountUpdate")
|
||||
|
||||
if form.Discoverable != nil {
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil {
|
||||
return nil, fmt.Errorf("error updating discoverable: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if form.Bot != nil {
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil {
|
||||
return nil, fmt.Errorf("error updating bot: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if form.DisplayName != nil {
|
||||
if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Note != nil {
|
||||
if err := util.ValidateNote(*form.Note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||
avatarInfo, err := p.updateAccountAvatar(form.Avatar, authed.Account.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo)
|
||||
}
|
||||
|
||||
if form.Header != nil && form.Header.Size != 0 {
|
||||
headerInfo, err := p.updateAccountHeader(form.Header, authed.Account.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo)
|
||||
}
|
||||
|
||||
if form.Locked != nil {
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Source != nil {
|
||||
if form.Source.Language != nil {
|
||||
if err := util.ValidateLanguage(*form.Source.Language); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Source.Sensitive != nil {
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Source.Privacy != nil {
|
||||
if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the account with all updated values set
|
||||
updatedAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(authed.Account.ID, updatedAccount); err != nil {
|
||||
return nil, fmt.Errorf("could not fetch updated account %s: %s", authed.Account.ID, err)
|
||||
}
|
||||
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsProfile,
|
||||
APActivityType: gtsmodel.ActivityStreamsUpdate,
|
||||
GTSModel: updatedAccount,
|
||||
OriginAccount: updatedAccount,
|
||||
}
|
||||
|
||||
acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
|
||||
}
|
||||
return acctSensitive, nil
|
||||
return p.accountProcessor.Update(authed.Account, form)
|
||||
}
|
||||
|
||||
func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
statuses := []gtsmodel.Status{}
|
||||
apiStatuses := []apimodel.Status{}
|
||||
if err := p.db.GetStatusesByTimeDescending(targetAccountID, &statuses, limit, excludeReplies, maxID, pinned, mediaOnly); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return apiStatuses, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, s := range statuses {
|
||||
visible, err := p.filter.StatusVisible(&s, authed.Account)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
|
||||
}
|
||||
if !visible {
|
||||
continue
|
||||
}
|
||||
|
||||
apiStatus, err := p.tc.StatusToMasto(&s, authed.Account)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
|
||||
}
|
||||
|
||||
apiStatuses = append(apiStatuses, *apiStatus)
|
||||
}
|
||||
|
||||
return apiStatuses, nil
|
||||
func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||
return p.accountProcessor.StatusesGet(authed.Account, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||
}
|
||||
|
||||
followers := []gtsmodel.Follow{}
|
||||
accounts := []apimodel.Account{}
|
||||
if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return accounts, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, f := range followers {
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
continue
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := p.db.GetByID(f.AccountID, a); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
continue
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// derefence account fields in case we haven't done it already
|
||||
if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
|
||||
// don't bail if we can't fetch them, we'll try another time
|
||||
p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
|
||||
}
|
||||
|
||||
account, err := p.tc.AccountToMastoPublic(a)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
accounts = append(accounts, *account)
|
||||
}
|
||||
return accounts, nil
|
||||
return p.accountProcessor.FollowersGet(authed.Account, targetAccountID)
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||
}
|
||||
|
||||
following := []gtsmodel.Follow{}
|
||||
accounts := []apimodel.Account{}
|
||||
if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return accounts, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, f := range following {
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
continue
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
continue
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// derefence account fields in case we haven't done it already
|
||||
if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
|
||||
// don't bail if we can't fetch them, we'll try another time
|
||||
p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
|
||||
}
|
||||
|
||||
account, err := p.tc.AccountToMastoPublic(a)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
accounts = append(accounts, *account)
|
||||
}
|
||||
return accounts, nil
|
||||
return p.accountProcessor.FollowingGet(authed.Account, targetAccountID)
|
||||
}
|
||||
|
||||
func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||
if authed == nil || authed.Account == nil {
|
||||
return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
|
||||
}
|
||||
|
||||
gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
|
||||
}
|
||||
|
||||
r, err := p.tc.RelationshipToMasto(gtsR)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
|
||||
}
|
||||
|
||||
return r, nil
|
||||
return p.accountProcessor.RelationshipGet(authed.Account, targetAccountID)
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
|
||||
// if there's a block between the accounts we shouldn't create the request ofc
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
|
||||
}
|
||||
|
||||
// make sure the target account actually exists in our db
|
||||
targetAcct := >smodel.Account{}
|
||||
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if a follow exists already
|
||||
follows, err := p.db.Follows(authed.Account, targetAcct)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
|
||||
}
|
||||
if follows {
|
||||
// already follows so just return the relationship
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// check if a follow exists already
|
||||
followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
|
||||
}
|
||||
if followRequested {
|
||||
// already follow requested so just return the relationship
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// make the follow request
|
||||
newFollowID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
fr := >smodel.FollowRequest{
|
||||
ID: newFollowID,
|
||||
AccountID: authed.Account.ID,
|
||||
TargetAccountID: form.TargetAccountID,
|
||||
ShowReblogs: true,
|
||||
URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host, newFollowID),
|
||||
Notify: false,
|
||||
}
|
||||
if form.Reblogs != nil {
|
||||
fr.ShowReblogs = *form.Reblogs
|
||||
}
|
||||
if form.Notify != nil {
|
||||
fr.Notify = *form.Notify
|
||||
}
|
||||
|
||||
// whack it in the database
|
||||
if err := p.db.Put(fr); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
|
||||
}
|
||||
|
||||
// if it's a local account that's not locked we can just straight up accept the follow request
|
||||
if !targetAcct.Locked && targetAcct.Domain == "" {
|
||||
if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
|
||||
}
|
||||
// return the new relationship
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
GTSModel: fr,
|
||||
OriginAccount: authed.Account,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
|
||||
// return whatever relationship results from this
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
return p.accountProcessor.FollowCreate(authed.Account, form)
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||
// if there's a block between the accounts we shouldn't do anything
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
||||
}
|
||||
|
||||
// make sure the target account actually exists in our db
|
||||
targetAcct := >smodel.Account{}
|
||||
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if a follow request exists, and remove it if it does (storing the URI for later)
|
||||
var frChanged bool
|
||||
var frURI string
|
||||
fr := >smodel.FollowRequest{}
|
||||
if err := p.db.GetWhere([]db.Where{
|
||||
{Key: "account_id", Value: authed.Account.ID},
|
||||
{Key: "target_account_id", Value: targetAccountID},
|
||||
}, fr); err == nil {
|
||||
frURI = fr.URI
|
||||
if err := p.db.DeleteByID(fr.ID, fr); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
|
||||
}
|
||||
frChanged = true
|
||||
}
|
||||
|
||||
// now do the same thing for any existing follow
|
||||
var fChanged bool
|
||||
var fURI string
|
||||
f := >smodel.Follow{}
|
||||
if err := p.db.GetWhere([]db.Where{
|
||||
{Key: "account_id", Value: authed.Account.ID},
|
||||
{Key: "target_account_id", Value: targetAccountID},
|
||||
}, f); err == nil {
|
||||
fURI = f.URI
|
||||
if err := p.db.DeleteByID(f.ID, f); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
|
||||
}
|
||||
fChanged = true
|
||||
}
|
||||
|
||||
// follow request status changed so send the UNDO activity to the channel for async processing
|
||||
if frChanged {
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||
GTSModel: >smodel.Follow{
|
||||
AccountID: authed.Account.ID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: frURI,
|
||||
},
|
||||
OriginAccount: authed.Account,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
}
|
||||
|
||||
// follow status changed so send the UNDO activity to the channel for async processing
|
||||
if fChanged {
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||
GTSModel: >smodel.Follow{
|
||||
AccountID: authed.Account.ID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: fURI,
|
||||
},
|
||||
OriginAccount: authed.Account,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
}
|
||||
|
||||
// return whatever relationship results from all this
|
||||
return p.AccountRelationshipGet(authed, targetAccountID)
|
||||
return p.accountProcessor.FollowRemove(authed.Account, targetAccountID)
|
||||
}
|
||||
|
|
96
internal/processing/account/account.go
Normal file
96
internal/processing/account/account.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
"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/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/visibility"
|
||||
"github.com/superseriousbusiness/oauth2/v4"
|
||||
)
|
||||
|
||||
// Processor wraps a bunch of functions for processing account actions.
|
||||
type Processor interface {
|
||||
// Create processes the given form for creating a new account, returning an oauth token for that account if successful.
|
||||
Create(applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)
|
||||
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
|
||||
Delete(account *gtsmodel.Account, deletedBy string) error
|
||||
// Get processes the given request for account information.
|
||||
Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error)
|
||||
// Update processes the update of an account with the given form
|
||||
Update(account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
|
||||
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
||||
// the account given in authed.
|
||||
StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode)
|
||||
// FollowersGet fetches a list of the target account's followers.
|
||||
FollowersGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||
// FollowingGet fetches a list of the accounts that target account is following.
|
||||
FollowingGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||
// RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
|
||||
RelationshipGet(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||
// FollowCreate handles a follow request to an account, either remote or local.
|
||||
FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
|
||||
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
||||
FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||
// UpdateHeader does the dirty work of checking the header part of an account update form,
|
||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||
// the account's new header image.
|
||||
UpdateAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error)
|
||||
// UpdateAvatar does the dirty work of checking the avatar part of an account update form,
|
||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||
// the account's new avatar image.
|
||||
UpdateHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error)
|
||||
}
|
||||
|
||||
type processor struct {
|
||||
tc typeutils.TypeConverter
|
||||
config *config.Config
|
||||
mediaHandler media.Handler
|
||||
fromClientAPI chan gtsmodel.FromClientAPI
|
||||
oauthServer oauth.Server
|
||||
filter visibility.Filter
|
||||
db db.DB
|
||||
federator federation.Federator
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// New returns a new account processor.
|
||||
func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, oauthServer oauth.Server, fromClientAPI chan gtsmodel.FromClientAPI, federator federation.Federator, config *config.Config, log *logrus.Logger) Processor {
|
||||
return &processor{
|
||||
tc: tc,
|
||||
config: config,
|
||||
mediaHandler: mediaHandler,
|
||||
fromClientAPI: fromClientAPI,
|
||||
oauthServer: oauthServer,
|
||||
filter: visibility.NewFilter(db, log),
|
||||
db: db,
|
||||
federator: federator,
|
||||
log: log,
|
||||
}
|
||||
}
|
64
internal/processing/account/create.go
Normal file
64
internal/processing/account/create.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/oauth2/v4"
|
||||
)
|
||||
|
||||
func (p *processor) Create(applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
|
||||
l := p.log.WithField("func", "accountCreate")
|
||||
|
||||
if err := p.db.IsEmailAvailable(form.Email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.db.IsUsernameAvailable(form.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// don't store a reason if we don't require one
|
||||
reason := form.Reason
|
||||
if !p.config.AccountsConfig.ReasonRequired {
|
||||
reason = ""
|
||||
}
|
||||
|
||||
l.Trace("creating new username and account")
|
||||
user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, application.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating new signup in the database: %s", err)
|
||||
}
|
||||
|
||||
l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID)
|
||||
accessToken, err := p.oauthServer.GenerateUserAccessToken(applicationToken, application.ClientSecret, user.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
|
||||
}
|
||||
|
||||
return &apimodel.Token{
|
||||
AccessToken: accessToken.GetAccess(),
|
||||
TokenType: "Bearer",
|
||||
Scope: accessToken.GetScope(),
|
||||
CreatedAt: accessToken.GetAccessCreateAt().Unix(),
|
||||
}, nil
|
||||
}
|
116
internal/processing/account/createfollow.go
Normal file
116
internal/processing/account/createfollow.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
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"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (p *processor) FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
|
||||
// if there's a block between the accounts we shouldn't create the request ofc
|
||||
blocked, err := p.db.Blocked(requestingAccount.ID, form.TargetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
|
||||
}
|
||||
|
||||
// make sure the target account actually exists in our db
|
||||
targetAcct := >smodel.Account{}
|
||||
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if a follow exists already
|
||||
follows, err := p.db.Follows(requestingAccount, targetAcct)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
|
||||
}
|
||||
if follows {
|
||||
// already follows so just return the relationship
|
||||
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// check if a follow exists already
|
||||
followRequested, err := p.db.FollowRequested(requestingAccount, targetAcct)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
|
||||
}
|
||||
if followRequested {
|
||||
// already follow requested so just return the relationship
|
||||
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// make the follow request
|
||||
newFollowID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
fr := >smodel.FollowRequest{
|
||||
ID: newFollowID,
|
||||
AccountID: requestingAccount.ID,
|
||||
TargetAccountID: form.TargetAccountID,
|
||||
ShowReblogs: true,
|
||||
URI: util.GenerateURIForFollow(requestingAccount.Username, p.config.Protocol, p.config.Host, newFollowID),
|
||||
Notify: false,
|
||||
}
|
||||
if form.Reblogs != nil {
|
||||
fr.ShowReblogs = *form.Reblogs
|
||||
}
|
||||
if form.Notify != nil {
|
||||
fr.Notify = *form.Notify
|
||||
}
|
||||
|
||||
// whack it in the database
|
||||
if err := p.db.Put(fr); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
|
||||
}
|
||||
|
||||
// if it's a local account that's not locked we can just straight up accept the follow request
|
||||
if !targetAcct.Locked && targetAcct.Domain == "" {
|
||||
if _, err := p.db.AcceptFollowRequest(requestingAccount.ID, form.TargetAccountID); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
|
||||
}
|
||||
// return the new relationship
|
||||
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
GTSModel: fr,
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
|
||||
// return whatever relationship results from this
|
||||
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
|
||||
}
|
270
internal/processing/account/delete.go
Normal file
270
internal/processing/account/delete.go
Normal file
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// Delete handles the complete deletion of an account.
|
||||
//
|
||||
// TODO in this function:
|
||||
// 1. Delete account's application(s), clients, and oauth tokens
|
||||
// 2. Delete account's blocks
|
||||
// 3. Delete account's emoji
|
||||
// 4. Delete account's follow requests
|
||||
// 5. Delete account's follows
|
||||
// 6. Delete account's statuses
|
||||
// 7. Delete account's media attachments
|
||||
// 8. Delete account's mentions
|
||||
// 9. Delete account's polls
|
||||
// 10. Delete account's notifications
|
||||
// 11. Delete account's bookmarks
|
||||
// 12. Delete account's faves
|
||||
// 13. Delete account's mutes
|
||||
// 14. Delete account's streams
|
||||
// 15. Delete account's tags
|
||||
// 16. Delete account's user
|
||||
// 17. Delete account's timeline
|
||||
// 18. Delete account itself
|
||||
func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error {
|
||||
l := p.log.WithFields(logrus.Fields{
|
||||
"func": "Delete",
|
||||
"username": account.Username,
|
||||
})
|
||||
|
||||
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
|
||||
if account.Domain == "" {
|
||||
// see if we can get a user for this account
|
||||
u := >smodel.User{}
|
||||
if err := p.db.GetWhere([]db.Where{{Key: "account_id", Value: account.ID}}, u); err == nil {
|
||||
// we got one! select all tokens with the user's ID
|
||||
tokens := []*oauth.Token{}
|
||||
if err := p.db.GetWhere([]db.Where{{Key: "user_id", Value: u.ID}}, &tokens); err == nil {
|
||||
// we have some tokens to delete
|
||||
for _, t := range tokens {
|
||||
// delete client(s) associated with this token
|
||||
if err := p.db.DeleteByID(t.ClientID, &oauth.Client{}); err != nil {
|
||||
l.Errorf("error deleting oauth client: %s", err)
|
||||
}
|
||||
// delete application(s) associated with this token
|
||||
if err := p.db.DeleteWhere([]db.Where{{Key: "client_id", Value: t.ClientID}}, >smodel.Application{}); err != nil {
|
||||
l.Errorf("error deleting application: %s", err)
|
||||
}
|
||||
// delete the token itself
|
||||
if err := p.db.DeleteByID(t.ID, t); err != nil {
|
||||
l.Errorf("error deleting oauth token: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// now delete any blocks that target this account
|
||||
if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Block{}); err != nil {
|
||||
l.Errorf("error deleting blocks targeting account: %s", err)
|
||||
}
|
||||
|
||||
// 3. Delete account's emoji
|
||||
// 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)
|
||||
}
|
||||
|
||||
// now delete any follow requests that target this account
|
||||
if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil {
|
||||
l.Errorf("error deleting follow requests targeting account: %s", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// now delete any follows that target this account
|
||||
if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil {
|
||||
l.Errorf("error deleting follows targeting account: %s", err)
|
||||
}
|
||||
|
||||
// 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.
|
||||
var maxID string
|
||||
selectStatusesLoop:
|
||||
for {
|
||||
statuses, err := p.db.GetStatusesForAccount(account.ID, 20, false, maxID, false, false)
|
||||
if err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// no statuses left for this instance so we're done
|
||||
l.Infof("Delete: done iterating through statuses for account %s", account.Username)
|
||||
break selectStatusesLoop
|
||||
}
|
||||
// an actual error has occurred
|
||||
l.Errorf("Delete: db error selecting statuses for account %s: %s", account.Username, err)
|
||||
break 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,
|
||||
GTSModel: s,
|
||||
OriginAccount: account,
|
||||
TargetAccount: account,
|
||||
}
|
||||
|
||||
if err := p.db.DeleteByID(s.ID, s); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
// actual error has occurred
|
||||
l.Errorf("Delete: db error status %s for account %s: %s", s.ID, account.Username, err)
|
||||
break 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)
|
||||
}
|
||||
|
||||
// 14. Delete account's streams
|
||||
|
||||
// 15. Delete account's tags
|
||||
// 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
|
||||
}
|
||||
|
||||
// 17. Delete account's timeline
|
||||
|
||||
// 18. Delete account itself
|
||||
// to prevent the account being created again, set all these fields and update it in the db
|
||||
// the account won't actually be *removed* from the database but it will be set to just a stub
|
||||
|
||||
account.Note = ""
|
||||
account.DisplayName = ""
|
||||
account.AvatarMediaAttachmentID = ""
|
||||
account.AvatarRemoteURL = ""
|
||||
account.HeaderMediaAttachmentID = ""
|
||||
account.HeaderRemoteURL = ""
|
||||
account.Reason = ""
|
||||
account.Fields = []gtsmodel.Field{}
|
||||
account.HideCollections = true
|
||||
account.Discoverable = false
|
||||
|
||||
account.UpdatedAt = time.Now()
|
||||
|
||||
account.SuspendedAt = time.Now()
|
||||
account.SuspensionOrigin = deletedBy
|
||||
|
||||
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
|
||||
}
|
59
internal/processing/account/get.go
Normal file
59
internal/processing/account/get.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) {
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, errors.New("account not found")
|
||||
}
|
||||
return nil, fmt.Errorf("db error: %s", err)
|
||||
}
|
||||
|
||||
// lazily dereference things on the account if it hasn't been done yet
|
||||
var requestingUsername string
|
||||
if requestingAccount != nil {
|
||||
requestingUsername = requestingAccount.Username
|
||||
}
|
||||
if err := p.federator.DereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
|
||||
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
|
||||
}
|
||||
|
||||
var mastoAccount *apimodel.Account
|
||||
var err error
|
||||
if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
|
||||
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
|
||||
} else {
|
||||
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting account: %s", err)
|
||||
}
|
||||
return mastoAccount, nil
|
||||
}
|
79
internal/processing/account/getfollowers.go
Normal file
79
internal/processing/account/getfollowers.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
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) FollowersGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||
blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||
}
|
||||
|
||||
followers := []gtsmodel.Follow{}
|
||||
accounts := []apimodel.Account{}
|
||||
if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return accounts, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, f := range followers {
|
||||
blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
continue
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := p.db.GetByID(f.AccountID, a); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
continue
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// derefence account fields in case we haven't done it already
|
||||
if err := p.federator.DereferenceAccountFields(a, requestingAccount.Username, false); err != nil {
|
||||
// don't bail if we can't fetch them, we'll try another time
|
||||
p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
|
||||
}
|
||||
|
||||
account, err := p.tc.AccountToMastoPublic(a)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
accounts = append(accounts, *account)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
79
internal/processing/account/getfollowing.go
Normal file
79
internal/processing/account/getfollowing.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
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) FollowingGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||
blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||
}
|
||||
|
||||
following := []gtsmodel.Follow{}
|
||||
accounts := []apimodel.Account{}
|
||||
if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return accounts, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, f := range following {
|
||||
blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
continue
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
continue
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// derefence account fields in case we haven't done it already
|
||||
if err := p.federator.DereferenceAccountFields(a, requestingAccount.Username, false); err != nil {
|
||||
// don't bail if we can't fetch them, we'll try another time
|
||||
p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
|
||||
}
|
||||
|
||||
account, err := p.tc.AccountToMastoPublic(a)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
accounts = append(accounts, *account)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
46
internal/processing/account/getrelationship.go
Normal file
46
internal/processing/account/getrelationship.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *processor) RelationshipGet(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||
if requestingAccount == nil {
|
||||
return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
|
||||
}
|
||||
|
||||
gtsR, err := p.db.GetRelationship(requestingAccount.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
|
||||
}
|
||||
|
||||
r, err := p.tc.RelationshipToMasto(gtsR)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
63
internal/processing/account/getstatuses.go
Normal file
63
internal/processing/account/getstatuses.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
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) StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
apiStatuses := []apimodel.Status{}
|
||||
statuses, err := p.db.GetStatusesForAccount(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
|
||||
if err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return apiStatuses, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, s := range statuses {
|
||||
visible, err := p.filter.StatusVisible(s, requestingAccount)
|
||||
if err != nil || !visible {
|
||||
continue
|
||||
}
|
||||
|
||||
apiStatus, err := p.tc.StatusToMasto(s, requestingAccount)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
|
||||
}
|
||||
|
||||
apiStatuses = append(apiStatuses, *apiStatus)
|
||||
}
|
||||
|
||||
return apiStatuses, nil
|
||||
}
|
110
internal/processing/account/removefollow.go
Normal file
110
internal/processing/account/removefollow.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
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) FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||
// if there's a block between the accounts we shouldn't do anything
|
||||
blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
||||
}
|
||||
|
||||
// make sure the target account actually exists in our db
|
||||
targetAcct := >smodel.Account{}
|
||||
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if a follow request exists, and remove it if it does (storing the URI for later)
|
||||
var frChanged bool
|
||||
var frURI string
|
||||
fr := >smodel.FollowRequest{}
|
||||
if err := p.db.GetWhere([]db.Where{
|
||||
{Key: "account_id", Value: requestingAccount.ID},
|
||||
{Key: "target_account_id", Value: targetAccountID},
|
||||
}, fr); err == nil {
|
||||
frURI = fr.URI
|
||||
if err := p.db.DeleteByID(fr.ID, fr); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
|
||||
}
|
||||
frChanged = true
|
||||
}
|
||||
|
||||
// now do the same thing for any existing follow
|
||||
var fChanged bool
|
||||
var fURI string
|
||||
f := >smodel.Follow{}
|
||||
if err := p.db.GetWhere([]db.Where{
|
||||
{Key: "account_id", Value: requestingAccount.ID},
|
||||
{Key: "target_account_id", Value: targetAccountID},
|
||||
}, f); err == nil {
|
||||
fURI = f.URI
|
||||
if err := p.db.DeleteByID(f.ID, f); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
|
||||
}
|
||||
fChanged = true
|
||||
}
|
||||
|
||||
// follow request status changed so send the UNDO activity to the channel for async processing
|
||||
if frChanged {
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||
GTSModel: >smodel.Follow{
|
||||
AccountID: requestingAccount.ID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: frURI,
|
||||
},
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
}
|
||||
|
||||
// follow status changed so send the UNDO activity to the channel for async processing
|
||||
if fChanged {
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||
GTSModel: >smodel.Follow{
|
||||
AccountID: requestingAccount.ID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: fURI,
|
||||
},
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
}
|
||||
|
||||
// return whatever relationship results from all this
|
||||
return p.RelationshipGet(requestingAccount, targetAccountID)
|
||||
}
|
199
internal/processing/account/update.go
Normal file
199
internal/processing/account/update.go
Normal file
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
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 account
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (p *processor) Update(account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
|
||||
l := p.log.WithField("func", "AccountUpdate")
|
||||
|
||||
if form.Discoverable != nil {
|
||||
if err := p.db.UpdateOneByID(account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil {
|
||||
return nil, fmt.Errorf("error updating discoverable: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if form.Bot != nil {
|
||||
if err := p.db.UpdateOneByID(account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil {
|
||||
return nil, fmt.Errorf("error updating bot: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if form.DisplayName != nil {
|
||||
if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Note != nil {
|
||||
if err := util.ValidateNote(*form.Note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(account.ID, "note", *form.Note, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||
avatarInfo, err := p.UpdateAvatar(form.Avatar, account.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Tracef("new avatar info for account %s is %+v", account.ID, avatarInfo)
|
||||
}
|
||||
|
||||
if form.Header != nil && form.Header.Size != 0 {
|
||||
headerInfo, err := p.UpdateHeader(form.Header, account.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Tracef("new header info for account %s is %+v", account.ID, headerInfo)
|
||||
}
|
||||
|
||||
if form.Locked != nil {
|
||||
if err := p.db.UpdateOneByID(account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Source != nil {
|
||||
if form.Source.Language != nil {
|
||||
if err := util.ValidateLanguage(*form.Source.Language); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Source.Sensitive != nil {
|
||||
if err := p.db.UpdateOneByID(account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Source.Privacy != nil {
|
||||
if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.db.UpdateOneByID(account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the account with all updated values set
|
||||
updatedAccount := >smodel.Account{}
|
||||
if err := p.db.GetByID(account.ID, updatedAccount); err != nil {
|
||||
return nil, fmt.Errorf("could not fetch updated account %s: %s", account.ID, err)
|
||||
}
|
||||
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsProfile,
|
||||
APActivityType: gtsmodel.ActivityStreamsUpdate,
|
||||
GTSModel: updatedAccount,
|
||||
OriginAccount: updatedAccount,
|
||||
}
|
||||
|
||||
acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
|
||||
}
|
||||
return acctSensitive, nil
|
||||
}
|
||||
|
||||
// UpdateAvatar does the dirty work of checking the avatar part of an account update form,
|
||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||
// the account's new avatar image.
|
||||
func (p *processor) UpdateAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
|
||||
var err error
|
||||
if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
|
||||
err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
|
||||
return nil, err
|
||||
}
|
||||
f, err := avatar.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided avatar: %s", err)
|
||||
}
|
||||
|
||||
// extract the bytes
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided avatar: %s", err)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided avatar: size 0 bytes")
|
||||
}
|
||||
|
||||
// do the setting
|
||||
avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error processing avatar: %s", err)
|
||||
}
|
||||
|
||||
return avatarInfo, f.Close()
|
||||
}
|
||||
|
||||
// UpdateHeader does the dirty work of checking the header part of an account update form,
|
||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||
// the account's new header image.
|
||||
func (p *processor) UpdateHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
|
||||
var err error
|
||||
if int(header.Size) > p.config.MediaConfig.MaxImageSize {
|
||||
err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
|
||||
return nil, err
|
||||
}
|
||||
f, err := header.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided header: %s", err)
|
||||
}
|
||||
|
||||
// extract the bytes
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided header: %s", err)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided header: size 0 bytes")
|
||||
}
|
||||
|
||||
// do the setting
|
||||
headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error processing header: %s", err)
|
||||
}
|
||||
|
||||
return headerInfo, f.Close()
|
||||
}
|
|
@ -19,55 +19,27 @@
|
|||
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)
|
||||
}
|
||||
|
||||
func (p *processor) AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
|
||||
return p.adminProcessor.DomainBlocksGet(authed.Account, export)
|
||||
}
|
||||
|
||||
func (p *processor) AdminDomainBlockGet(authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
|
||||
return p.adminProcessor.DomainBlockGet(authed.Account, id, export)
|
||||
}
|
||||
|
||||
func (p *processor) AdminDomainBlockDelete(authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
|
||||
return p.adminProcessor.DomainBlockDelete(authed.Account, id)
|
||||
}
|
||||
|
|
60
internal/processing/admin/admin.go
Normal file
60
internal/processing/admin/admin.go
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
154
internal/processing/admin/createdomainblock.go
Normal file
154
internal/processing/admin/createdomainblock.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
internal/processing/admin/deletedomainblock.go
Normal file
36
internal/processing/admin/deletedomainblock.go
Normal file
|
@ -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
|
||||
}
|
73
internal/processing/admin/emoji.go
Normal file
73
internal/processing/admin/emoji.go
Normal file
|
@ -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
|
||||
}
|
30
internal/processing/admin/getdomainblock.go
Normal file
30
internal/processing/admin/getdomainblock.go
Normal file
|
@ -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
|
||||
}
|
30
internal/processing/admin/getdomainblocks.go
Normal file
30
internal/processing/admin/getdomainblocks.go
Normal file
|
@ -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
|
||||
}
|
|
@ -20,6 +20,7 @@ package processing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -89,7 +90,7 @@ func (p *processor) dereferenceFediRequest(username string, requestingAccountURI
|
|||
return requestingAccount, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||
func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||
|
@ -98,17 +99,17 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
|
|||
|
||||
var requestedPerson vocab.ActivityStreamsPerson
|
||||
var err error
|
||||
if util.IsPublicKeyPath(request.URL) {
|
||||
if util.IsPublicKeyPath(requestURL) {
|
||||
// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
|
||||
requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
} else if util.IsUserPath(request.URL) {
|
||||
} else if util.IsUserPath(requestURL) {
|
||||
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
|
||||
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||
if err != nil || !authenticated {
|
||||
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
|
||||
}
|
||||
|
||||
// if we're already handshaking/dereferencing a remote account, we can skip the dereferencing part
|
||||
|
@ -144,7 +145,7 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||
func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||
|
@ -152,9 +153,9 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
|
|||
}
|
||||
|
||||
// authenticate the request
|
||||
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||
if err != nil || !authenticated {
|
||||
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
|
||||
}
|
||||
|
||||
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
|
||||
|
@ -189,7 +190,7 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||
func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||
|
@ -197,9 +198,9 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
|
|||
}
|
||||
|
||||
// authenticate the request
|
||||
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||
if err != nil || !authenticated {
|
||||
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
|
||||
}
|
||||
|
||||
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
|
||||
|
@ -234,7 +235,7 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||
func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||
|
@ -242,9 +243,9 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
|
|||
}
|
||||
|
||||
// authenticate the request
|
||||
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||
if err != nil || !authenticated {
|
||||
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
|
||||
}
|
||||
|
||||
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
|
||||
|
@ -294,7 +295,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) {
|
||||
func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||
|
@ -356,6 +357,5 @@ func (p *processor) GetNodeInfo(request *http.Request) (*apimodel.Nodeinfo, gtse
|
|||
|
||||
func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator)
|
||||
posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
|
||||
return posted, err
|
||||
return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
|
@ -161,17 +162,69 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
|||
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||
}
|
||||
|
||||
if statusToDelete.GTSAuthorAccount == nil {
|
||||
statusToDelete.GTSAuthorAccount = clientMsg.OriginAccount
|
||||
}
|
||||
|
||||
// delete all attachments for this status
|
||||
for _, a := range statusToDelete.Attachments {
|
||||
if err := p.mediaProcessor.Delete(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete all mentions for this status
|
||||
for _, m := range statusToDelete.Mentions {
|
||||
if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete all notifications for this status
|
||||
if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete this status from any and all timelines
|
||||
if err := p.deleteStatusFromTimelines(statusToDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.federateStatusDelete(statusToDelete, clientMsg.OriginAccount)
|
||||
return p.federateStatusDelete(statusToDelete)
|
||||
case gtsmodel.ActivityStreamsProfile, gtsmodel.ActivityStreamsPerson:
|
||||
// DELETE ACCOUNT/PROFILE
|
||||
accountToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Account)
|
||||
if !ok {
|
||||
return errors.New("account was not parseable as *gtsmodel.Account")
|
||||
}
|
||||
|
||||
var deletedBy string
|
||||
if clientMsg.OriginAccount != nil {
|
||||
deletedBy = clientMsg.OriginAccount.ID
|
||||
}
|
||||
|
||||
return p.accountProcessor.Delete(accountToDelete, deletedBy)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: move all the below functions into federation.Federator
|
||||
|
||||
func (p *processor) federateStatus(status *gtsmodel.Status) error {
|
||||
if status.GTSAuthorAccount == nil {
|
||||
a := >smodel.Account{}
|
||||
if err := p.db.GetByID(status.AccountID, a); err != nil {
|
||||
return fmt.Errorf("federateStatus: error fetching status author account: %s", err)
|
||||
}
|
||||
status.GTSAuthorAccount = a
|
||||
}
|
||||
|
||||
// do nothing if this isn't our status
|
||||
if status.GTSAuthorAccount.Domain != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
asStatus, err := p.tc.StatusToAS(status)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
|
||||
|
@ -186,20 +239,33 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (p *processor) federateStatusDelete(status *gtsmodel.Status, originAccount *gtsmodel.Account) error {
|
||||
func (p *processor) federateStatusDelete(status *gtsmodel.Status) error {
|
||||
if status.GTSAuthorAccount == nil {
|
||||
a := >smodel.Account{}
|
||||
if err := p.db.GetByID(status.AccountID, a); err != nil {
|
||||
return fmt.Errorf("federateStatus: error fetching status author account: %s", err)
|
||||
}
|
||||
status.GTSAuthorAccount = a
|
||||
}
|
||||
|
||||
// do nothing if this isn't our status
|
||||
if status.GTSAuthorAccount.Domain != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
asStatus, err := p.tc.StatusToAS(status)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err)
|
||||
}
|
||||
|
||||
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||
outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||
return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err)
|
||||
}
|
||||
|
||||
actorIRI, err := url.Parse(originAccount.URI)
|
||||
actorIRI, err := url.Parse(status.GTSAuthorAccount.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", originAccount.URI, err)
|
||||
return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", status.GTSAuthorAccount.URI, err)
|
||||
}
|
||||
|
||||
// create a delete and set the appropriate actor on it
|
||||
|
@ -326,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)
|
||||
|
|
|
@ -21,7 +21,6 @@ package processing
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
@ -49,7 +48,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
}
|
||||
|
||||
l.Debug("will now derefence incoming status")
|
||||
if err := p.dereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
|
||||
if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
|
||||
return fmt.Errorf("error dereferencing status from federator: %s", err)
|
||||
}
|
||||
if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
|
||||
|
@ -72,7 +71,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
}
|
||||
|
||||
l.Debug("will now derefence incoming account")
|
||||
if err := p.dereferenceAccountFields(incomingAccount, "", false); err != nil {
|
||||
if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {
|
||||
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
||||
}
|
||||
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
|
||||
|
@ -105,7 +104,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
return errors.New("announce was not parseable as *gtsmodel.Status")
|
||||
}
|
||||
|
||||
if err := p.dereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
|
||||
if err := p.federator.DereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
|
||||
return fmt.Errorf("error dereferencing announce from federator: %s", err)
|
||||
}
|
||||
|
||||
|
@ -140,7 +139,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
}
|
||||
|
||||
l.Debug("will now derefence incoming account")
|
||||
if err := p.dereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
|
||||
if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
|
||||
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
||||
}
|
||||
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
|
||||
|
@ -160,6 +159,27 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
if !ok {
|
||||
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||
}
|
||||
|
||||
// delete all attachments for this status
|
||||
for _, a := range statusToDelete.Attachments {
|
||||
if err := p.mediaProcessor.Delete(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete all mentions for this status
|
||||
for _, m := range statusToDelete.Mentions {
|
||||
if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete all notifications for this status
|
||||
if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove this status from any and all timelines
|
||||
return p.deleteStatusFromTimelines(statusToDelete)
|
||||
case gtsmodel.ActivityStreamsProfile:
|
||||
// DELETE A PROFILE/ACCOUNT
|
||||
|
@ -183,299 +203,3 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dereferenceStatusFields fetches all the information we temporarily pinned to an incoming
|
||||
// federated status, back in the federating db's Create function.
|
||||
//
|
||||
// When a status comes in from the federation API, there are certain fields that
|
||||
// haven't been dereferenced yet, because we needed to provide a snappy synchronous
|
||||
// response to the caller. By the time it reaches this function though, it's being
|
||||
// processed asynchronously, so we have all the time in the world to fetch the various
|
||||
// bits and bobs that are attached to the status, and properly flesh it out, before we
|
||||
// send the status to any timelines and notify people.
|
||||
//
|
||||
// Things to dereference and fetch here:
|
||||
//
|
||||
// 1. Media attachments.
|
||||
// 2. Hashtags.
|
||||
// 3. Emojis.
|
||||
// 4. Mentions.
|
||||
// 5. Posting account.
|
||||
// 6. Replied-to-status.
|
||||
//
|
||||
// SIDE EFFECTS:
|
||||
// This function will deference all of the above, insert them in the database as necessary,
|
||||
// and attach them to the status. The status itself will not be added to the database yet,
|
||||
// that's up the caller to do.
|
||||
func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingUsername string) error {
|
||||
l := p.log.WithFields(logrus.Fields{
|
||||
"func": "dereferenceStatusFields",
|
||||
"status": fmt.Sprintf("%+v", status),
|
||||
})
|
||||
l.Debug("entering function")
|
||||
|
||||
t, err := p.federator.GetTransportForUser(requestingUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating transport: %s", err)
|
||||
}
|
||||
|
||||
// the status should have an ID by now, but just in case it doesn't let's generate one here
|
||||
// because we'll need it further down
|
||||
if status.ID == "" {
|
||||
newID, err := id.NewULIDFromTime(status.CreatedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status.ID = newID
|
||||
}
|
||||
|
||||
// 1. Media attachments.
|
||||
//
|
||||
// At this point we should know:
|
||||
// * the media type of the file we're looking for (a.File.ContentType)
|
||||
// * the blurhash (a.Blurhash)
|
||||
// * the file type (a.Type)
|
||||
// * the remote URL (a.RemoteURL)
|
||||
// This should be enough to pass along to the media processor.
|
||||
attachmentIDs := []string{}
|
||||
for _, a := range status.GTSMediaAttachments {
|
||||
l.Debugf("dereferencing attachment: %+v", a)
|
||||
|
||||
// it might have been processed elsewhere so check first if it's already in the database or not
|
||||
maybeAttachment := >smodel.MediaAttachment{}
|
||||
err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
|
||||
if err == nil {
|
||||
// we already have it in the db, dereferenced, no need to do it again
|
||||
l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
|
||||
attachmentIDs = append(attachmentIDs, maybeAttachment.ID)
|
||||
continue
|
||||
}
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
// we have a real error
|
||||
return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err)
|
||||
}
|
||||
// it just doesn't exist yet so carry on
|
||||
l.Debug("attachment doesn't exist yet, calling ProcessRemoteAttachment", a)
|
||||
deferencedAttachment, err := p.mediaHandler.ProcessRemoteAttachment(t, a, status.AccountID)
|
||||
if err != nil {
|
||||
p.log.Errorf("error dereferencing status attachment: %s", err)
|
||||
continue
|
||||
}
|
||||
l.Debugf("dereferenced attachment: %+v", deferencedAttachment)
|
||||
deferencedAttachment.StatusID = status.ID
|
||||
deferencedAttachment.Description = a.Description
|
||||
if err := p.db.Put(deferencedAttachment); err != nil {
|
||||
return fmt.Errorf("error inserting dereferenced attachment with remote url %s: %s", a.RemoteURL, err)
|
||||
}
|
||||
attachmentIDs = append(attachmentIDs, deferencedAttachment.ID)
|
||||
}
|
||||
status.Attachments = attachmentIDs
|
||||
|
||||
// 2. Hashtags
|
||||
|
||||
// 3. Emojis
|
||||
|
||||
// 4. Mentions
|
||||
// At this point, mentions should have the namestring and mentionedAccountURI set on them.
|
||||
//
|
||||
// We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
|
||||
mentions := []string{}
|
||||
for _, m := range status.GTSMentions {
|
||||
if m.ID == "" {
|
||||
mID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.ID = mID
|
||||
}
|
||||
|
||||
uri, err := url.Parse(m.MentionedAccountURI)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
|
||||
continue
|
||||
}
|
||||
|
||||
m.StatusID = status.ID
|
||||
m.OriginAccountID = status.GTSAuthorAccount.ID
|
||||
m.OriginAccountURI = status.GTSAuthorAccount.URI
|
||||
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
|
||||
// proper error
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
return fmt.Errorf("db error checking for account with uri %s", uri.String())
|
||||
}
|
||||
|
||||
// we just don't have it yet, so we should go get it....
|
||||
accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, uri)
|
||||
if err != nil {
|
||||
// we can't dereference it so just skip it
|
||||
l.Debugf("error dereferencing remote account with uri %s: %s", uri.String(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
targetAccount, err = p.tc.ASRepresentationToAccount(accountable, false)
|
||||
if err != nil {
|
||||
l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
targetAccountID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetAccount.ID = targetAccountID
|
||||
|
||||
if err := p.db.Put(targetAccount); err != nil {
|
||||
return fmt.Errorf("db error inserting account with uri %s", uri.String())
|
||||
}
|
||||
}
|
||||
|
||||
// by this point, we know the targetAccount exists in our database with an ID :)
|
||||
m.TargetAccountID = targetAccount.ID
|
||||
if err := p.db.Put(m); err != nil {
|
||||
return fmt.Errorf("error creating mention: %s", err)
|
||||
}
|
||||
mentions = append(mentions, m.ID)
|
||||
}
|
||||
status.Mentions = mentions
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error {
|
||||
l := p.log.WithFields(logrus.Fields{
|
||||
"func": "dereferenceAccountFields",
|
||||
"requestingUsername": requestingUsername,
|
||||
})
|
||||
|
||||
t, err := p.federator.GetTransportForUser(requestingUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting transport for user: %s", err)
|
||||
}
|
||||
|
||||
// fetch the header and avatar
|
||||
if err := p.fetchHeaderAndAviForAccount(account, t, refresh); err != nil {
|
||||
// if this doesn't work, just skip it -- we can do it later
|
||||
l.Debugf("error fetching header/avi for account: %s", err)
|
||||
}
|
||||
|
||||
if err := p.db.UpdateByID(account.ID, account); err != nil {
|
||||
return fmt.Errorf("error updating account in database: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error {
|
||||
if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" {
|
||||
// we can't do anything unfortunately
|
||||
return errors.New("dereferenceAnnounce: no URI to dereference")
|
||||
}
|
||||
|
||||
// check if we already have the boosted status in the database
|
||||
boostedStatus := >smodel.Status{}
|
||||
err := p.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
|
||||
if err == nil {
|
||||
// nice, we already have it so we don't actually need to dereference it from remote
|
||||
announce.Content = boostedStatus.Content
|
||||
announce.ContentWarning = boostedStatus.ContentWarning
|
||||
announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
|
||||
announce.Sensitive = boostedStatus.Sensitive
|
||||
announce.Language = boostedStatus.Language
|
||||
announce.Text = boostedStatus.Text
|
||||
announce.BoostOfID = boostedStatus.ID
|
||||
announce.Visibility = boostedStatus.Visibility
|
||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||
announce.GTSBoostedStatus = boostedStatus
|
||||
return nil
|
||||
}
|
||||
|
||||
// we don't have it so we need to dereference it
|
||||
remoteStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
// make sure we have the author account in the db
|
||||
attributedToProp := statusable.GetActivityStreamsAttributedTo()
|
||||
for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
|
||||
accountURI := iter.GetIRI()
|
||||
if accountURI == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: accountURI.String()}}, >smodel.Account{}); err == nil {
|
||||
// we already have it, fine
|
||||
continue
|
||||
}
|
||||
|
||||
// we don't have the boosted status author account yet so dereference it
|
||||
accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, accountURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote account with id %s: %s", accountURI.String(), err)
|
||||
}
|
||||
account, err := p.tc.ASRepresentationToAccount(accountable, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
|
||||
}
|
||||
|
||||
accountID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.ID = accountID
|
||||
|
||||
if err := p.db.Put(account); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
|
||||
}
|
||||
|
||||
if err := p.dereferenceAccountFields(account, requestingUsername, false); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing fields on account with id %s : %s", accountURI.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// now convert the statusable into something we can understand
|
||||
boostedStatus, err = p.tc.ASStatusToStatus(statusable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
boostedStatusID, err := id.NewULIDFromTime(boostedStatus.CreatedAt)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
boostedStatus.ID = boostedStatusID
|
||||
|
||||
if err := p.db.Put(boostedStatus); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
// now dereference additional fields straight away (we're already async here so we have time)
|
||||
if err := p.dereferenceStatusFields(boostedStatus, requestingUsername); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing status fields for status with id %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||
}
|
||||
|
||||
// update with the newly dereferenced fields
|
||||
if err := p.db.UpdateByID(boostedStatus.ID, boostedStatus); err != nil {
|
||||
return fmt.Errorf("dereferenceAnnounce: error updating dereferenced status in the db: %s", err)
|
||||
}
|
||||
|
||||
// we have everything we need!
|
||||
announce.Content = boostedStatus.Content
|
||||
announce.ContentWarning = boostedStatus.ContentWarning
|
||||
announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
|
||||
announce.Sensitive = boostedStatus.Sensitive
|
||||
announce.Language = boostedStatus.Language
|
||||
announce.Text = boostedStatus.Text
|
||||
announce.BoostOfID = boostedStatus.ID
|
||||
announce.Visibility = boostedStatus.Visibility
|
||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||
announce.GTSBoostedStatus = boostedStatus
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)
|
|||
|
||||
// process avatar if provided
|
||||
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||
_, err := p.updateAccountAvatar(form.Avatar, ia.ID)
|
||||
_, err := p.accountProcessor.UpdateAvatar(form.Avatar, ia.ID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)
|
|||
|
||||
// process header if provided
|
||||
if form.Header != nil && form.Header.Size != 0 {
|
||||
_, err := p.updateAccountHeader(form.Header, ia.ID)
|
||||
_, err := p.accountProcessor.UpdateHeader(form.Header, ia.ID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err, "error processing header")
|
||||
}
|
||||
|
|
|
@ -19,268 +19,23 @@
|
|||
package processing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
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/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
|
||||
// First check this user/account is permitted to create media
|
||||
// There's no point continuing otherwise.
|
||||
//
|
||||
// TODO: move this check to the oauth.Authed function and do it for all accounts
|
||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||
return nil, errors.New("not authorized to post new media")
|
||||
}
|
||||
|
||||
// open the attachment and extract the bytes from it
|
||||
f, err := form.File.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening attachment: %s", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading attachment: %s", err)
|
||||
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided attachment: size 0 bytes")
|
||||
}
|
||||
|
||||
// allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
|
||||
attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading attachment: %s", err)
|
||||
}
|
||||
|
||||
// now we need to add extra fields that the attachment processor doesn't know (from the form)
|
||||
// TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
|
||||
|
||||
// first description
|
||||
attachment.Description = form.Description
|
||||
|
||||
// now parse the focus parameter
|
||||
focusx, focusy, err := parseFocus(form.Focus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attachment.FileMeta.Focus.X = focusx
|
||||
attachment.FileMeta.Focus.Y = focusy
|
||||
|
||||
// prepare the frontend representation now -- if there are any errors here at least we can bail without
|
||||
// having already put something in the database and then having to clean it up again (eugh)
|
||||
mastoAttachment, err := p.tc.AttachmentToMasto(attachment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
|
||||
}
|
||||
|
||||
// now we can confidently put the attachment in the database
|
||||
if err := p.db.Put(attachment); err != nil {
|
||||
return nil, fmt.Errorf("error storing media attachment in db: %s", err)
|
||||
}
|
||||
|
||||
return &mastoAttachment, nil
|
||||
return p.mediaProcessor.Create(authed.Account, form)
|
||||
}
|
||||
|
||||
func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
|
||||
attachment := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// attachment doesn't exist
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
||||
}
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
||||
}
|
||||
|
||||
if attachment.AccountID != authed.Account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
||||
}
|
||||
|
||||
a, err := p.tc.AttachmentToMasto(attachment)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
return p.mediaProcessor.GetMedia(authed.Account, mediaAttachmentID)
|
||||
}
|
||||
|
||||
func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
|
||||
attachment := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// attachment doesn't exist
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
||||
}
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
||||
}
|
||||
|
||||
if attachment.AccountID != authed.Account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
||||
}
|
||||
|
||||
if form.Description != nil {
|
||||
attachment.Description = *form.Description
|
||||
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if form.Focus != nil {
|
||||
focusx, focusy, err := parseFocus(*form.Focus)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err)
|
||||
}
|
||||
attachment.FileMeta.Focus.X = focusx
|
||||
attachment.FileMeta.Focus.Y = focusy
|
||||
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
a, err := p.tc.AttachmentToMasto(attachment)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
return p.mediaProcessor.Update(authed.Account, mediaAttachmentID, form)
|
||||
}
|
||||
|
||||
func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
|
||||
// parse the form fields
|
||||
mediaSize, err := media.ParseMediaSize(form.MediaSize)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
|
||||
}
|
||||
|
||||
mediaType, err := media.ParseMediaType(form.MediaType)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
|
||||
}
|
||||
|
||||
spl := strings.Split(form.FileName, ".")
|
||||
if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
|
||||
}
|
||||
wantedMediaID := spl[0]
|
||||
|
||||
// get the account that owns the media and make sure it's not suspended
|
||||
acct := >smodel.Account{}
|
||||
if err := p.db.GetByID(form.AccountID, acct); err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
|
||||
}
|
||||
if !acct.SuspendedAt.IsZero() {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
|
||||
}
|
||||
|
||||
// make sure the requesting account and the media account don't block each other
|
||||
if authed.Account != nil {
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err))
|
||||
}
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// the way we store emojis is a little different from the way we store other attachments,
|
||||
// so we need to take different steps depending on the media type being requested
|
||||
content := &apimodel.Content{}
|
||||
var storagePath string
|
||||
switch mediaType {
|
||||
case media.Emoji:
|
||||
e := >smodel.Emoji{}
|
||||
if err := p.db.GetByID(wantedMediaID, e); err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
|
||||
}
|
||||
if e.Disabled {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
|
||||
}
|
||||
switch mediaSize {
|
||||
case media.Original:
|
||||
content.ContentType = e.ImageContentType
|
||||
storagePath = e.ImagePath
|
||||
case media.Static:
|
||||
content.ContentType = e.ImageStaticContentType
|
||||
storagePath = e.ImageStaticPath
|
||||
default:
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
|
||||
}
|
||||
case media.Attachment, media.Header, media.Avatar:
|
||||
a := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(wantedMediaID, a); err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
|
||||
}
|
||||
if a.AccountID != form.AccountID {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
|
||||
}
|
||||
switch mediaSize {
|
||||
case media.Original:
|
||||
content.ContentType = a.File.ContentType
|
||||
storagePath = a.File.Path
|
||||
case media.Small:
|
||||
content.ContentType = a.Thumbnail.ContentType
|
||||
storagePath = a.Thumbnail.Path
|
||||
default:
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := p.storage.RetrieveFileFrom(storagePath)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
|
||||
}
|
||||
|
||||
content.ContentLength = int64(len(bytes))
|
||||
content.Content = bytes
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func parseFocus(focus string) (focusx, focusy float32, err error) {
|
||||
if focus == "" {
|
||||
return
|
||||
}
|
||||
spl := strings.Split(focus, ",")
|
||||
if len(spl) != 2 {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
xStr := spl[0]
|
||||
yStr := spl[1]
|
||||
if xStr == "" || yStr == "" {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
fx, err := strconv.ParseFloat(xStr, 32)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
|
||||
return
|
||||
}
|
||||
if fx > 1 || fx < -1 {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
focusx = float32(fx)
|
||||
fy, err := strconv.ParseFloat(yStr, 32)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
|
||||
return
|
||||
}
|
||||
if fy > 1 || fy < -1 {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
focusy = float32(fy)
|
||||
return
|
||||
return p.mediaProcessor.GetFile(authed.Account, form)
|
||||
}
|
||||
|
|
79
internal/processing/media/create.go
Normal file
79
internal/processing/media/create.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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 media
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *processor) Create(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
|
||||
// open the attachment and extract the bytes from it
|
||||
f, err := form.File.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening attachment: %s", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading attachment: %s", err)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided attachment: size 0 bytes")
|
||||
}
|
||||
|
||||
// allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
|
||||
attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), account.ID, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading attachment: %s", err)
|
||||
}
|
||||
|
||||
// now we need to add extra fields that the attachment processor doesn't know (from the form)
|
||||
// TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
|
||||
|
||||
// first description
|
||||
attachment.Description = form.Description
|
||||
|
||||
// now parse the focus parameter
|
||||
focusx, focusy, err := parseFocus(form.Focus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attachment.FileMeta.Focus.X = focusx
|
||||
attachment.FileMeta.Focus.Y = focusy
|
||||
|
||||
// prepare the frontend representation now -- if there are any errors here at least we can bail without
|
||||
// having already put something in the database and then having to clean it up again (eugh)
|
||||
mastoAttachment, err := p.tc.AttachmentToMasto(attachment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
|
||||
}
|
||||
|
||||
// now we can confidently put the attachment in the database
|
||||
if err := p.db.Put(attachment); err != nil {
|
||||
return nil, fmt.Errorf("error storing media attachment in db: %s", err)
|
||||
}
|
||||
|
||||
return &mastoAttachment, nil
|
||||
}
|
51
internal/processing/media/delete.go
Normal file
51
internal/processing/media/delete.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode {
|
||||
a := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(mediaAttachmentID, a); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// attachment already gone
|
||||
return nil
|
||||
}
|
||||
// actual error
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
errs := []string{}
|
||||
|
||||
// delete the thumbnail from storage
|
||||
if a.Thumbnail.Path != "" {
|
||||
if err := p.storage.RemoveFileAt(a.Thumbnail.Path); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("remove thumbnail at path %s: %s", a.Thumbnail.Path, err))
|
||||
}
|
||||
}
|
||||
|
||||
// delete the file from storage
|
||||
if a.File.Path != "" {
|
||||
if err := p.storage.RemoveFileAt(a.File.Path); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("remove file at path %s: %s", a.File.Path, err))
|
||||
}
|
||||
}
|
||||
|
||||
// delete the attachment
|
||||
if err := p.db.DeleteByID(mediaAttachmentID, a); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
errs = append(errs, fmt.Sprintf("remove attachment: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return gtserror.NewErrorInternalError(fmt.Errorf("Delete: one or more errors removing attachment with id %s: %s", mediaAttachmentID, strings.Join(errs, "; ")))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
120
internal/processing/media/getfile.go
Normal file
120
internal/processing/media/getfile.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
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 media
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
)
|
||||
|
||||
func (p *processor) GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
|
||||
// parse the form fields
|
||||
mediaSize, err := media.ParseMediaSize(form.MediaSize)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
|
||||
}
|
||||
|
||||
mediaType, err := media.ParseMediaType(form.MediaType)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
|
||||
}
|
||||
|
||||
spl := strings.Split(form.FileName, ".")
|
||||
if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
|
||||
}
|
||||
wantedMediaID := spl[0]
|
||||
|
||||
// get the account that owns the media and make sure it's not suspended
|
||||
acct := >smodel.Account{}
|
||||
if err := p.db.GetByID(form.AccountID, acct); err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
|
||||
}
|
||||
if !acct.SuspendedAt.IsZero() {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
|
||||
}
|
||||
|
||||
// make sure the requesting account and the media account don't block each other
|
||||
if account != nil {
|
||||
blocked, err := p.db.Blocked(account.ID, form.AccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, account.ID, err))
|
||||
}
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, account.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// the way we store emojis is a little different from the way we store other attachments,
|
||||
// so we need to take different steps depending on the media type being requested
|
||||
content := &apimodel.Content{}
|
||||
var storagePath string
|
||||
switch mediaType {
|
||||
case media.Emoji:
|
||||
e := >smodel.Emoji{}
|
||||
if err := p.db.GetByID(wantedMediaID, e); err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
|
||||
}
|
||||
if e.Disabled {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
|
||||
}
|
||||
switch mediaSize {
|
||||
case media.Original:
|
||||
content.ContentType = e.ImageContentType
|
||||
storagePath = e.ImagePath
|
||||
case media.Static:
|
||||
content.ContentType = e.ImageStaticContentType
|
||||
storagePath = e.ImageStaticPath
|
||||
default:
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
|
||||
}
|
||||
case media.Attachment, media.Header, media.Avatar:
|
||||
a := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(wantedMediaID, a); err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
|
||||
}
|
||||
if a.AccountID != form.AccountID {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
|
||||
}
|
||||
switch mediaSize {
|
||||
case media.Original:
|
||||
content.ContentType = a.File.ContentType
|
||||
storagePath = a.File.Path
|
||||
case media.Small:
|
||||
content.ContentType = a.Thumbnail.ContentType
|
||||
storagePath = a.Thumbnail.Path
|
||||
default:
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := p.storage.RetrieveFileFrom(storagePath)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
|
||||
}
|
||||
|
||||
content.ContentLength = int64(len(bytes))
|
||||
content.Content = bytes
|
||||
return content, nil
|
||||
}
|
51
internal/processing/media/getmedia.go
Normal file
51
internal/processing/media/getmedia.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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 media
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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) GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
|
||||
attachment := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// attachment doesn't exist
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
||||
}
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
||||
}
|
||||
|
||||
if attachment.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
||||
}
|
||||
|
||||
a, err := p.tc.AttachmentToMasto(attachment)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
63
internal/processing/media/media.go
Normal file
63
internal/processing/media/media.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 media
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/blob"
|
||||
"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 media actions.
|
||||
type Processor interface {
|
||||
// Create creates a new media attachment belonging to the given account, using the request form.
|
||||
Create(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
|
||||
// Delete deletes the media attachment with the given ID, including all files pertaining to that attachment.
|
||||
Delete(mediaAttachmentID string) gtserror.WithCode
|
||||
GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
|
||||
GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode)
|
||||
Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
|
||||
}
|
||||
|
||||
type processor struct {
|
||||
tc typeutils.TypeConverter
|
||||
config *config.Config
|
||||
mediaHandler media.Handler
|
||||
storage blob.Storage
|
||||
db db.DB
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// New returns a new media processor.
|
||||
func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, storage blob.Storage, config *config.Config, log *logrus.Logger) Processor {
|
||||
return &processor{
|
||||
tc: tc,
|
||||
config: config,
|
||||
mediaHandler: mediaHandler,
|
||||
storage: storage,
|
||||
db: db,
|
||||
log: log,
|
||||
}
|
||||
}
|
70
internal/processing/media/update.go
Normal file
70
internal/processing/media/update.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
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 media
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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) Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
|
||||
attachment := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// attachment doesn't exist
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
||||
}
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
||||
}
|
||||
|
||||
if attachment.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
||||
}
|
||||
|
||||
if form.Description != nil {
|
||||
attachment.Description = *form.Description
|
||||
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if form.Focus != nil {
|
||||
focusx, focusy, err := parseFocus(*form.Focus)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err)
|
||||
}
|
||||
attachment.FileMeta.Focus.X = focusx
|
||||
attachment.FileMeta.Focus.Y = focusy
|
||||
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
a, err := p.tc.AttachmentToMasto(attachment)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
63
internal/processing/media/util.go
Normal file
63
internal/processing/media/util.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 media
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseFocus(focus string) (focusx, focusy float32, err error) {
|
||||
if focus == "" {
|
||||
return
|
||||
}
|
||||
spl := strings.Split(focus, ",")
|
||||
if len(spl) != 2 {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
xStr := spl[0]
|
||||
yStr := spl[1]
|
||||
if xStr == "" || yStr == "" {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
fx, err := strconv.ParseFloat(xStr, 32)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
|
||||
return
|
||||
}
|
||||
if fx > 1 || fx < -1 {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
focusx = float32(fx)
|
||||
fy, err := strconv.ParseFloat(yStr, 32)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
|
||||
return
|
||||
}
|
||||
if fy > 1 || fy < -1 {
|
||||
err = fmt.Errorf("improperly formatted focus %s", focus)
|
||||
return
|
||||
}
|
||||
focusy = float32(fy)
|
||||
return
|
||||
}
|
|
@ -21,6 +21,7 @@ package processing
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
@ -32,8 +33,11 @@ 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/account"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
|
||||
mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||
"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 +85,14 @@ 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)
|
||||
// AdminDomainBlocksGet returns a list of currently blocked domains.
|
||||
AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
|
||||
// AdminDomainBlockGet returns one domain block, specified by ID.
|
||||
AdminDomainBlockGet(authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
|
||||
// AdminDomainBlockDelete deletes one domain block, specified by ID, returning the deleted domain block.
|
||||
AdminDomainBlockDelete(authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode)
|
||||
|
||||
// AppCreate processes the creation of a new API application
|
||||
AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
|
||||
|
@ -154,22 +166,22 @@ type Processor interface {
|
|||
|
||||
// GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
|
||||
// before returning a JSON serializable interface to the caller.
|
||||
GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||
GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||
|
||||
// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
|
||||
// authentication before returning a JSON serializable interface to the caller.
|
||||
GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||
GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||
|
||||
// GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
|
||||
// authentication before returning a JSON serializable interface to the caller.
|
||||
GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||
GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||
|
||||
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
|
||||
// authentication before returning a JSON serializable interface to the caller.
|
||||
GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||
GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
|
||||
|
||||
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
||||
GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
||||
GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
||||
|
||||
// GetNodeInfoRel returns a well known response giving the path to node info.
|
||||
GetNodeInfoRel(request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
|
||||
|
@ -210,8 +222,11 @@ type processor struct {
|
|||
SUB-PROCESSORS
|
||||
*/
|
||||
|
||||
accountProcessor account.Processor
|
||||
adminProcessor admin.Processor
|
||||
statusProcessor status.Processor
|
||||
streamingProcessor streaming.Processor
|
||||
mediaProcessor mediaProcessor.Processor
|
||||
}
|
||||
|
||||
// NewProcessor returns a new Processor that uses the given federator and logger
|
||||
|
@ -222,6 +237,9 @@ 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)
|
||||
accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator, config, log)
|
||||
adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config, log)
|
||||
mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config, log)
|
||||
|
||||
return &processor{
|
||||
fromClientAPI: fromClientAPI,
|
||||
|
@ -238,8 +256,11 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
|
|||
db: db,
|
||||
filter: visibility.NewFilter(db, log),
|
||||
|
||||
accountProcessor: accountProcessor,
|
||||
adminProcessor: adminProcessor,
|
||||
statusProcessor: statusProcessor,
|
||||
streamingProcessor: streamingProcessor,
|
||||
mediaProcessor: mediaProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,14 +272,18 @@ func (p *processor) Start() error {
|
|||
select {
|
||||
case clientMsg := <-p.fromClientAPI:
|
||||
p.log.Infof("received message FROM client API: %+v", clientMsg)
|
||||
if err := p.processFromClientAPI(clientMsg); err != nil {
|
||||
p.log.Error(err)
|
||||
}
|
||||
go func() {
|
||||
if err := p.processFromClientAPI(clientMsg); err != nil {
|
||||
p.log.Error(err)
|
||||
}
|
||||
}()
|
||||
case federatorMsg := <-p.fromFederator:
|
||||
p.log.Infof("received message FROM federator: %+v", federatorMsg)
|
||||
if err := p.processFromFederator(federatorMsg); err != nil {
|
||||
p.log.Error(err)
|
||||
}
|
||||
go func() {
|
||||
if err := p.processFromFederator(federatorMsg); err != nil {
|
||||
p.log.Error(err)
|
||||
}
|
||||
}()
|
||||
case <-p.stop:
|
||||
break DistLoop
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve
|
|||
}
|
||||
|
||||
// properly dereference everything in the status (media attachments etc)
|
||||
if err := p.dereferenceStatusFields(status, authed.Account.Username); err != nil {
|
||||
if err := p.federator.DereferenceStatusFields(status, authed.Account.Username); err != nil {
|
||||
return nil, fmt.Errorf("error dereferencing status fields: %s", err)
|
||||
}
|
||||
|
||||
|
@ -223,7 +223,7 @@ func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve
|
|||
return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
|
||||
}
|
||||
|
||||
if err := p.dereferenceAccountFields(account, authed.Account.Username, false); err != nil {
|
||||
if err := p.federator.DereferenceAccountFields(account, authed.Account.Username, false); err != nil {
|
||||
return nil, fmt.Errorf("searchAccountByURI: error further dereferencing account with uri %s: %s", uri.String(), err)
|
||||
}
|
||||
|
||||
|
@ -301,7 +301,7 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r
|
|||
}
|
||||
|
||||
// properly dereference all the fields on the account immediately
|
||||
if err := p.dereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
|
||||
if err := p.federator.DereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
|
||||
return nil, fmt.Errorf("searchAccountByMention: error dereferencing fields on account with uri %s: %s", acctURI.String(), err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a
|
|||
APActivityType: gtsmodel.ActivityStreamsDelete,
|
||||
GTSModel: targetStatus,
|
||||
OriginAccount: account,
|
||||
TargetAccount: account,
|
||||
}
|
||||
|
||||
return mastoStatus, nil
|
|
@ -1,135 +0,0 @@
|
|||
/*
|
||||
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 processing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
)
|
||||
|
||||
/*
|
||||
HELPER FUNCTIONS
|
||||
*/
|
||||
|
||||
// TODO: try to combine the below two functions because this is a lot of code repetition.
|
||||
|
||||
// updateAccountAvatar does the dirty work of checking the avatar part of an account update form,
|
||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||
// the account's new avatar image.
|
||||
func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
|
||||
var err error
|
||||
if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
|
||||
err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
|
||||
return nil, err
|
||||
}
|
||||
f, err := avatar.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided avatar: %s", err)
|
||||
}
|
||||
|
||||
// extract the bytes
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided avatar: %s", err)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided avatar: size 0 bytes")
|
||||
}
|
||||
|
||||
// do the setting
|
||||
avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error processing avatar: %s", err)
|
||||
}
|
||||
|
||||
return avatarInfo, f.Close()
|
||||
}
|
||||
|
||||
// updateAccountHeader does the dirty work of checking the header part of an account update form,
|
||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||
// the account's new header image.
|
||||
func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
|
||||
var err error
|
||||
if int(header.Size) > p.config.MediaConfig.MaxImageSize {
|
||||
err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
|
||||
return nil, err
|
||||
}
|
||||
f, err := header.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided header: %s", err)
|
||||
}
|
||||
|
||||
// extract the bytes
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided header: %s", err)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided header: size 0 bytes")
|
||||
}
|
||||
|
||||
// do the setting
|
||||
headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error processing header: %s", err)
|
||||
}
|
||||
|
||||
return headerInfo, f.Close()
|
||||
}
|
||||
|
||||
// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
|
||||
// on behalf of requestingUsername.
|
||||
//
|
||||
// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
|
||||
//
|
||||
// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
|
||||
// to reflect the creation of these new attachments.
|
||||
func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
|
||||
if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
|
||||
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.AvatarRemoteURL,
|
||||
Avatar: true,
|
||||
}, targetAccount.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing avatar for user: %s", err)
|
||||
}
|
||||
targetAccount.AvatarMediaAttachmentID = a.ID
|
||||
}
|
||||
|
||||
if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
|
||||
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.HeaderRemoteURL,
|
||||
Header: true,
|
||||
}, targetAccount.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing header for user: %s", err)
|
||||
}
|
||||
targetAccount.HeaderMediaAttachmentID = a.ID
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -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, export bool) (*model.DomainBlock, error)
|
||||
|
||||
/*
|
||||
FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL
|
||||
|
|
|
@ -644,3 +644,23 @@ func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notifi
|
|||
Status: mastoStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *converter) DomainBlockToMasto(b *gtsmodel.DomainBlock, export bool) (*model.DomainBlock, error) {
|
||||
|
||||
domainBlock := &model.DomainBlock{
|
||||
Domain: b.Domain,
|
||||
PublicComment: b.PublicComment,
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -66,8 +66,8 @@ const (
|
|||
// APRequestingActorIRI can be used to set and retrieve the actor of an incoming federation request.
|
||||
// This will usually be the owner of whatever activity is being posted.
|
||||
APRequestingActorIRI APContextKey = "requestingActorIRI"
|
||||
// APRequestingPublicKeyID can be used to set and retrieve the public key ID of an incoming federation request.
|
||||
APRequestingPublicKeyID APContextKey = "requestingPublicKeyID"
|
||||
// APRequestingPublicKeyVerifier can be used to set and retrieve the public key verifier of an incoming federation request.
|
||||
APRequestingPublicKeyVerifier APContextKey = "requestingPublicKeyVerifier"
|
||||
// APFromFederatorChanKey can be used to pass a pointer to the fromFederator channel into the federator for use in callbacks.
|
||||
APFromFederatorChanKey APContextKey = "fromFederatorChan"
|
||||
)
|
||||
|
|
|
@ -19,9 +19,20 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount
|
|||
relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus)
|
||||
if err != nil {
|
||||
l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err)
|
||||
return false, fmt.Errorf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err)
|
||||
}
|
||||
targetAccount := relevantAccounts.StatusAuthor
|
||||
|
||||
domainBlocked, err := f.domainBlockedRelevant(relevantAccounts)
|
||||
if err != nil {
|
||||
l.Debugf("error checking domain block: %s", err)
|
||||
return false, fmt.Errorf("error checking domain block: %s", err)
|
||||
}
|
||||
|
||||
if domainBlocked {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
targetAccount := relevantAccounts.StatusAuthor
|
||||
// if target account is suspended then don't show the status
|
||||
if !targetAccount.SuspendedAt.IsZero() {
|
||||
l.Trace("target account suspended at is not zero")
|
||||
|
@ -123,8 +134,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount
|
|||
}
|
||||
|
||||
// status boosts accounts id
|
||||
if relevantAccounts.BoostedAccount != nil {
|
||||
if blocked, err := f.db.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil {
|
||||
if relevantAccounts.BoostedStatusAuthor != nil {
|
||||
if blocked, err := f.db.Blocked(relevantAccounts.BoostedStatusAuthor.ID, requestingAccount.ID); err != nil {
|
||||
return false, err
|
||||
} else if blocked {
|
||||
l.Trace("a block exists between requesting account and boosted account")
|
||||
|
@ -152,6 +163,16 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount
|
|||
}
|
||||
}
|
||||
|
||||
// boost mentions accounts
|
||||
for _, a := range relevantAccounts.BoostedMentionedAccounts {
|
||||
if blocked, err := f.db.Blocked(a.ID, requestingAccount.ID); err != nil {
|
||||
return false, err
|
||||
} else if blocked {
|
||||
l.Trace("a block exists between requesting account and a boosted mentioned account")
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// if the requesting account is mentioned in the status it should always be visible
|
||||
for _, acct := range relevantAccounts.MentionedAccounts {
|
||||
if acct.ID == requestingAccount.ID {
|
||||
|
|
|
@ -3,12 +3,14 @@ package visibility
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*relevantAccounts, error) {
|
||||
accounts := &relevantAccounts{
|
||||
MentionedAccounts: []*gtsmodel.Account{},
|
||||
MentionedAccounts: []*gtsmodel.Account{},
|
||||
BoostedMentionedAccounts: []*gtsmodel.Account{},
|
||||
}
|
||||
|
||||
// get the author account
|
||||
|
@ -30,29 +32,6 @@ func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (
|
|||
accounts.ReplyToAccount = repliedToAccount
|
||||
}
|
||||
|
||||
// get the boosted account from the status and add it to the pile
|
||||
if targetStatus.BoostOfID != "" {
|
||||
// retrieve the boosted status first
|
||||
boostedStatus := >smodel.Status{}
|
||||
if err := f.db.GetByID(targetStatus.BoostOfID, boostedStatus); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s", targetStatus.BoostOfID, err)
|
||||
}
|
||||
boostedAccount := >smodel.Account{}
|
||||
if err := f.db.GetByID(boostedStatus.AccountID, boostedAccount); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s", boostedStatus.AccountID, err)
|
||||
}
|
||||
accounts.BoostedAccount = boostedAccount
|
||||
|
||||
// the boosted status might be a reply to another account so we should get that too
|
||||
if boostedStatus.InReplyToAccountID != "" {
|
||||
boostedStatusRepliedToAccount := >smodel.Account{}
|
||||
if err := f.db.GetByID(boostedStatus.InReplyToAccountID, boostedStatusRepliedToAccount); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s", boostedStatus.InReplyToAccountID, err)
|
||||
}
|
||||
accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount
|
||||
}
|
||||
}
|
||||
|
||||
// now get all accounts with IDs that are mentioned in the status
|
||||
for _, mentionID := range targetStatus.Mentions {
|
||||
|
||||
|
@ -68,14 +47,145 @@ func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (
|
|||
accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount)
|
||||
}
|
||||
|
||||
// get the boosted account from the status and add it to the pile
|
||||
if targetStatus.BoostOfID != "" {
|
||||
// retrieve the boosted status first
|
||||
boostedStatus := >smodel.Status{}
|
||||
if err := f.db.GetByID(targetStatus.BoostOfID, boostedStatus); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s", targetStatus.BoostOfID, err)
|
||||
}
|
||||
boostedAccount := >smodel.Account{}
|
||||
if err := f.db.GetByID(boostedStatus.AccountID, boostedAccount); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s", boostedStatus.AccountID, err)
|
||||
}
|
||||
accounts.BoostedStatusAuthor = boostedAccount
|
||||
|
||||
// the boosted status might be a reply to another account so we should get that too
|
||||
if boostedStatus.InReplyToAccountID != "" {
|
||||
boostedStatusRepliedToAccount := >smodel.Account{}
|
||||
if err := f.db.GetByID(boostedStatus.InReplyToAccountID, boostedStatusRepliedToAccount); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s", boostedStatus.InReplyToAccountID, err)
|
||||
}
|
||||
accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount
|
||||
}
|
||||
|
||||
// now get all accounts with IDs that are mentioned in the status
|
||||
for _, mentionID := range boostedStatus.Mentions {
|
||||
mention := >smodel.Mention{}
|
||||
if err := f.db.GetByID(mentionID, mention); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boosted mention with id %s: %s", mentionID, err)
|
||||
}
|
||||
|
||||
mentionedAccount := >smodel.Account{}
|
||||
if err := f.db.GetByID(mention.TargetAccountID, mentionedAccount); err != nil {
|
||||
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boosted mentioned account: %s", err)
|
||||
}
|
||||
accounts.BoostedMentionedAccounts = append(accounts.BoostedMentionedAccounts, mentionedAccount)
|
||||
}
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status.
|
||||
type relevantAccounts struct {
|
||||
StatusAuthor *gtsmodel.Account
|
||||
ReplyToAccount *gtsmodel.Account
|
||||
BoostedAccount *gtsmodel.Account
|
||||
// Who wrote the status
|
||||
StatusAuthor *gtsmodel.Account
|
||||
// Who is the status replying to
|
||||
ReplyToAccount *gtsmodel.Account
|
||||
// Which accounts are mentioned (tagged) in the status
|
||||
MentionedAccounts []*gtsmodel.Account
|
||||
// Who authed the boosted status
|
||||
BoostedStatusAuthor *gtsmodel.Account
|
||||
// If the boosted status replies to another account, who does it reply to?
|
||||
BoostedReplyToAccount *gtsmodel.Account
|
||||
MentionedAccounts []*gtsmodel.Account
|
||||
// Who is mentioned (tagged) in the boosted status
|
||||
BoostedMentionedAccounts []*gtsmodel.Account
|
||||
}
|
||||
|
||||
// blockedDomain checks whether the given domain is blocked by us or not
|
||||
func (f *filter) blockedDomain(host string) (bool, error) {
|
||||
b := >smodel.DomainBlock{}
|
||||
err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b)
|
||||
if err == nil {
|
||||
// block exists
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// there are no entries so there's no block
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// there's an actual error
|
||||
return false, err
|
||||
}
|
||||
|
||||
// domainBlockedRelevant checks through all relevant accounts attached to a status
|
||||
// to make sure none of them are domain blocked by this instance.
|
||||
//
|
||||
// Will return true+nil if there's a block, false+nil if there's no block, or
|
||||
// an error if something goes wrong.
|
||||
func (f *filter) domainBlockedRelevant(r *relevantAccounts) (bool, error) {
|
||||
if r.StatusAuthor != nil {
|
||||
b, err := f.blockedDomain(r.StatusAuthor.Domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.ReplyToAccount != nil {
|
||||
b, err := f.blockedDomain(r.ReplyToAccount.Domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range r.MentionedAccounts {
|
||||
b, err := f.blockedDomain(a.Domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.BoostedStatusAuthor != nil {
|
||||
b, err := f.blockedDomain(r.BoostedStatusAuthor.Domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.BoostedReplyToAccount != nil {
|
||||
b, err := f.blockedDomain(r.BoostedReplyToAccount.Domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range r.BoostedMentionedAccounts {
|
||||
b, err := f.blockedDomain(a.Domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -19,12 +19,13 @@
|
|||
package testrig
|
||||
|
||||
import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/blob"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
)
|
||||
|
||||
// NewTestFederator returns a federator with the given database and (mock!!) transport controller.
|
||||
func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator {
|
||||
return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db))
|
||||
func NewTestFederator(db db.DB, tc transport.Controller, storage blob.Storage) federation.Federator {
|
||||
return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db), NewTestMediaHandler(db, storage))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue