mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-19 15:01:01 +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
|
@ -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