mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:32:24 -05: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.db = testrig.NewTestDB() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.accountModule = account.New(suite.config, suite.processor, suite.log).(*account.Module) | 	suite.accountModule = account.New(suite.config, suite.processor, suite.log).(*account.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	testrig.StandardDBSetup(suite.db) | ||||||
|  |  | ||||||
|  | @ -82,7 +82,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 		maxID = maxIDString | 		maxID = maxIDString | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	pinned := false | 	pinnedOnly := false | ||||||
| 	pinnedString := c.Query(PinnedKey) | 	pinnedString := c.Query(PinnedKey) | ||||||
| 	if pinnedString != "" { | 	if pinnedString != "" { | ||||||
| 		i, err := strconv.ParseBool(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"}) | 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"}) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		pinned = i | 		pinnedOnly = i | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mediaOnly := false | 	mediaOnly := false | ||||||
|  | @ -106,7 +106,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 		mediaOnly = i | 		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 { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error from processor account statuses get: %s", errWithCode) | 		l.Debugf("error from processor account statuses get: %s", errWithCode) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  |  | ||||||
|  | @ -29,10 +29,19 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	// BasePath is the base API path for this module | 	// BasePath is the base API path for this module. | ||||||
| 	BasePath = "/api/v1/admin" | 	BasePath = "/api/v1/admin" | ||||||
| 	// EmojiPath is used for posting/deleting custom emojis | 	// EmojiPath is used for posting/deleting custom emojis. | ||||||
| 	EmojiPath = BasePath + "/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) | // 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 | // Route attaches all routes from this module to the given router | ||||||
| func (m *Module) Route(r router.Router) error { | func (m *Module) Route(r router.Router) error { | ||||||
| 	r.AttachHandler(http.MethodPost, EmojiPath, m.emojiCreatePOSTHandler) | 	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 | 	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.db = testrig.NewTestDB() | ||||||
| 	suite.log = testrig.NewTestLog() | 	suite.log = testrig.NewTestLog() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"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) { | func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { | ||||||
| 	l := m.log.WithField("func", "InstanceUpdatePATCHHandler") | 	l := m.log.WithField("func", "InstanceUpdatePATCHHandler") | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
|  |  | ||||||
|  | @ -84,7 +84,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() { | ||||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | 	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) | 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 
 | 
 | ||||||
| 	// setup module being tested | 	// setup module being tested | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ func (suite *StatusBoostTestSuite) SetupTest() { | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	testrig.StandardDBSetup(suite.db) | ||||||
|  |  | ||||||
|  | @ -57,7 +57,7 @@ func (suite *StatusCreateTestSuite) SetupTest() { | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	testrig.StandardDBSetup(suite.db) | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ func (suite *StatusFaveTestSuite) SetupTest() { | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	testrig.StandardDBSetup(suite.db) | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ func (suite *StatusFavedByTestSuite) SetupTest() { | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	testrig.StandardDBSetup(suite.db) | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ func (suite *StatusGetTestSuite) SetupTest() { | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	testrig.StandardDBSetup(suite.db) | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ func (suite *StatusUnfaveTestSuite) SetupTest() { | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	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 | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"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. | // 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) | 	l.Tracef("negotiated format: %s", format) | ||||||
| 
 | 
 | ||||||
| 	// make a copy of the context to pass along so we don't break anything | 	// transfer the signature verifier from the gin context to the request context | ||||||
| 	cp := c.Copy() | 	ctx := c.Request.Context() | ||||||
| 	user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well | 	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 { | 	if err != nil { | ||||||
| 		l.Info(err.Error()) | 		l.Info(err.Error()) | ||||||
| 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  |  | ||||||
|  | @ -19,10 +19,12 @@ | ||||||
| package user | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"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. | // 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) | 	l.Tracef("negotiated format: %s", format) | ||||||
| 
 | 
 | ||||||
| 	// make a copy of the context to pass along so we don't break anything | 	// transfer the signature verifier from the gin context to the request context | ||||||
| 	cp := c.Copy() | 	ctx := c.Request.Context() | ||||||
| 	user, err := m.processor.GetFediFollowing(requestedUsername, cp.Request) // handles auth as well | 	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 { | 	if err != nil { | ||||||
| 		l.Info(err.Error()) | 		l.Info(err.Error()) | ||||||
| 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  |  | ||||||
|  | @ -19,11 +19,13 @@ | ||||||
| package user | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. | // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. | ||||||
|  | @ -40,7 +42,14 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) { | ||||||
| 		return | 		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 err != nil { | ||||||
| 		if withCode, ok := err.(gtserror.WithCode); ok { | 		if withCode, ok := err.(gtserror.WithCode); ok { | ||||||
| 			l.Debug(withCode.Error()) | 			l.Debug(withCode.Error()) | ||||||
|  |  | ||||||
|  | @ -1,10 +1,12 @@ | ||||||
| package user | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key. | // 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) | 	l.Tracef("negotiated format: %s", format) | ||||||
| 
 | 
 | ||||||
| 	// make a copy of the context to pass along so we don't break anything | 	// transfer the signature verifier from the gin context to the request context | ||||||
| 	cp := c.Copy() | 	ctx := c.Request.Context() | ||||||
| 	user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetFediUser handles auth as well | 	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 { | 	if err != nil { | ||||||
| 		l.Info(err.Error()) | 		l.Info(err.Error()) | ||||||
| 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  |  | ||||||
|  | @ -1,10 +1,12 @@ | ||||||
| package user | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"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. | // 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) | 	l.Tracef("negotiated format: %s", format) | ||||||
| 
 | 
 | ||||||
| 	// make a copy of the context to pass along so we don't break anything | 	// transfer the signature verifier from the gin context to the request context | ||||||
| 	cp := c.Copy() | 	ctx := c.Request.Context() | ||||||
| 	status, err := m.processor.GetFediStatus(requestedUsername, requestedStatusID, cp.Request) // handles auth as well | 	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 { | 	if err != nil { | ||||||
| 		l.Info(err.Error()) | 		l.Info(err.Error()) | ||||||
| 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  |  | ||||||
|  | @ -19,10 +19,12 @@ | ||||||
| package user | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // UsersGETHandler should be served at https://example.org/users/:username. | // 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) | 	l.Tracef("negotiated format: %s", format) | ||||||
| 
 | 
 | ||||||
| 	// make a copy of the context to pass along so we don't break anything | 	// transfer the signature verifier from the gin context to the request context | ||||||
| 	cp := c.Copy() | 	ctx := c.Request.Context() | ||||||
| 	user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetFediUser handles auth as well | 	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 { | 	if err != nil { | ||||||
| 		l.Info(err.Error()) | 		l.Info(err.Error()) | ||||||
| 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  |  | ||||||
|  | @ -42,7 +42,7 @@ func (suite *UserGetTestSuite) SetupTest() { | ||||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.log = testrig.NewTestLog() | 	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.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
| 	suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module) | 	suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db) | 	testrig.StandardDBSetup(suite.db) | ||||||
|  | @ -98,7 +98,7 @@ func (suite *UserGetTestSuite) TestGetUser() { | ||||||
| 		}, nil | 		}, nil | ||||||
| 	})) | 	})) | ||||||
| 	// get this transport controller embedded right in the user module we're testing | 	// 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) | 	processor := testrig.NewTestProcessor(suite.db, suite.storage, federator) | ||||||
| 	userModule := user.New(suite.config, processor, suite.log).(*user.Module) | 	userModule := user.New(suite.config, processor, suite.log).(*user.Module) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,12 +19,14 @@ | ||||||
| package webfinger | package webfinger | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"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 | // 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 | 		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 { | 	if err != nil { | ||||||
| 		l.Debugf("aborting request with an error: %s", err.Error()) | 		l.Debugf("aborting request with an error: %s", err.Error()) | ||||||
| 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -33,18 +34,21 @@ const robotsPath = "/robots.txt" | ||||||
| type Module struct { | type Module struct { | ||||||
| 	config *config.Config | 	config *config.Config | ||||||
| 	log    *logrus.Logger | 	log    *logrus.Logger | ||||||
|  | 	db     db.DB | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // New returns a new security module | // 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{ | 	return &Module{ | ||||||
| 		config: config, | 		config: config, | ||||||
| 		log:    log, | 		log:    log, | ||||||
|  | 		db:     db, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Route attaches security middleware to the given router | // Route attaches security middleware to the given router | ||||||
| func (m *Module) Route(s router.Router) error { | func (m *Module) Route(s router.Router) error { | ||||||
|  | 	s.AttachMiddleware(m.SignatureCheck) | ||||||
| 	s.AttachMiddleware(m.FlocBlock) | 	s.AttachMiddleware(m.FlocBlock) | ||||||
| 	s.AttachMiddleware(m.ExtraHeaders) | 	s.AttachMiddleware(m.ExtraHeaders) | ||||||
| 	s.AttachMiddleware(m.UserAgentBlock) | 	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) | 	mediaHandler := media.New(c, dbService, storageBackend, log) | ||||||
| 	oauthServer := oauth.New(dbService, log) | 	oauthServer := oauth.New(dbService, log) | ||||||
| 	transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, 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) | 	processor := processing.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, timelineManager, dbService, log) | ||||||
| 	if err := processor.Start(); err != nil { | 	if err := processor.Start(); err != nil { | ||||||
| 		return fmt.Errorf("error starting processor: %s", err) | 		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) | 	fileServerModule := fileserver.New(c, processor, log) | ||||||
| 	adminModule := admin.New(c, processor, log) | 	adminModule := admin.New(c, processor, log) | ||||||
| 	statusModule := status.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) | 	streamingModule := streaming.New(c, processor, log) | ||||||
| 
 | 
 | ||||||
| 	apis := []api.ClientModule{ | 	apis := []api.ClientModule{ | ||||||
|  |  | ||||||
|  | @ -57,7 +57,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log | ||||||
| 			Body:       r, | 			Body:       r, | ||||||
| 		}, nil | 		}, nil | ||||||
| 	})) | 	})) | ||||||
| 	federator := testrig.NewTestFederator(dbService, transportController) | 	federator := testrig.NewTestFederator(dbService, transportController, storageBackend) | ||||||
| 
 | 
 | ||||||
| 	processor := testrig.NewTestProcessor(dbService, storageBackend, federator) | 	processor := testrig.NewTestProcessor(dbService, storageBackend, federator) | ||||||
| 	if err := processor.Start(); err != nil { | 	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) | 	fileServerModule := fileserver.New(c, processor, log) | ||||||
| 	adminModule := admin.New(c, processor, log) | 	adminModule := admin.New(c, processor, log) | ||||||
| 	statusModule := status.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) | 	streamingModule := streaming.New(c, processor, log) | ||||||
| 
 | 
 | ||||||
| 	apis := []api.ClientModule{ | 	apis := []api.ClientModule{ | ||||||
|  |  | ||||||
|  | @ -65,11 +65,6 @@ type DB interface { | ||||||
| 	// In case of no entries, a 'no entries' error will be returned | 	// In case of no entries, a 'no entries' error will be returned | ||||||
| 	GetWhere(where []Where, i interface{}) error | 	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. | 	// 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. | 	// 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 | 	// 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 is a shortcut for the common action of counting statuses produced by accountID. | ||||||
| 	CountStatusesByAccountID(accountID string) (int, error) | 	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 | 	// 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! | 	// be very memory intensive so you probably shouldn't do this! | ||||||
| 	// In case of no entries, a 'no entries' error will be returned | 	// 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. | 	// 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. | 	// 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 returns the number of known instances known that the given domain federates with. | ||||||
| 	GetDomainCountForInstance(domain string) (int, error) | 	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 | 		USEFUL CONVERSION FUNCTIONS | ||||||
| 	*/ | 	*/ | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package pg | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/go-pg/pg/v10" | 	"github.com/go-pg/pg/v10" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -50,3 +51,33 @@ func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error) | ||||||
| 
 | 
 | ||||||
| 	return q.Count() | 	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 | 	return count, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) error { | func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) { | ||||||
| 	q := ps.conn.Model(statuses).Order("created_at DESC") | 	ps.log.Debugf("getting statuses for account %s", accountID) | ||||||
|  | 	statuses := []*gtsmodel.Status{} | ||||||
|  | 
 | ||||||
|  | 	q := ps.conn.Model(&statuses).Order("id DESC") | ||||||
| 	if accountID != "" { | 	if accountID != "" { | ||||||
| 		q = q.Where("account_id = ?", accountID) | 		q = q.Where("account_id = ?", accountID) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if limit != 0 { | 	if limit != 0 { | ||||||
| 		q = q.Limit(limit) | 		q = q.Limit(limit) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if excludeReplies { | 	if excludeReplies { | ||||||
| 		q = q.Where("? IS NULL", pg.Ident("in_reply_to_id")) | 		q = q.Where("? IS NULL", pg.Ident("in_reply_to_id")) | ||||||
| 	} | 	} | ||||||
| 	if pinned { | 
 | ||||||
|  | 	if pinnedOnly { | ||||||
| 		q = q.Where("pinned = ?", true) | 		q = q.Where("pinned = ?", true) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if mediaOnly { | 	if mediaOnly { | ||||||
| 		q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) { | 		q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) { | ||||||
| 			return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil | 			return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if maxID != "" { | 	if maxID != "" { | ||||||
| 		s := >smodel.Status{} | 		q = q.Where("id < ?", maxID) | ||||||
| 		if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		q = q.Where("status.created_at < ?", s.CreatedAt) |  | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if err := q.Select(); err != nil { | 	if err := q.Select(); err != nil { | ||||||
| 		if err == pg.ErrNoRows { | 		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 { | func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { | ||||||
|  |  | ||||||
|  | @ -25,7 +25,6 @@ import ( | ||||||
| 	"encoding/pem" | 	"encoding/pem" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | @ -35,6 +34,7 @@ import ( | ||||||
| 	"github.com/go-fed/httpsig" | 	"github.com/go-fed/httpsig" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"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. | // 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. | // 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 publicKey interface{} | ||||||
| 	var pkOwnerURI *url.URL | 	var pkOwnerURI *url.URL | ||||||
| 	var err error | 	var err error | ||||||
| 
 | 
 | ||||||
| 	// set this extra field for signature validation | 	// thanks to signaturecheck.go in the security package, we should already have a signature verifier set on the context | ||||||
| 	r.Header.Set("host", f.config.Host) | 	vi := ctx.Value(util.APRequestingPublicKeyVerifier) | ||||||
| 
 | 	if vi == nil { | ||||||
| 	verifier, err := httpsig.NewVerifier(r) | 		l.Debug("request wasn't signed") | ||||||
| 	if err != nil { | 		return nil, false, nil // request wasn't signed | ||||||
| 		return nil, fmt.Errorf("could not create http sig verifier: %s", err) | 	} | ||||||
|  | 
 | ||||||
|  | 	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()) | 	requestingPublicKeyID, err := url.Parse(verifier.KeyId()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("could not parse key id into a url: %s", err) | 		l.Debug("couldn't parse public key URL") | ||||||
| 	} | 		return nil, false, nil // couldn't parse the public key ID url | ||||||
| 
 |  | ||||||
| 	// 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) |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	requestingRemoteAccount := >smodel.Account{} | 	requestingRemoteAccount := >smodel.Account{} | ||||||
|  | @ -152,12 +148,12 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht | ||||||
| 		// LOCAL ACCOUNT REQUEST | 		// LOCAL ACCOUNT REQUEST | ||||||
| 		// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing | 		// 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 { | 		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 | 		publicKey = requestingLocalAccount.PublicKey | ||||||
| 		pkOwnerURI, err = url.Parse(requestingLocalAccount.URI) | 		pkOwnerURI, err = url.Parse(requestingLocalAccount.URI) | ||||||
| 		if err != nil { | 		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 { | 	} 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 | 		// REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY | ||||||
|  | @ -165,7 +161,7 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht | ||||||
| 		publicKey = requestingRemoteAccount.PublicKey | 		publicKey = requestingRemoteAccount.PublicKey | ||||||
| 		pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI) | 		pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI) | ||||||
| 		if err != nil { | 		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 { | 	} else { | ||||||
| 		// REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY | 		// 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 | 		// so we need to authenticate the request properly by dereferencing the remote key | ||||||
| 		transport, err := f.GetTransportForUser(requestedUsername) | 		transport, err := f.GetTransportForUser(requestedUsername) | ||||||
| 		if err != nil { | 		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. | 		// The actual http call to the remote server is made right here in the Dereference function. | ||||||
| 		b, err := transport.Dereference(context.Background(), requestingPublicKeyID) | 		b, err := transport.Dereference(context.Background(), requestingPublicKeyID) | ||||||
| 		if err != nil { | 		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 | 		// if the key isn't in the response, we can't authenticate the request | ||||||
| 		requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID) | 		requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID) | ||||||
| 		if err != nil { | 		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 | 		// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey | ||||||
| 		pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() | 		pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() | ||||||
| 		if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { | 		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 | 		// and decode the PEM so that we can parse it as a golang public key | ||||||
| 		pubKeyPem := pkPemProp.Get() | 		pubKeyPem := pkPemProp.Get() | ||||||
| 		block, _ := pem.Decode([]byte(pubKeyPem)) | 		block, _ := pem.Decode([]byte(pubKeyPem)) | ||||||
| 		if block == nil || block.Type != "PUBLIC KEY" { | 		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) | 		publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) | ||||||
| 		if err != nil { | 		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 | 		// all good! we just need the URI of the key owner to return | ||||||
| 		pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() | 		pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() | ||||||
| 		if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { | 		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() | 		pkOwnerURI = pkOwnerProp.GetIRI() | ||||||
| 	} | 	} | ||||||
| 	if publicKey == nil { | 	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! | 	// do the actual authentication here! | ||||||
| 	algo := httpsig.RSA_SHA256 // TODO: make this more robust | 	algo := httpsig.RSA_SHA256 // TODO: make this more robust | ||||||
| 	if err := verifier.Verify(publicKey, algo); err != nil { | 	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 | 	return pkOwnerURI, true, 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 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,7 +9,11 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/streams" | 	"github.com/go-fed/activity/streams" | ||||||
| 	"github.com/go-fed/activity/streams/vocab" | 	"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/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -17,6 +21,10 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u | ||||||
| 	f.startHandshake(username, remoteAccountID) | 	f.startHandshake(username, remoteAccountID) | ||||||
| 	defer f.stopHandshake(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) | 	transport, err := f.GetTransportForUser(username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("transport err: %s", err) | 		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) { | 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) | 	transport, err := f.GetTransportForUser(username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("transport err: %s", err) | 		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) { | 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) | 	transport, err := f.GetTransportForUser(username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("transport err: %s", err) | 		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) | 	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) | 		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 { | 	if err != nil { | ||||||
| 		l.Debugf("request not authenticated: %s", err) | 		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 | 	// 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 { | 	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{} | 		a := >smodel.Account{} | ||||||
| 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil { | 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil { | ||||||
| 			_, ok := err.(db.ErrNoEntries) | 			_, ok := err.(db.ErrNoEntries) | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
| package federation | package federation | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"context" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
|  | @ -29,6 +29,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" | 	"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/transport" | 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
| ) | ) | ||||||
|  | @ -41,7 +42,13 @@ type Federator interface { | ||||||
| 	FederatingDB() federatingdb.DB | 	FederatingDB() federatingdb.DB | ||||||
| 	// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources. | 	// 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. | 	// 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 | 	// 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. | 	// account, or an error if it doesn't exist or can't be retrieved. | ||||||
| 	FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error) | 	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 | 	// 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 | 	// 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) | 	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. | 	// GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username. | ||||||
| 	// This can be used for making signed http requests. | 	// This can be used for making signed http requests. | ||||||
| 	// | 	// | ||||||
|  | @ -72,6 +85,7 @@ type federator struct { | ||||||
| 	clock               pub.Clock | 	clock               pub.Clock | ||||||
| 	typeConverter       typeutils.TypeConverter | 	typeConverter       typeutils.TypeConverter | ||||||
| 	transportController transport.Controller | 	transportController transport.Controller | ||||||
|  | 	mediaHandler        media.Handler | ||||||
| 	actor               pub.FederatingActor | 	actor               pub.FederatingActor | ||||||
| 	log                 *logrus.Logger | 	log                 *logrus.Logger | ||||||
| 	handshakes          map[string][]*url.URL | 	handshakes          map[string][]*url.URL | ||||||
|  | @ -79,7 +93,7 @@ type federator struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewFederator returns a new federator | // 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{} | 	clock := &Clock{} | ||||||
| 	f := &federator{ | 	f := &federator{ | ||||||
|  | @ -89,6 +103,7 @@ func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController tr | ||||||
| 		clock:               &Clock{}, | 		clock:               &Clock{}, | ||||||
| 		typeConverter:       typeConverter, | 		typeConverter:       typeConverter, | ||||||
| 		transportController: transportController, | 		transportController: transportController, | ||||||
|  | 		mediaHandler:        mediaHandler, | ||||||
| 		log:                 log, | 		log:                 log, | ||||||
| 		handshakeSync:       &sync.Mutex{}, | 		handshakeSync:       &sync.Mutex{}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -89,7 +89,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() { | ||||||
| 		return nil, nil | 		return nil, nil | ||||||
| 	})) | 	})) | ||||||
| 	// setup module being tested | 	// 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 | 	// setup request | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  | @ -155,7 +155,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
| 	// now setup module being tested, with the mock transport controller | 	// 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 | 	// setup request | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  |  | ||||||
|  | @ -30,6 +30,9 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (f *federator) FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error) { | 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) | 	t, err := f.GetTransportForUser(requestingUsername) | ||||||
| 	if err != nil { | 	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" | 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 { | type DomainBlock struct { | ||||||
| 	// ID of this block in the database | 	// ID of this block in the database | ||||||
| 	ID string `pg:"type:CHAR(26),pk,notnull,unique"` | 	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. | 	// blocked domain | ||||||
| 	// For example: 'example.org' also blocks 'gts.example.org'. '.com' blocks *any* '.com' domains. | 	Domain string `pg:",pk,notnull,unique"` | ||||||
| 	// TODO: implement wildcards here |  | ||||||
| 	Domain string `pg:",notnull"` |  | ||||||
| 	// When was this block created | 	// When was this block created | ||||||
| 	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | 	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
| 	// When was this block updated | 	// When was this block updated | ||||||
| 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
| 	// Account ID of the creator of this block | 	// Account ID of the creator of this block | ||||||
| 	CreatedByAccountID string `pg:"type:CHAR(26),notnull"` | 	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 | 	// Private comment on this block, viewable to admins | ||||||
| 	PrivateComment string | 	PrivateComment string | ||||||
| 	// Public comment on this block, viewable (optionally) by everyone | 	// Public comment on this block, viewable (optionally) by everyone | ||||||
| 	PublicComment string | 	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 of this instance in the database | ||||||
| 	ID string `pg:"type:CHAR(26),pk,notnull,unique"` | 	ID string `pg:"type:CHAR(26),pk,notnull,unique"` | ||||||
| 	// Instance domain eg example.org | 	// 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 of this instance as it would like to be displayed. | ||||||
| 	Title string | 	Title string | ||||||
| 	// base URI of this instance eg https://example.org | 	// 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 { | 	if requireToken && a.Token == nil { | ||||||
| 		return nil, errors.New("token not supplied") | 		return nil, errors.New("token not supplied") | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if requireApp && a.Application == nil { | 	if requireApp && a.Application == nil { | ||||||
| 		return nil, errors.New("application not supplied") | 		return nil, errors.New("application not supplied") | ||||||
| 	} | 	} | ||||||
| 	if requireUser && a.User == nil { | 
 | ||||||
|  | 	if requireUser { | ||||||
|  | 		if a.User == nil { | ||||||
| 			return nil, errors.New("user not supplied") | 			return nil, errors.New("user not supplied") | ||||||
| 		} | 		} | ||||||
| 	if requireAccount && a.Account == nil { | 		if a.User.Disabled || !a.User.Approved { | ||||||
|  | 			return nil, errors.New("user disabled or not approved") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if requireAccount { | ||||||
|  | 		if a.Account == nil { | ||||||
| 			return nil, errors.New("account not supplied") | 			return nil, errors.New("account not supplied") | ||||||
| 		} | 		} | ||||||
|  | 		if !a.Account.SuspendedAt.IsZero() { | ||||||
|  | 			return nil, errors.New("account suspended") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return a, nil | 	return a, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,512 +19,43 @@ | ||||||
| package processing | package processing | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 
 |  | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	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/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"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) { | func (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) { | ||||||
| 	l := p.log.WithField("func", "accountCreate") | 	return p.accountProcessor.Create(authed.Token, authed.Application, form) | ||||||
| 
 |  | ||||||
| 	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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) { | func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) { | ||||||
| 	targetAccount := >smodel.Account{} | 	return p.accountProcessor.Get(authed.Account, targetAccountID) | ||||||
| 	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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) { | func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) { | ||||||
| 	l := p.log.WithField("func", "AccountUpdate") | 	return p.accountProcessor.Update(authed.Account, form) | ||||||
| 
 |  | ||||||
| 	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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { | func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { | ||||||
| 	targetAccount := >smodel.Account{} | 	return p.accountProcessor.StatusesGet(authed.Account, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) | ||||||
| 	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) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { | func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { | ||||||
| 	blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID) | 	return p.accountProcessor.FollowersGet(authed.Account, 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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { | func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { | ||||||
| 	blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID) | 	return p.accountProcessor.FollowingGet(authed.Account, 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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { | func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { | ||||||
| 	if authed == nil || authed.Account == nil { | 	return p.accountProcessor.RelationshipGet(authed.Account, targetAccountID) | ||||||
| 		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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) { | 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 | 	return p.accountProcessor.FollowCreate(authed.Account, form) | ||||||
| 	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) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { | 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 | 	return p.accountProcessor.FollowRemove(authed.Account, targetAccountID) | ||||||
| 	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) |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										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 | package processing | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 
 |  | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	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" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) { | func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) { | ||||||
| 	if !authed.User.Admin { | 	return p.adminProcessor.EmojiCreate(authed.Account, authed.User, form) | ||||||
| 		return nil, fmt.Errorf("user %s not an admin", authed.User.ID) | } | ||||||
| 	} | 
 | ||||||
| 
 | func (p *processor) AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) { | ||||||
| 	// open the emoji and extract the bytes from it | 	return p.adminProcessor.DomainBlockCreate(authed.Account, form) | ||||||
| 	f, err := form.Image.Open() | } | ||||||
| 	if err != nil { | 
 | ||||||
| 		return nil, fmt.Errorf("error opening emoji: %s", err) | func (p *processor) AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { | ||||||
| 	} | 	return p.adminProcessor.DomainBlocksGet(authed.Account, export) | ||||||
| 	buf := new(bytes.Buffer) | } | ||||||
| 	size, err := io.Copy(buf, f) | 
 | ||||||
| 	if err != nil { | func (p *processor) AdminDomainBlockGet(authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) { | ||||||
| 		return nil, fmt.Errorf("error reading emoji: %s", err) | 	return p.adminProcessor.DomainBlockGet(authed.Account, id, export) | ||||||
| 	} | } | ||||||
| 	if size == 0 { | 
 | ||||||
| 		return nil, errors.New("could not read provided emoji: size 0 bytes") | func (p *processor) AdminDomainBlockDelete(authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode) { | ||||||
| 	} | 	return p.adminProcessor.DomainBlockDelete(authed.Account, id) | ||||||
| 
 |  | ||||||
| 	// 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 |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										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 ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | @ -89,7 +90,7 @@ func (p *processor) dereferenceFediRequest(username string, requestingAccountURI | ||||||
| 	return requestingAccount, nil | 	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 | 	// get the account the request is referring to | ||||||
| 	requestedAccount := >smodel.Account{} | 	requestedAccount := >smodel.Account{} | ||||||
| 	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | 	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 requestedPerson vocab.ActivityStreamsPerson | ||||||
| 	var err error | 	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 | 		// 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) | 		requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, gtserror.NewErrorInternalError(err) | 			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 | 		// 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) | 		requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) | ||||||
| 		if err != nil { | 		if err != nil || !authenticated { | ||||||
| 			return nil, gtserror.NewErrorNotAuthorized(err) | 			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 | 		// 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 | 	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 | 	// get the account the request is referring to | ||||||
| 	requestedAccount := >smodel.Account{} | 	requestedAccount := >smodel.Account{} | ||||||
| 	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | 	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 | 	// authenticate the request | ||||||
| 	requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) | 	requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) | ||||||
| 	if err != nil { | 	if err != nil || !authenticated { | ||||||
| 		return nil, gtserror.NewErrorNotAuthorized(err) | 		return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI) | 	requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI) | ||||||
|  | @ -189,7 +190,7 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req | ||||||
| 	return data, nil | 	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 | 	// get the account the request is referring to | ||||||
| 	requestedAccount := >smodel.Account{} | 	requestedAccount := >smodel.Account{} | ||||||
| 	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | 	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 | 	// authenticate the request | ||||||
| 	requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) | 	requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) | ||||||
| 	if err != nil { | 	if err != nil || !authenticated { | ||||||
| 		return nil, gtserror.NewErrorNotAuthorized(err) | 		return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI) | 	requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI) | ||||||
|  | @ -234,7 +235,7 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req | ||||||
| 	return data, nil | 	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 | 	// get the account the request is referring to | ||||||
| 	requestedAccount := >smodel.Account{} | 	requestedAccount := >smodel.Account{} | ||||||
| 	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | 	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | ||||||
|  | @ -242,9 +243,9 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// authenticate the request | 	// authenticate the request | ||||||
| 	requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) | 	requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) | ||||||
| 	if err != nil { | 	if err != nil || !authenticated { | ||||||
| 		return nil, gtserror.NewErrorNotAuthorized(err) | 		return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI) | 	requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI) | ||||||
|  | @ -294,7 +295,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st | ||||||
| 	return data, nil | 	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 | 	// get the account the request is referring to | ||||||
| 	requestedAccount := >smodel.Account{} | 	requestedAccount := >smodel.Account{} | ||||||
| 	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | 	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) { | func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { | ||||||
| 	contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) | 	contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) | ||||||
| 	posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) | 	return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) | ||||||
| 	return posted, err |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ import ( | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/streams" | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"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") | 				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 { | 			if err := p.deleteStatusFromTimelines(statusToDelete); err != nil { | ||||||
| 				return err | 				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 | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // TODO: move all the below functions into federation.Federator | ||||||
|  | 
 | ||||||
| func (p *processor) federateStatus(status *gtsmodel.Status) error { | 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) | 	asStatus, err := p.tc.StatusToAS(status) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("federateStatus: error converting status to as format: %s", err) | 		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 | 	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) | 	asStatus, err := p.tc.StatusToAS(status) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err) | 		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 { | 	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 { | 	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 | 	// 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 { | 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) | 	asAnnounce, err := p.tc.BoostToAS(boost, originAccount, targetAccount) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("federateUnannounce: error converting status to announce: %s", err) | 		return fmt.Errorf("federateUnannounce: error converting status to announce: %s", err) | ||||||
|  |  | ||||||
|  | @ -21,7 +21,6 @@ package processing | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"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") | 			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) | 				return fmt.Errorf("error dereferencing status from federator: %s", err) | ||||||
| 			} | 			} | ||||||
| 			if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil { | 			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") | 			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) | 				return fmt.Errorf("error dereferencing account from federator: %s", err) | ||||||
| 			} | 			} | ||||||
| 			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { | 			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") | 				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) | 				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") | 			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) | 				return fmt.Errorf("error dereferencing account from federator: %s", err) | ||||||
| 			} | 			} | ||||||
| 			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { | 			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { | ||||||
|  | @ -160,6 +159,27 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | ||||||
| 			if !ok { | 			if !ok { | ||||||
| 				return errors.New("note was not parseable as *gtsmodel.Status") | 				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) | 			return p.deleteStatusFromTimelines(statusToDelete) | ||||||
| 		case gtsmodel.ActivityStreamsProfile: | 		case gtsmodel.ActivityStreamsProfile: | ||||||
| 			// DELETE A PROFILE/ACCOUNT | 			// DELETE A PROFILE/ACCOUNT | ||||||
|  | @ -183,299 +203,3 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | ||||||
| 
 | 
 | ||||||
| 	return nil | 	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 | 	// process avatar if provided | ||||||
| 	if form.Avatar != nil && form.Avatar.Size != 0 { | 	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 { | 		if err != nil { | ||||||
| 			return nil, gtserror.NewErrorBadRequest(err, "error processing avatar") | 			return nil, gtserror.NewErrorBadRequest(err, "error processing avatar") | ||||||
| 		} | 		} | ||||||
|  | @ -139,7 +139,7 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest) | ||||||
| 
 | 
 | ||||||
| 	// process header if provided | 	// process header if provided | ||||||
| 	if form.Header != nil && form.Header.Size != 0 { | 	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 { | 		if err != nil { | ||||||
| 			return nil, gtserror.NewErrorBadRequest(err, "error processing header") | 			return nil, gtserror.NewErrorBadRequest(err, "error processing header") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -19,268 +19,23 @@ | ||||||
| package processing | package processing | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	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/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { | func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { | ||||||
| 	// First check this user/account is permitted to create media | 	return p.mediaProcessor.Create(authed.Account, form) | ||||||
| 	// 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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { | func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { | ||||||
| 	attachment := >smodel.MediaAttachment{} | 	return p.mediaProcessor.GetMedia(authed.Account, mediaAttachmentID) | ||||||
| 	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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { | func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { | ||||||
| 	attachment := >smodel.MediaAttachment{} | 	return p.mediaProcessor.Update(authed.Account, mediaAttachmentID, form) | ||||||
| 	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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) { | func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) { | ||||||
| 	// parse the form fields | 	return p.mediaProcessor.GetFile(authed.Account, form) | ||||||
| 	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 |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										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 ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | @ -32,8 +33,11 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/status" | 	"github.com/superseriousbusiness/gotosocial/internal/processing/account" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/streaming" | 	"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/timeline" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/visibility" | 	"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 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) | 	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 processes the creation of a new API application | ||||||
| 	AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) | 	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 | 	// 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. | 	// 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 | 	// 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. | 	// 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 | 	// 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. | 	// 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 | 	// 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. | 	// 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 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 returns a well known response giving the path to node info. | ||||||
| 	GetNodeInfoRel(request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) | 	GetNodeInfoRel(request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) | ||||||
|  | @ -210,8 +222,11 @@ type processor struct { | ||||||
| 		SUB-PROCESSORS | 		SUB-PROCESSORS | ||||||
| 	*/ | 	*/ | ||||||
| 
 | 
 | ||||||
|  | 	accountProcessor   account.Processor | ||||||
|  | 	adminProcessor     admin.Processor | ||||||
| 	statusProcessor    status.Processor | 	statusProcessor    status.Processor | ||||||
| 	streamingProcessor streaming.Processor | 	streamingProcessor streaming.Processor | ||||||
|  | 	mediaProcessor     mediaProcessor.Processor | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewProcessor returns a new Processor that uses the given federator and logger | // 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) | 	statusProcessor := status.New(db, tc, config, fromClientAPI, log) | ||||||
| 	streamingProcessor := streaming.New(db, tc, oauthServer, config, 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{ | 	return &processor{ | ||||||
| 		fromClientAPI:   fromClientAPI, | 		fromClientAPI:   fromClientAPI, | ||||||
|  | @ -238,8 +256,11 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f | ||||||
| 		db:              db, | 		db:              db, | ||||||
| 		filter:          visibility.NewFilter(db, log), | 		filter:          visibility.NewFilter(db, log), | ||||||
| 
 | 
 | ||||||
|  | 		accountProcessor:   accountProcessor, | ||||||
|  | 		adminProcessor:     adminProcessor, | ||||||
| 		statusProcessor:    statusProcessor, | 		statusProcessor:    statusProcessor, | ||||||
| 		streamingProcessor: streamingProcessor, | 		streamingProcessor: streamingProcessor, | ||||||
|  | 		mediaProcessor:     mediaProcessor, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -251,14 +272,18 @@ func (p *processor) Start() error { | ||||||
| 			select { | 			select { | ||||||
| 			case clientMsg := <-p.fromClientAPI: | 			case clientMsg := <-p.fromClientAPI: | ||||||
| 				p.log.Infof("received message FROM client API: %+v", clientMsg) | 				p.log.Infof("received message FROM client API: %+v", clientMsg) | ||||||
|  | 				go func() { | ||||||
| 					if err := p.processFromClientAPI(clientMsg); err != nil { | 					if err := p.processFromClientAPI(clientMsg); err != nil { | ||||||
| 						p.log.Error(err) | 						p.log.Error(err) | ||||||
| 					} | 					} | ||||||
|  | 				}() | ||||||
| 			case federatorMsg := <-p.fromFederator: | 			case federatorMsg := <-p.fromFederator: | ||||||
| 				p.log.Infof("received message FROM federator: %+v", federatorMsg) | 				p.log.Infof("received message FROM federator: %+v", federatorMsg) | ||||||
|  | 				go func() { | ||||||
| 					if err := p.processFromFederator(federatorMsg); err != nil { | 					if err := p.processFromFederator(federatorMsg); err != nil { | ||||||
| 						p.log.Error(err) | 						p.log.Error(err) | ||||||
| 					} | 					} | ||||||
|  | 				}() | ||||||
| 			case <-p.stop: | 			case <-p.stop: | ||||||
| 				break DistLoop | 				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) | 			// 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) | 				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) | 			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) | 			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 | 		// 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) | 			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, | 		APActivityType: gtsmodel.ActivityStreamsDelete, | ||||||
| 		GTSModel:       targetStatus, | 		GTSModel:       targetStatus, | ||||||
| 		OriginAccount:  account, | 		OriginAccount:  account, | ||||||
|  | 		TargetAccount:  account, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return mastoStatus, nil | 	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) | 	RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) | ||||||
| 	// NotificationToMasto converts a gts notification into a mastodon notification | 	// NotificationToMasto converts a gts notification into a mastodon notification | ||||||
| 	NotificationToMasto(n *gtsmodel.Notification) (*model.Notification, error) | 	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 | 		FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL | ||||||
|  |  | ||||||
|  | @ -644,3 +644,23 @@ func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notifi | ||||||
| 		Status:    mastoStatus, | 		Status:    mastoStatus, | ||||||
| 	}, nil | 	}, 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. | 	// 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. | 	// This will usually be the owner of whatever activity is being posted. | ||||||
| 	APRequestingActorIRI APContextKey = "requestingActorIRI" | 	APRequestingActorIRI APContextKey = "requestingActorIRI" | ||||||
| 	// APRequestingPublicKeyID can be used to set and retrieve the public key ID of an incoming federation request. | 	// APRequestingPublicKeyVerifier can be used to set and retrieve the public key verifier of an incoming federation request. | ||||||
| 	APRequestingPublicKeyID APContextKey = "requestingPublicKeyID" | 	APRequestingPublicKeyVerifier APContextKey = "requestingPublicKeyVerifier" | ||||||
| 	// APFromFederatorChanKey can be used to pass a pointer to the fromFederator channel into the federator for use in callbacks. | 	// APFromFederatorChanKey can be used to pass a pointer to the fromFederator channel into the federator for use in callbacks. | ||||||
| 	APFromFederatorChanKey APContextKey = "fromFederatorChan" | 	APFromFederatorChanKey APContextKey = "fromFederatorChan" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -19,9 +19,20 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount | ||||||
| 	relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus) | 	relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) | 		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 target account is suspended then don't show the status | ||||||
| 	if !targetAccount.SuspendedAt.IsZero() { | 	if !targetAccount.SuspendedAt.IsZero() { | ||||||
| 		l.Trace("target account suspended at is not zero") | 		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 | 	// status boosts accounts id | ||||||
| 	if relevantAccounts.BoostedAccount != nil { | 	if relevantAccounts.BoostedStatusAuthor != nil { | ||||||
| 		if blocked, err := f.db.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { | 		if blocked, err := f.db.Blocked(relevantAccounts.BoostedStatusAuthor.ID, requestingAccount.ID); err != nil { | ||||||
| 			return false, err | 			return false, err | ||||||
| 		} else if blocked { | 		} else if blocked { | ||||||
| 			l.Trace("a block exists between requesting account and boosted account") | 			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 | 	// if the requesting account is mentioned in the status it should always be visible | ||||||
| 	for _, acct := range relevantAccounts.MentionedAccounts { | 	for _, acct := range relevantAccounts.MentionedAccounts { | ||||||
| 		if acct.ID == requestingAccount.ID { | 		if acct.ID == requestingAccount.ID { | ||||||
|  |  | ||||||
|  | @ -3,12 +3,14 @@ package visibility | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*relevantAccounts, error) { | func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*relevantAccounts, error) { | ||||||
| 	accounts := &relevantAccounts{ | 	accounts := &relevantAccounts{ | ||||||
| 		MentionedAccounts:        []*gtsmodel.Account{}, | 		MentionedAccounts:        []*gtsmodel.Account{}, | ||||||
|  | 		BoostedMentionedAccounts: []*gtsmodel.Account{}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// get the author account | 	// get the author account | ||||||
|  | @ -30,29 +32,6 @@ func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) ( | ||||||
| 		accounts.ReplyToAccount = repliedToAccount | 		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 | 	// now get all accounts with IDs that are mentioned in the status | ||||||
| 	for _, mentionID := range targetStatus.Mentions { | 	for _, mentionID := range targetStatus.Mentions { | ||||||
| 
 | 
 | ||||||
|  | @ -68,14 +47,145 @@ func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) ( | ||||||
| 		accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) | 		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 | 	return accounts, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. | // relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. | ||||||
| type relevantAccounts struct { | type relevantAccounts struct { | ||||||
|  | 	// Who wrote the status | ||||||
| 	StatusAuthor *gtsmodel.Account | 	StatusAuthor *gtsmodel.Account | ||||||
|  | 	// Who is the status replying to | ||||||
| 	ReplyToAccount *gtsmodel.Account | 	ReplyToAccount *gtsmodel.Account | ||||||
| 	BoostedAccount        *gtsmodel.Account | 	// Which accounts are mentioned (tagged) in the status | ||||||
| 	BoostedReplyToAccount *gtsmodel.Account |  | ||||||
| 	MentionedAccounts []*gtsmodel.Account | 	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 | ||||||
|  | 	// 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 | package testrig | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/blob" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation" | 	"github.com/superseriousbusiness/gotosocial/internal/federation" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/transport" | 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NewTestFederator returns a federator with the given database and (mock!!) transport controller. | // NewTestFederator returns a federator with the given database and (mock!!) transport controller. | ||||||
| func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator { | func NewTestFederator(db db.DB, tc transport.Controller, storage blob.Storage) federation.Federator { | ||||||
| 	return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db)) | 	return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db), NewTestMediaHandler(db, storage)) | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue