mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 18:02:25 -05:00 
			
		
		
		
	biiiiig refactor
This commit is contained in:
		
					parent
					
						
							
								90b1c94b89
							
						
					
				
			
			
				commit
				
					
						1ec22fe52c
					
				
			
		
					 151 changed files with 3231 additions and 4556 deletions
				
			
		|  | @ -17,25 +17,22 @@ | |||
| */ | ||||
| 
 | ||||
| // Package apimodule is basically a wrapper for a lot of modules (in subdirectories) that satisfy the ClientAPIModule interface. | ||||
| package apimodule | ||||
| package api | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| ) | ||||
| 
 | ||||
| // ClientAPIModule represents a chunk of code (usually contained in a single package) that adds a set | ||||
| // ClientModule represents a chunk of code (usually contained in a single package) that adds a set | ||||
| // of functionalities and/or side effects to a router, by mapping routes and/or middlewares onto it--in other words, a REST API ;) | ||||
| // A ClientAPIMpdule with routes corresponds roughly to one main path of the gotosocial REST api, for example /api/v1/accounts/ or /oauth/ | ||||
| type ClientAPIModule interface { | ||||
| type ClientModule interface { | ||||
| 	Route(s router.Router) error | ||||
| 	CreateTables(db db.DB) error | ||||
| } | ||||
| 
 | ||||
| // FederationAPIModule represents a chunk of code (usually contained in a single package) that adds a set | ||||
| // FederationModule represents a chunk of code (usually contained in a single package) that adds a set | ||||
| // of functionalities and/or side effects to a router, by mapping routes and/or middlewares onto it--in other words, a REST API ;) | ||||
| // Unlike ClientAPIModule, federation API module is not intended to be interacted with by clients directly -- it is primarily a server-to-server interface. | ||||
| type FederationAPIModule interface { | ||||
| type FederationModule interface { | ||||
|    Route(s router.Router) error | ||||
|    CreateTables(db db.DB) error | ||||
| } | ||||
|  | @ -19,20 +19,15 @@ | |||
| package account | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 
 | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| ) | ||||
| 
 | ||||
|  | @ -52,21 +47,15 @@ const ( | |||
| // Module implements the ClientAPIModule interface for account-related actions | ||||
| type Module struct { | ||||
| 	config    *config.Config | ||||
| 	db           db.DB | ||||
| 	oauthServer  oauth.Server | ||||
| 	mediaHandler media.Handler | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	processor message.Processor | ||||
| 	log       *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new account module | ||||
| func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.Handler, tc typeutils.TypeConverter, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||
| 	return &Module{ | ||||
| 		config:    config, | ||||
| 		db:           db, | ||||
| 		oauthServer:  oauthServer, | ||||
| 		mediaHandler: mediaHandler, | ||||
| 		tc:           tc, | ||||
| 		processor: processor, | ||||
| 		log:       log, | ||||
| 	} | ||||
| } | ||||
|  | @ -79,27 +68,6 @@ func (m *Module) Route(r router.Router) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateTables creates the required tables for this module in the given database | ||||
| func (m *Module) CreateTables(db db.DB) error { | ||||
| 	models := []interface{}{ | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Follow{}, | ||||
| 		>smodel.FollowRequest{}, | ||||
| 		>smodel.Status{}, | ||||
| 		>smodel.Application{}, | ||||
| 		>smodel.EmailDomainBlock{}, | ||||
| 		>smodel.MediaAttachment{}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, m := range models { | ||||
| 		if err := db.CreateTable(m); err != nil { | ||||
| 			return fmt.Errorf("error creating table: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Module) muxHandler(c *gin.Context) { | ||||
| 	ru := c.Request.RequestURI | ||||
| 	switch c.Request.Method { | ||||
							
								
								
									
										35
									
								
								internal/api/client/account/account_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								internal/api/client/account/account_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| package account_test | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/account" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| ) | ||||
| 
 | ||||
| type AccountStandardTestSuite struct { | ||||
| 	// standard suite interfaces | ||||
| 	suite.Suite | ||||
| 	config    *config.Config | ||||
| 	db        db.DB | ||||
| 	log       *logrus.Logger | ||||
| 	tc        typeutils.TypeConverter | ||||
| 	processor message.Processor | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
| 	testClients      map[string]*oauth.Client | ||||
| 	testApplications map[string]*gtsmodel.Application | ||||
| 	testUsers        map[string]*gtsmodel.User | ||||
| 	testAccounts     map[string]*gtsmodel.Account | ||||
| 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||
| 	testStatuses     map[string]*gtsmodel.Status | ||||
| 
 | ||||
| 	// module being tested | ||||
| 	accountModule *account.Module | ||||
| } | ||||
|  | @ -20,18 +20,14 @@ package account | |||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| 	"github.com/superseriousbusiness/oauth2/v4" | ||||
| ) | ||||
| 
 | ||||
| // AccountCreatePOSTHandler handles create account requests, validates them, | ||||
|  | @ -39,7 +35,7 @@ import ( | |||
| // It should be served as a POST at /api/v1/accounts | ||||
| func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "accountCreatePOSTHandler") | ||||
| 	authed, err := oauth.MustAuth(c, true, true, false, false) | ||||
| 	authed, err := oauth.Authed(c, true, true, false, false) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
|  | @ -47,7 +43,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | |||
| 	} | ||||
| 
 | ||||
| 	l.Trace("parsing request form") | ||||
| 	form := &mastotypes.AccountCreateRequest{} | ||||
| 	form := &model.AccountCreateRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||
| 		l.Debugf("could not parse form from request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | ||||
|  | @ -55,7 +51,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | |||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("validating form %+v", form) | ||||
| 	if err := validateCreateAccount(form, m.config.AccountsConfig, m.db); err != nil { | ||||
| 	if err := validateCreateAccount(form, m.config.AccountsConfig); err != nil { | ||||
| 		l.Debugf("error validating form: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
|  | @ -70,7 +66,9 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ti, err := m.accountCreate(form, signUpIP, authed.Token, authed.Application) | ||||
| 	form.IP = signUpIP | ||||
| 
 | ||||
| 	ti, err := m.processor.AccountCreate(authed, form) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("internal server error while creating new account: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
|  | @ -80,41 +78,9 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | |||
| 	c.JSON(http.StatusOK, ti) | ||||
| } | ||||
| 
 | ||||
| // 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 (m *Module) accountCreate(form *mastotypes.AccountCreateRequest, signUpIP net.IP, token oauth2.TokenInfo, app *gtsmodel.Application) (*mastotypes.Token, error) { | ||||
| 	l := m.log.WithField("func", "accountCreate") | ||||
| 
 | ||||
| 	// don't store a reason if we don't require one | ||||
| 	reason := form.Reason | ||||
| 	if !m.config.AccountsConfig.ReasonRequired { | ||||
| 		reason = "" | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("creating new username and account") | ||||
| 	user, err := m.db.NewSignup(form.Username, reason, m.config.AccountsConfig.RequireApproval, form.Email, form.Password, signUpIP, form.Locale, app.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, app.ID) | ||||
| 	accessToken, err := m.oauthServer.GenerateUserAccessToken(token, app.ClientSecret, user.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err) | ||||
| 	} | ||||
| 
 | ||||
| 	return &mastotypes.Token{ | ||||
| 		AccessToken: accessToken.GetAccess(), | ||||
| 		TokenType:   "Bearer", | ||||
| 		Scope:       accessToken.GetScope(), | ||||
| 		CreatedAt:   accessToken.GetAccessCreateAt().Unix(), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // validateCreateAccount checks through all the necessary prerequisites for creating a new account, | ||||
| // according to the provided account create request. If the account isn't eligible, an error will be returned. | ||||
| func validateCreateAccount(form *mastotypes.AccountCreateRequest, c *config.AccountsConfig, database db.DB) error { | ||||
| func validateCreateAccount(form *model.AccountCreateRequest, c *config.AccountsConfig) error { | ||||
| 	if !c.OpenRegistration { | ||||
| 		return errors.New("registration is not open for this server") | ||||
| 	} | ||||
|  | @ -143,13 +109,5 @@ func validateCreateAccount(form *mastotypes.AccountCreateRequest, c *config.Acco | |||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := database.IsEmailAvailable(form.Email); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := database.IsUsernameAvailable(form.Username); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										384
									
								
								internal/api/client/account/accountcreate_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								internal/api/client/account/accountcreate_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,384 @@ | |||
| // /* | ||||
| //    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_test | ||||
| 
 | ||||
| // import ( | ||||
| // 	"bytes" | ||||
| // 	"encoding/json" | ||||
| // 	"fmt" | ||||
| // 	"io" | ||||
| // 	"io/ioutil" | ||||
| // 	"mime/multipart" | ||||
| // 	"net/http" | ||||
| // 	"net/http/httptest" | ||||
| // 	"os" | ||||
| // 	"testing" | ||||
| 
 | ||||
| // 	"github.com/gin-gonic/gin" | ||||
| // 	"github.com/google/uuid" | ||||
| // 	"github.com/stretchr/testify/assert" | ||||
| // 	"github.com/stretchr/testify/suite" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/api/client/account" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||
| 
 | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| // 	"golang.org/x/crypto/bcrypt" | ||||
| // ) | ||||
| 
 | ||||
| // type AccountCreateTestSuite struct { | ||||
| // 	AccountStandardTestSuite | ||||
| // } | ||||
| 
 | ||||
| // func (suite *AccountCreateTestSuite) SetupSuite() { | ||||
| // 	testrig.StandardDBSetup(suite.db) | ||||
| // 	suite.testTokens = testrig.NewTestTokens() | ||||
| // 	suite.testClients = testrig.NewTestClients() | ||||
| // 	suite.testApplications = testrig.NewTestApplications() | ||||
| // 	suite.testUsers = testrig.NewTestUsers() | ||||
| // 	suite.testAccounts = testrig.NewTestAccounts() | ||||
| // 	suite.testAttachments = testrig.NewTestAttachments() | ||||
| // 	suite.testStatuses = testrig.NewTestStatuses() | ||||
| // } | ||||
| 
 | ||||
| // func (suite *AccountCreateTestSuite) SetupTest() { | ||||
| // 	suite.config = testrig.NewTestConfig() | ||||
| // 	suite.db = testrig.NewTestDB() | ||||
| // 	suite.log = testrig.NewTestLog() | ||||
| // 	suite.processor = testrig.NewTestProcessor(suite.db) | ||||
| // 	suite.accountModule = account.New(suite.config, suite.processor, suite.log).(*account.Module) | ||||
| // } | ||||
| 
 | ||||
| // func (suite *AccountCreateTestSuite) TearDownTest() { | ||||
| // 	testrig.StandardDBTeardown(suite.db) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerSuccessful checks the happy path for an account creation request: all the fields provided are valid, | ||||
| // // and at the end of it a new user and account should be added into the database. | ||||
| // // | ||||
| // // This is the handler served at /api/v1/accounts as POST | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() { | ||||
| 
 | ||||
| // 	t := suite.testTokens["local_account_1"] | ||||
| // 	oauthToken := oauth.TokenToOauthToken(t) | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| 
 | ||||
| // 	// 1. we should have OK from our call to the function | ||||
| // 	suite.EqualValues(http.StatusOK, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have a token in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	t := &model.Token{} | ||||
| // 	err = json.Unmarshal(b, t) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), "we're authorized now!", t.AccessToken) | ||||
| 
 | ||||
| // 	// check new account | ||||
| 
 | ||||
| // 	// 1. we should be able to get the new account from the db | ||||
| // 	acct := >smodel.Account{} | ||||
| // 	err = suite.db.GetLocalAccountByUsername("test_user", acct) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.NotNil(suite.T(), acct) | ||||
| // 	// 2. reason should be set | ||||
| // 	assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("reason"), acct.Reason) | ||||
| // 	// 3. display name should be equal to username by default | ||||
| // 	assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("username"), acct.DisplayName) | ||||
| // 	// 4. domain should be nil because this is a local account | ||||
| // 	assert.Nil(suite.T(), nil, acct.Domain) | ||||
| // 	// 5. id should be set and parseable as a uuid | ||||
| // 	assert.NotNil(suite.T(), acct.ID) | ||||
| // 	_, err = uuid.Parse(acct.ID) | ||||
| // 	assert.Nil(suite.T(), err) | ||||
| // 	// 6. private and public key should be set | ||||
| // 	assert.NotNil(suite.T(), acct.PrivateKey) | ||||
| // 	assert.NotNil(suite.T(), acct.PublicKey) | ||||
| 
 | ||||
| // 	// check new user | ||||
| 
 | ||||
| // 	// 1. we should be able to get the new user from the db | ||||
| // 	usr := >smodel.User{} | ||||
| // 	err = suite.db.GetWhere("unconfirmed_email", suite.newUserFormHappyPath.Get("email"), usr) | ||||
| // 	assert.Nil(suite.T(), err) | ||||
| // 	assert.NotNil(suite.T(), usr) | ||||
| 
 | ||||
| // 	// 2. user should have account id set to account we got above | ||||
| // 	assert.Equal(suite.T(), acct.ID, usr.AccountID) | ||||
| 
 | ||||
| // 	// 3. id should be set and parseable as a uuid | ||||
| // 	assert.NotNil(suite.T(), usr.ID) | ||||
| // 	_, err = uuid.Parse(usr.ID) | ||||
| // 	assert.Nil(suite.T(), err) | ||||
| 
 | ||||
| // 	// 4. locale should be equal to what we requested | ||||
| // 	assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("locale"), usr.Locale) | ||||
| 
 | ||||
| // 	// 5. created by application id should be equal to the app id | ||||
| // 	assert.Equal(suite.T(), suite.testApplication.ID, usr.CreatedByApplicationID) | ||||
| 
 | ||||
| // 	// 6. password should be matcheable to what we set above | ||||
| // 	err = bcrypt.CompareHashAndPassword([]byte(usr.EncryptedPassword), []byte(suite.newUserFormHappyPath.Get("password"))) | ||||
| // 	assert.Nil(suite.T(), err) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no authorization is provided: | ||||
| // // only registered applications can create accounts, and we don't provide one here. | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoAuth() { | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| 
 | ||||
| // 	// 1. we should have forbidden from our call to the function because we didn't auth | ||||
| // 	suite.EqualValues(http.StatusForbidden, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no form is provided at all. | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoForm() { | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| // 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), `{"error":"missing one or more required form values"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerWeakPassword makes sure that the handler fails when a weak password is provided | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeakPassword() { | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| // 	// set a weak password | ||||
| // 	ctx.Request.Form.Set("password", "weak") | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| // 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), `{"error":"insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerWeirdLocale makes sure that the handler fails when a weird locale is provided | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeirdLocale() { | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| // 	// set an invalid locale | ||||
| // 	ctx.Request.Form.Set("locale", "neverneverland") | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| // 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), `{"error":"language: tag is not well-formed"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerRegistrationsClosed makes sure that the handler fails when registrations are closed | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerRegistrationsClosed() { | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 
 | ||||
| // 	// close registrations | ||||
| // 	suite.config.AccountsConfig.OpenRegistration = false | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| // 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), `{"error":"registration is not open for this server"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when no reason is provided but one is required | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerReasonNotProvided() { | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 
 | ||||
| // 	// remove reason | ||||
| // 	ctx.Request.Form.Set("reason", "") | ||||
| 
 | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| // 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), `{"error":"no reason provided"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // // TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when a crappy reason is presented but a good one is required | ||||
| // func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerInsufficientReason() { | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| // 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 
 | ||||
| // 	// remove reason | ||||
| // 	ctx.Request.Form.Set("reason", "just cuz") | ||||
| 
 | ||||
| // 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| // 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	b, err := ioutil.ReadAll(result.Body) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	assert.Equal(suite.T(), `{"error":"reason should be at least 40 chars but 'just cuz' was 8"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // /* | ||||
| // 	TESTING: AccountUpdateCredentialsPATCHHandler | ||||
| // */ | ||||
| 
 | ||||
| // func (suite *AccountCreateTestSuite) TestAccountUpdateCredentialsPATCHHandler() { | ||||
| 
 | ||||
| // 	// put test local account in db | ||||
| // 	err := suite.db.Put(suite.testAccountLocal) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	// attach avatar to request | ||||
| // 	aviFile, err := os.Open("../../media/test/test-jpeg.jpg") | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	body := &bytes.Buffer{} | ||||
| // 	writer := multipart.NewWriter(body) | ||||
| 
 | ||||
| // 	part, err := writer.CreateFormFile("avatar", "test-jpeg.jpg") | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	_, err = io.Copy(part, aviFile) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	err = aviFile.Close() | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	err = writer.Close() | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), body) // the endpoint we're hitting | ||||
| // 	ctx.Request.Header.Set("Content-Type", writer.FormDataContentType()) | ||||
| // 	suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| 
 | ||||
| // 	// 1. we should have OK because our request was valid | ||||
| // 	suite.EqualValues(http.StatusOK, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	// TODO: implement proper checks here | ||||
| // 	// | ||||
| // 	// b, err := ioutil.ReadAll(result.Body) | ||||
| // 	// assert.NoError(suite.T(), err) | ||||
| // 	// assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // func TestAccountCreateTestSuite(t *testing.T) { | ||||
| // 	suite.Run(t, new(AccountCreateTestSuite)) | ||||
| // } | ||||
|  | @ -22,8 +22,7 @@ import ( | |||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // AccountGETHandler serves the account information held by the server in response to a GET | ||||
|  | @ -31,25 +30,21 @@ import ( | |||
| // | ||||
| // See: https://docs.joinmastodon.org/methods/accounts/ | ||||
| func (m *Module) AccountGETHandler(c *gin.Context) { | ||||
| 	authed, err := oauth.Authed(c, false, false, false, false) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetAcctID := c.Param(IDKey) | ||||
| 	if targetAcctID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetAccount := >smodel.Account{} | ||||
| 	if err := m.db.GetByID(targetAcctID, targetAccount); err != nil { | ||||
| 		if _, ok := err.(db.ErrNoEntries); ok { | ||||
| 			c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"}) | ||||
| 			return | ||||
| 		} | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	acctInfo, err := m.tc.AccountToMastoPublic(targetAccount) | ||||
| 	acctInfo, err := m.processor.AccountGet(authed, targetAcctID) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
							
								
								
									
										71
									
								
								internal/api/client/account/accountupdate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								internal/api/client/account/accountupdate.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| /* | ||||
|    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 ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // AccountUpdateCredentialsPATCHHandler allows a user to modify their account/profile settings. | ||||
| // It should be served as a PATCH at /api/v1/accounts/update_credentials | ||||
| // | ||||
| // TODO: this can be optimized massively by building up a picture of what we want the new account | ||||
| // details to be, and then inserting it all in the database at once. As it is, we do queries one-by-one | ||||
| // which is not gonna make the database very happy when lots of requests are going through. | ||||
| // This way it would also be safer because the update won't happen until *all* the fields are validated. | ||||
| // Otherwise we risk doing a partial update and that's gonna cause probllleeemmmsss. | ||||
| func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "accountUpdateCredentialsPATCHHandler") | ||||
| 	authed, err := oauth.Authed(c, true, false, false, true) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 	l.Tracef("retrieved account %+v", authed.Account.ID) | ||||
| 
 | ||||
| 	l.Trace("parsing request form") | ||||
| 	form := &model.UpdateCredentialsRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||
| 		l.Debugf("could not parse form from request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// if everything on the form is nil, then nothing has been set and we shouldn't continue | ||||
| 	if form.Discoverable == nil && form.Bot == nil && form.DisplayName == nil && form.Note == nil && form.Avatar == nil && form.Header == nil && form.Locked == nil && form.Source == nil && form.FieldsAttributes == nil { | ||||
| 		l.Debugf("could not parse form from request") | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	acctSensitive, err := m.processor.AccountGet(authed, authed.Account.ID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("could not update account: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("conversion successful, returning OK and mastosensitive account %+v", acctSensitive) | ||||
| 	c.JSON(http.StatusOK, acctSensitive) | ||||
| } | ||||
							
								
								
									
										303
									
								
								internal/api/client/account/accountupdate_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								internal/api/client/account/accountupdate_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,303 @@ | |||
| // /* | ||||
| //    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_test | ||||
| 
 | ||||
| // import ( | ||||
| // 	"bytes" | ||||
| // 	"context" | ||||
| // 	"fmt" | ||||
| // 	"io" | ||||
| // 	"mime/multipart" | ||||
| // 	"net/http" | ||||
| // 	"net/http/httptest" | ||||
| // 	"net/url" | ||||
| // 	"os" | ||||
| // 	"testing" | ||||
| // 	"time" | ||||
| 
 | ||||
| // 	"github.com/gin-gonic/gin" | ||||
| // 	"github.com/google/uuid" | ||||
| // 	"github.com/sirupsen/logrus" | ||||
| // 	"github.com/stretchr/testify/assert" | ||||
| // 	"github.com/stretchr/testify/mock" | ||||
| // 	"github.com/stretchr/testify/suite" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/api/client/account" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| // 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| // 	"github.com/superseriousbusiness/oauth2/v4" | ||||
| // 	"github.com/superseriousbusiness/oauth2/v4/models" | ||||
| // 	oauthmodels "github.com/superseriousbusiness/oauth2/v4/models" | ||||
| // ) | ||||
| 
 | ||||
| // type AccountUpdateTestSuite struct { | ||||
| // 	suite.Suite | ||||
| // 	config               *config.Config | ||||
| // 	log                  *logrus.Logger | ||||
| // 	testAccountLocal     *gtsmodel.Account | ||||
| // 	testApplication      *gtsmodel.Application | ||||
| // 	testToken            oauth2.TokenInfo | ||||
| // 	mockOauthServer      *oauth.MockServer | ||||
| // 	mockStorage          *storage.MockStorage | ||||
| // 	mediaHandler         media.Handler | ||||
| // 	mastoConverter       typeutils.TypeConverter | ||||
| // 	db                   db.DB | ||||
| // 	accountModule        *account.Module | ||||
| // 	newUserFormHappyPath url.Values | ||||
| // } | ||||
| 
 | ||||
| // /* | ||||
| // 	TEST INFRASTRUCTURE | ||||
| // */ | ||||
| 
 | ||||
| // // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| // func (suite *AccountUpdateTestSuite) SetupSuite() { | ||||
| // 	// some of our subsequent entities need a log so create this here | ||||
| // 	log := logrus.New() | ||||
| // 	log.SetLevel(logrus.TraceLevel) | ||||
| // 	suite.log = log | ||||
| 
 | ||||
| // 	suite.testAccountLocal = >smodel.Account{ | ||||
| // 		ID:       uuid.NewString(), | ||||
| // 		Username: "test_user", | ||||
| // 	} | ||||
| 
 | ||||
| // 	// can use this test application throughout | ||||
| // 	suite.testApplication = >smodel.Application{ | ||||
| // 		ID:           "weeweeeeeeeeeeeeee", | ||||
| // 		Name:         "a test application", | ||||
| // 		Website:      "https://some-application-website.com", | ||||
| // 		RedirectURI:  "http://localhost:8080", | ||||
| // 		ClientID:     "a-known-client-id", | ||||
| // 		ClientSecret: "some-secret", | ||||
| // 		Scopes:       "read", | ||||
| // 		VapidKey:     "aaaaaa-aaaaaaaa-aaaaaaaaaaa", | ||||
| // 	} | ||||
| 
 | ||||
| // 	// can use this test token throughout | ||||
| // 	suite.testToken = &oauthmodels.Token{ | ||||
| // 		ClientID:      "a-known-client-id", | ||||
| // 		RedirectURI:   "http://localhost:8080", | ||||
| // 		Scope:         "read", | ||||
| // 		Code:          "123456789", | ||||
| // 		CodeCreateAt:  time.Now(), | ||||
| // 		CodeExpiresIn: time.Duration(10 * time.Minute), | ||||
| // 	} | ||||
| 
 | ||||
| // 	// Direct config to local postgres instance | ||||
| // 	c := config.Empty() | ||||
| // 	c.Protocol = "http" | ||||
| // 	c.Host = "localhost" | ||||
| // 	c.DBConfig = &config.DBConfig{ | ||||
| // 		Type:            "postgres", | ||||
| // 		Address:         "localhost", | ||||
| // 		Port:            5432, | ||||
| // 		User:            "postgres", | ||||
| // 		Password:        "postgres", | ||||
| // 		Database:        "postgres", | ||||
| // 		ApplicationName: "gotosocial", | ||||
| // 	} | ||||
| // 	c.MediaConfig = &config.MediaConfig{ | ||||
| // 		MaxImageSize: 2 << 20, | ||||
| // 	} | ||||
| // 	c.StorageConfig = &config.StorageConfig{ | ||||
| // 		Backend:       "local", | ||||
| // 		BasePath:      "/tmp", | ||||
| // 		ServeProtocol: "http", | ||||
| // 		ServeHost:     "localhost", | ||||
| // 		ServeBasePath: "/fileserver/media", | ||||
| // 	} | ||||
| // 	suite.config = c | ||||
| 
 | ||||
| // 	// use an actual database for this, because it's just easier than mocking one out | ||||
| // 	database, err := db.NewPostgresService(context.Background(), c, log) | ||||
| // 	if err != nil { | ||||
| // 		suite.FailNow(err.Error()) | ||||
| // 	} | ||||
| // 	suite.db = database | ||||
| 
 | ||||
| // 	// we need to mock the oauth server because account creation needs it to create a new token | ||||
| // 	suite.mockOauthServer = &oauth.MockServer{} | ||||
| // 	suite.mockOauthServer.On("GenerateUserAccessToken", suite.testToken, suite.testApplication.ClientSecret, mock.AnythingOfType("string")).Run(func(args mock.Arguments) { | ||||
| // 		l := suite.log.WithField("func", "GenerateUserAccessToken") | ||||
| // 		token := args.Get(0).(oauth2.TokenInfo) | ||||
| // 		l.Infof("received token %+v", token) | ||||
| // 		clientSecret := args.Get(1).(string) | ||||
| // 		l.Infof("received clientSecret %+v", clientSecret) | ||||
| // 		userID := args.Get(2).(string) | ||||
| // 		l.Infof("received userID %+v", userID) | ||||
| // 	}).Return(&models.Token{ | ||||
| // 		Code: "we're authorized now!", | ||||
| // 	}, nil) | ||||
| 
 | ||||
| // 	suite.mockStorage = &storage.MockStorage{} | ||||
| // 	// We don't need storage to do anything for these tests, so just simulate a success and do nothing -- we won't need to return anything from storage | ||||
| // 	suite.mockStorage.On("StoreFileAt", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil) | ||||
| 
 | ||||
| // 	// set a media handler because some handlers (eg update credentials) need to upload media (new header/avatar) | ||||
| // 	suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log) | ||||
| 
 | ||||
| // 	suite.mastoConverter = typeutils.NewConverter(suite.config, suite.db) | ||||
| 
 | ||||
| // 	// and finally here's the thing we're actually testing! | ||||
| // 	suite.accountModule = account.New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*account.Module) | ||||
| // } | ||||
| 
 | ||||
| // func (suite *AccountUpdateTestSuite) TearDownSuite() { | ||||
| // 	if err := suite.db.Stop(context.Background()); err != nil { | ||||
| // 		logrus.Panicf("error closing db connection: %s", err) | ||||
| // 	} | ||||
| // } | ||||
| 
 | ||||
| // // SetupTest creates a db connection and creates necessary tables before each test | ||||
| // func (suite *AccountUpdateTestSuite) SetupTest() { | ||||
| // 	// create all the tables we might need in thie suite | ||||
| // 	models := []interface{}{ | ||||
| // 		>smodel.User{}, | ||||
| // 		>smodel.Account{}, | ||||
| // 		>smodel.Follow{}, | ||||
| // 		>smodel.FollowRequest{}, | ||||
| // 		>smodel.Status{}, | ||||
| // 		>smodel.Application{}, | ||||
| // 		>smodel.EmailDomainBlock{}, | ||||
| // 		>smodel.MediaAttachment{}, | ||||
| // 	} | ||||
| // 	for _, m := range models { | ||||
| // 		if err := suite.db.CreateTable(m); err != nil { | ||||
| // 			logrus.Panicf("db connection error: %s", err) | ||||
| // 		} | ||||
| // 	} | ||||
| 
 | ||||
| // 	// form to submit for happy path account create requests -- this will be changed inside tests so it's better to set it before each test | ||||
| // 	suite.newUserFormHappyPath = url.Values{ | ||||
| // 		"reason":    []string{"a very good reason that's at least 40 characters i swear"}, | ||||
| // 		"username":  []string{"test_user"}, | ||||
| // 		"email":     []string{"user@example.org"}, | ||||
| // 		"password":  []string{"very-strong-password"}, | ||||
| // 		"agreement": []string{"true"}, | ||||
| // 		"locale":    []string{"en"}, | ||||
| // 	} | ||||
| 
 | ||||
| // 	// same with accounts config | ||||
| // 	suite.config.AccountsConfig = &config.AccountsConfig{ | ||||
| // 		OpenRegistration: true, | ||||
| // 		RequireApproval:  true, | ||||
| // 		ReasonRequired:   true, | ||||
| // 	} | ||||
| // } | ||||
| 
 | ||||
| // // TearDownTest drops tables to make sure there's no data in the db | ||||
| // func (suite *AccountUpdateTestSuite) TearDownTest() { | ||||
| 
 | ||||
| // 	// remove all the tables we might have used so it's clear for the next test | ||||
| // 	models := []interface{}{ | ||||
| // 		>smodel.User{}, | ||||
| // 		>smodel.Account{}, | ||||
| // 		>smodel.Follow{}, | ||||
| // 		>smodel.FollowRequest{}, | ||||
| // 		>smodel.Status{}, | ||||
| // 		>smodel.Application{}, | ||||
| // 		>smodel.EmailDomainBlock{}, | ||||
| // 		>smodel.MediaAttachment{}, | ||||
| // 	} | ||||
| // 	for _, m := range models { | ||||
| // 		if err := suite.db.DropTable(m); err != nil { | ||||
| // 			logrus.Panicf("error dropping table: %s", err) | ||||
| // 		} | ||||
| // 	} | ||||
| // } | ||||
| 
 | ||||
| // /* | ||||
| // 	ACTUAL TESTS | ||||
| // */ | ||||
| 
 | ||||
| // /* | ||||
| // 	TESTING: AccountUpdateCredentialsPATCHHandler | ||||
| // */ | ||||
| 
 | ||||
| // func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() { | ||||
| 
 | ||||
| // 	// put test local account in db | ||||
| // 	err := suite.db.Put(suite.testAccountLocal) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	// attach avatar to request form | ||||
| // 	avatarFile, err := os.Open("../../media/test/test-jpeg.jpg") | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| // 	body := &bytes.Buffer{} | ||||
| // 	writer := multipart.NewWriter(body) | ||||
| 
 | ||||
| // 	avatarPart, err := writer.CreateFormFile("avatar", "test-jpeg.jpg") | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	_, err = io.Copy(avatarPart, avatarFile) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	err = avatarFile.Close() | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	// set display name to a new value | ||||
| // 	displayNamePart, err := writer.CreateFormField("display_name") | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	_, err = io.Copy(displayNamePart, bytes.NewBufferString("test_user_wohoah")) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	// set locked to true | ||||
| // 	lockedPart, err := writer.CreateFormField("locked") | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	_, err = io.Copy(lockedPart, bytes.NewBufferString("true")) | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	// close the request writer, the form is now prepared | ||||
| // 	err = writer.Close() | ||||
| // 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| // 	// setup | ||||
| // 	recorder := httptest.NewRecorder() | ||||
| // 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal) | ||||
| // 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| // 	ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), body) // the endpoint we're hitting | ||||
| // 	ctx.Request.Header.Set("Content-Type", writer.FormDataContentType()) | ||||
| // 	suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) | ||||
| 
 | ||||
| // 	// check response | ||||
| 
 | ||||
| // 	// 1. we should have OK because our request was valid | ||||
| // 	suite.EqualValues(http.StatusOK, recorder.Code) | ||||
| 
 | ||||
| // 	// 2. we should have an error message in the result body | ||||
| // 	result := recorder.Result() | ||||
| // 	defer result.Body.Close() | ||||
| // 	// TODO: implement proper checks here | ||||
| // 	// | ||||
| // 	// b, err := ioutil.ReadAll(result.Body) | ||||
| // 	// assert.NoError(suite.T(), err) | ||||
| // 	// assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b)) | ||||
| // } | ||||
| 
 | ||||
| // func TestAccountUpdateTestSuite(t *testing.T) { | ||||
| // 	suite.Run(t, new(AccountUpdateTestSuite)) | ||||
| // } | ||||
|  | @ -30,21 +30,19 @@ import ( | |||
| // It should be served as a GET at /api/v1/accounts/verify_credentials | ||||
| func (m *Module) AccountVerifyGETHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "accountVerifyGETHandler") | ||||
| 	authed, err := oauth.MustAuth(c, true, false, false, true) | ||||
| 	authed, err := oauth.Authed(c, true, false, false, true) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("retrieved account %+v, converting to mastosensitive...", authed.Account.ID) | ||||
| 	acctSensitive, err := m.tc.AccountToMastoSensitive(authed.Account) | ||||
| 	acctSensitive, err := m.processor.AccountGet(authed, authed.Account.ID) | ||||
| 	if err != nil { | ||||
| 		l.Tracef("could not convert account into mastosensitive account: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		l.Debugf("error getting account from processor: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("conversion successful, returning OK and mastosensitive account %+v", acctSensitive) | ||||
| 	c.JSON(http.StatusOK, acctSensitive) | ||||
| } | ||||
|  | @ -19,17 +19,13 @@ | |||
| package admin | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
|  | @ -42,19 +38,15 @@ const ( | |||
| // Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc) | ||||
| type Module struct { | ||||
| 	config    *config.Config | ||||
| 	db           db.DB | ||||
| 	mediaHandler media.Handler | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	processor message.Processor | ||||
| 	log       *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new admin module | ||||
| func New(config *config.Config, db db.DB, mediaHandler media.Handler, tc typeutils.TypeConverter, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||
| 	return &Module{ | ||||
| 		config:    config, | ||||
| 		db:           db, | ||||
| 		mediaHandler: mediaHandler, | ||||
| 		tc:           tc, | ||||
| 		processor: processor, | ||||
| 		log:       log, | ||||
| 	} | ||||
| } | ||||
|  | @ -64,25 +56,3 @@ func (m *Module) Route(r router.Router) error { | |||
| 	r.AttachHandler(http.MethodPost, EmojiPath, m.emojiCreatePOSTHandler) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateTables creates the necessary tables for this module in the given database | ||||
| func (m *Module) CreateTables(db db.DB) error { | ||||
| 	models := []interface{}{ | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Follow{}, | ||||
| 		>smodel.FollowRequest{}, | ||||
| 		>smodel.Status{}, | ||||
| 		>smodel.Application{}, | ||||
| 		>smodel.EmailDomainBlock{}, | ||||
| 		>smodel.MediaAttachment{}, | ||||
| 		>smodel.Emoji{}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, m := range models { | ||||
| 		if err := db.CreateTable(m); err != nil { | ||||
| 			return fmt.Errorf("error creating table: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | @ -19,15 +19,13 @@ | |||
| package admin | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
|  | @ -42,7 +40,7 @@ func (m *Module) emojiCreatePOSTHandler(c *gin.Context) { | |||
| 	}) | ||||
| 
 | ||||
| 	// make sure we're authed with an admin account | ||||
| 	authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything* | ||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything* | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
|  | @ -56,7 +54,7 @@ func (m *Module) emojiCreatePOSTHandler(c *gin.Context) { | |||
| 
 | ||||
| 	// extract the media create form from the request context | ||||
| 	l.Tracef("parsing request form: %+v", c.Request.Form) | ||||
| 	form := &mastotypes.EmojiCreateRequest{} | ||||
| 	form := &model.EmojiCreateRequest{} | ||||
| 	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)}) | ||||
|  | @ -71,51 +69,17 @@ func (m *Module) emojiCreatePOSTHandler(c *gin.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// open the emoji and extract the bytes from it | ||||
| 	f, err := form.Image.Open() | ||||
| 	mastoEmoji, err := m.processor.AdminEmojiCreate(authed, form) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error opening emoji: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not open provided emoji: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 	buf := new(bytes.Buffer) | ||||
| 	size, err := io.Copy(buf, f) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error reading emoji: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not read provided emoji: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if size == 0 { | ||||
| 		l.Debug("could not read provided emoji: size 0 bytes") | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "could not read provided emoji: size 0 bytes"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using | ||||
| 	emoji, err := m.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error reading emoji: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not process emoji: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoEmoji, err := m.tc.EmojiToMasto(emoji) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error converting emoji to mastotype: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not convert emoji: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := m.db.Put(emoji); err != nil { | ||||
| 		l.Debugf("database error while processing emoji: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("database error while processing emoji: %s", err)}) | ||||
| 		l.Debugf("error creating emoji: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoEmoji) | ||||
| } | ||||
| 
 | ||||
| func validateCreateEmoji(form *mastotypes.EmojiCreateRequest) error { | ||||
| func validateCreateEmoji(form *model.EmojiCreateRequest) error { | ||||
| 	// check there actually is an image attached and it's not size 0 | ||||
| 	if form.Image == nil || form.Image.Size == 0 { | ||||
| 		return errors.New("no emoji given") | ||||
|  | @ -19,16 +19,13 @@ | |||
| package app | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| ) | ||||
| 
 | ||||
| // BasePath is the base path for this api module | ||||
|  | @ -36,18 +33,16 @@ const BasePath = "/api/v1/apps" | |||
| 
 | ||||
| // Module implements the ClientAPIModule interface for requests relating to registering/removing applications | ||||
| type Module struct { | ||||
| 	server oauth.Server | ||||
| 	db     db.DB | ||||
| 	tc     typeutils.TypeConverter | ||||
| 	config    *config.Config | ||||
| 	processor message.Processor | ||||
| 	log       *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new auth module | ||||
| func New(srv oauth.Server, db db.DB, tc typeutils.TypeConverter, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||
| 	return &Module{ | ||||
| 		server: srv, | ||||
| 		db:     db, | ||||
| 		tc:     tc, | ||||
| 		config:    config, | ||||
| 		processor: processor, | ||||
| 		log:       log, | ||||
| 	} | ||||
| } | ||||
|  | @ -57,21 +52,3 @@ func (m *Module) Route(s router.Router) error { | |||
| 	s.AttachHandler(http.MethodPost, BasePath, m.AppsPOSTHandler) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateTables creates the necessary tables for this module in the given database | ||||
| func (m *Module) CreateTables(db db.DB) error { | ||||
| 	models := []interface{}{ | ||||
| 		&oauth.Client{}, | ||||
| 		&oauth.Token{}, | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Application{}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, m := range models { | ||||
| 		if err := db.CreateTable(m); err != nil { | ||||
| 			return fmt.Errorf("error creating table: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										79
									
								
								internal/api/client/app/appcreate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								internal/api/client/app/appcreate.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 app | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // AppsPOSTHandler should be served at https://example.org/api/v1/apps | ||||
| // It is equivalent to: https://docs.joinmastodon.org/methods/apps/ | ||||
| func (m *Module) AppsPOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "AppsPOSTHandler") | ||||
| 	l.Trace("entering AppsPOSTHandler") | ||||
| 
 | ||||
| 	authed, err := oauth.Authed(c, false, false, false, false) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	form := &model.ApplicationCreateRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil { | ||||
| 		c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// permitted length for most fields | ||||
| 	formFieldLen := 64 | ||||
| 	// redirect can be a bit bigger because we probably need to encode data in the redirect uri | ||||
| 	formRedirectLen := 512 | ||||
| 
 | ||||
| 	// check lengths of fields before proceeding so the user can't spam huge entries into the database | ||||
| 	if len(form.ClientName) > formFieldLen { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.Website) > formFieldLen { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", formFieldLen)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.RedirectURIs) > formRedirectLen { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", formRedirectLen)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.Scopes) > formFieldLen { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", formFieldLen)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoApp, err := m.processor.AppCreate(authed, form) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/ | ||||
| 	c.JSON(http.StatusOK, mastoApp) | ||||
| } | ||||
|  | @ -19,14 +19,12 @@ | |||
| package auth | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| ) | ||||
| 
 | ||||
|  | @ -41,16 +39,16 @@ const ( | |||
| 
 | ||||
| // Module implements the ClientAPIModule interface for | ||||
| type Module struct { | ||||
| 	server oauth.Server | ||||
| 	db     db.DB | ||||
| 	config    *config.Config | ||||
| 	processor message.Processor | ||||
| 	log       *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new auth module | ||||
| func New(srv oauth.Server, db db.DB, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||
| 	return &Module{ | ||||
| 		server: srv, | ||||
| 		db:     db, | ||||
| 		config:    config, | ||||
| 		processor: processor, | ||||
| 		log:       log, | ||||
| 	} | ||||
| } | ||||
|  | @ -68,21 +66,3 @@ func (m *Module) Route(s router.Router) error { | |||
| 	s.AttachMiddleware(m.OauthTokenMiddleware) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateTables creates the necessary tables for this module in the given database | ||||
| func (m *Module) CreateTables(db db.DB) error { | ||||
| 	models := []interface{}{ | ||||
| 		&oauth.Client{}, | ||||
| 		&oauth.Token{}, | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Application{}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, m := range models { | ||||
| 		if err := db.CreateTable(m); err != nil { | ||||
| 			return fmt.Errorf("error creating table: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | @ -28,7 +28,7 @@ import ( | |||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | @ -103,7 +103,7 @@ func (suite *AuthTestSuite) SetupTest() { | |||
| 
 | ||||
| 	log := logrus.New() | ||||
| 	log.SetLevel(logrus.TraceLevel) | ||||
| 	db, err := db.New(context.Background(), suite.config, log) | ||||
| 	db, err := db.NewPostgresService(context.Background(), suite.config, log) | ||||
| 	if err != nil { | ||||
| 		logrus.Panicf("error creating database connection: %s", err) | ||||
| 	} | ||||
|  | @ -27,8 +27,8 @@ import ( | |||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| ) | ||||
| 
 | ||||
| // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize | ||||
|  | @ -178,7 +178,7 @@ func parseAuthForm(c *gin.Context, l *logrus.Entry) error { | |||
| 	s := sessions.Default(c) | ||||
| 
 | ||||
| 	// first make sure they've filled out the authorize form with the required values | ||||
| 	form := &mastotypes.OAuthAuthorize{} | ||||
| 	form := &model.OAuthAuthorize{} | ||||
| 	if err := c.ShouldBind(form); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -20,7 +20,7 @@ package auth | |||
| 
 | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
|  | @ -24,7 +24,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
| 
 | ||||
|  | @ -23,12 +23,12 @@ import ( | |||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
|  | @ -46,18 +46,16 @@ const ( | |||
| // The goal here is to serve requested media files if the gotosocial server is configured to use local storage. | ||||
| type FileServer struct { | ||||
| 	config      *config.Config | ||||
| 	db          db.DB | ||||
| 	storage     storage.Storage | ||||
| 	processor   message.Processor | ||||
| 	log         *logrus.Logger | ||||
| 	storageBase string | ||||
| } | ||||
| 
 | ||||
| // New returns a new fileServer module | ||||
| func New(config *config.Config, db db.DB, storage storage.Storage, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||
| 	return &FileServer{ | ||||
| 		config:      config, | ||||
| 		db:          db, | ||||
| 		storage:     storage, | ||||
| 		processor:   processor, | ||||
| 		log:         log, | ||||
| 		storageBase: config.StorageConfig.ServeBasePath, | ||||
| 	} | ||||
|  | @ -25,7 +25,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| ) | ||||
| 
 | ||||
|  | @ -30,11 +30,12 @@ import ( | |||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
|  | @ -49,6 +50,7 @@ type ServeFileTestSuite struct { | |||
| 	log          *logrus.Logger | ||||
| 	storage      storage.Storage | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	processor    message.Processor | ||||
| 	mediaHandler media.Handler | ||||
| 	oauthServer  oauth.Server | ||||
| 
 | ||||
|  | @ -74,12 +76,13 @@ func (suite *ServeFileTestSuite) SetupSuite() { | |||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 
 | ||||
| 	// setup module being tested | ||||
| 	suite.fileServer = fileserver.New(suite.config, suite.db, suite.storage, suite.log).(*fileserver.FileServer) | ||||
| 	suite.fileServer = fileserver.New(suite.config, suite.processor, suite.log).(*fileserver.FileServer) | ||||
| } | ||||
| 
 | ||||
| func (suite *ServeFileTestSuite) TearDownSuite() { | ||||
|  | @ -23,13 +23,12 @@ import ( | |||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| ) | ||||
| 
 | ||||
| // BasePath is the base API path for making media requests | ||||
|  | @ -37,20 +36,16 @@ const BasePath = "/api/v1/media" | |||
| 
 | ||||
| // Module implements the ClientAPIModule interface for media | ||||
| type Module struct { | ||||
| 	mediaHandler media.Handler | ||||
| 	config    *config.Config | ||||
| 	db           db.DB | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	processor message.Processor | ||||
| 	log       *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new auth module | ||||
| func New(db db.DB, mediaHandler media.Handler, tc typeutils.TypeConverter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||
| 	return &Module{ | ||||
| 		mediaHandler: mediaHandler, | ||||
| 		config:    config, | ||||
| 		db:           db, | ||||
| 		tc:           tc, | ||||
| 		processor: processor, | ||||
| 		log:       log, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										91
									
								
								internal/api/client/media/mediacreate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								internal/api/client/media/mediacreate.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,91 @@ | |||
| /* | ||||
|    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" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // MediaCreatePOSTHandler handles requests to create/upload media attachments | ||||
| func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "statusCreatePOSTHandler") | ||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything* | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// extract the media create form from the request context | ||||
| 	l.Tracef("parsing request form: %s", c.Request.Form) | ||||
| 	form := &model.AttachmentRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||
| 		l.Debugf("could not parse form from request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | ||||
| 		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 := validateCreateMedia(form, m.config.MediaConfig); err != nil { | ||||
| 		l.Debugf("error validating form: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoAttachment, err := m.processor.MediaCreate(authed, form) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusAccepted, mastoAttachment) | ||||
| } | ||||
| 
 | ||||
| func validateCreateMedia(form *model.AttachmentRequest, config *config.MediaConfig) error { | ||||
| 	// check there actually is a file attached and it's not size 0 | ||||
| 	if form.File == nil || form.File.Size == 0 { | ||||
| 		return errors.New("no attachment given") | ||||
| 	} | ||||
| 
 | ||||
| 	// a very superficial check to see if no size limits are exceeded | ||||
| 	// we still don't actually know which media types we're dealing with but the other handlers will go into more detail there | ||||
| 	maxSize := config.MaxVideoSize | ||||
| 	if config.MaxImageSize > maxSize { | ||||
| 		maxSize = config.MaxImageSize | ||||
| 	} | ||||
| 	if form.File.Size > int64(maxSize) { | ||||
| 		return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(form.Description) < config.MinDescriptionChars || len(form.Description) > config.MaxDescriptionChars { | ||||
| 		return fmt.Errorf("image description length must be between %d and %d characters (inclusive), but provided image description was %d chars", config.MinDescriptionChars, config.MaxDescriptionChars, len(form.Description)) | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: validate focus here | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -32,12 +32,13 @@ import ( | |||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	mediamodule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media" | ||||
| 	mediamodule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
|  | @ -54,6 +55,7 @@ type MediaCreateTestSuite struct { | |||
| 	tc           typeutils.TypeConverter | ||||
| 	mediaHandler media.Handler | ||||
| 	oauthServer  oauth.Server | ||||
| 	processor    message.Processor | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
|  | @ -80,9 +82,10 @@ func (suite *MediaCreateTestSuite) SetupSuite() { | |||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) | ||||
| 
 | ||||
| 	// setup module being tested | ||||
| 	suite.mediaModule = mediamodule.New(suite.db, suite.mediaHandler, suite.tc, suite.config, suite.log).(*mediamodule.Module) | ||||
| 	suite.mediaModule = mediamodule.New(suite.config, suite.processor, suite.log).(*mediamodule.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *MediaCreateTestSuite) TearDownSuite() { | ||||
|  | @ -158,26 +161,26 @@ func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() | |||
| 	assert.NoError(suite.T(), err) | ||||
| 	fmt.Println(string(b)) | ||||
| 
 | ||||
| 	attachmentReply := &mastotypes.Attachment{} | ||||
| 	attachmentReply := &model.Attachment{} | ||||
| 	err = json.Unmarshal(b, attachmentReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	assert.Equal(suite.T(), "this is a test image -- a cool background from somewhere", attachmentReply.Description) | ||||
| 	assert.Equal(suite.T(), "image", attachmentReply.Type) | ||||
| 	assert.EqualValues(suite.T(), mastotypes.MediaMeta{ | ||||
| 		Original: mastotypes.MediaDimensions{ | ||||
| 	assert.EqualValues(suite.T(), model.MediaMeta{ | ||||
| 		Original: model.MediaDimensions{ | ||||
| 			Width:  1920, | ||||
| 			Height: 1080, | ||||
| 			Size:   "1920x1080", | ||||
| 			Aspect: 1.7777778, | ||||
| 		}, | ||||
| 		Small: mastotypes.MediaDimensions{ | ||||
| 		Small: model.MediaDimensions{ | ||||
| 			Width:  256, | ||||
| 			Height: 144, | ||||
| 			Size:   "256x144", | ||||
| 			Aspect: 1.7777778, | ||||
| 		}, | ||||
| 		Focus: mastotypes.MediaFocus{ | ||||
| 		Focus: model.MediaFocus{ | ||||
| 			X: -0.5, | ||||
| 			Y: 0.5, | ||||
| 		}, | ||||
|  | @ -19,20 +19,15 @@ | |||
| package status | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
|  | @ -80,21 +75,15 @@ const ( | |||
| // Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with statuses | ||||
| type Module struct { | ||||
| 	config    *config.Config | ||||
| 	db           db.DB | ||||
| 	mediaHandler media.Handler | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	distributor  distributor.Distributor | ||||
| 	processor message.Processor | ||||
| 	log       *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new account module | ||||
| func New(config *config.Config, db db.DB, mediaHandler media.Handler, tc typeutils.TypeConverter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||
| 	return &Module{ | ||||
| 		config:    config, | ||||
| 		db:           db, | ||||
| 		mediaHandler: mediaHandler, | ||||
| 		tc:           tc, | ||||
| 		distributor:  distributor, | ||||
| 		processor: processor, | ||||
| 		log:       log, | ||||
| 	} | ||||
| } | ||||
|  | @ -105,41 +94,12 @@ func (m *Module) Route(r router.Router) error { | |||
| 	r.AttachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler) | ||||
| 
 | ||||
| 	r.AttachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler) | ||||
| 	r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusFavePOSTHandler) | ||||
| 	r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusUnfavePOSTHandler) | ||||
| 
 | ||||
| 	r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateTables populates necessary tables in the given DB | ||||
| func (m *Module) CreateTables(db db.DB) error { | ||||
| 	models := []interface{}{ | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Block{}, | ||||
| 		>smodel.Follow{}, | ||||
| 		>smodel.FollowRequest{}, | ||||
| 		>smodel.Status{}, | ||||
| 		>smodel.StatusFave{}, | ||||
| 		>smodel.StatusBookmark{}, | ||||
| 		>smodel.StatusMute{}, | ||||
| 		>smodel.StatusPin{}, | ||||
| 		>smodel.Application{}, | ||||
| 		>smodel.EmailDomainBlock{}, | ||||
| 		>smodel.MediaAttachment{}, | ||||
| 		>smodel.Emoji{}, | ||||
| 		>smodel.Tag{}, | ||||
| 		>smodel.Mention{}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, m := range models { | ||||
| 		if err := db.CreateTable(m); err != nil { | ||||
| 			return fmt.Errorf("error creating table: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // muxHandler is a little workaround to overcome the limitations of Gin | ||||
| func (m *Module) muxHandler(c *gin.Context) { | ||||
| 	m.log.Debug("entering mux handler") | ||||
							
								
								
									
										37
									
								
								internal/api/client/status/status_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/api/client/status/status_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| package status_test | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| ) | ||||
| 
 | ||||
| type StatusStandardTestSuite struct { | ||||
| 	// standard suite interfaces | ||||
| 	suite.Suite | ||||
| 	config    *config.Config | ||||
| 	db        db.DB | ||||
| 	log       *logrus.Logger | ||||
| 	tc        typeutils.TypeConverter | ||||
| 	processor message.Processor | ||||
| 	storage   storage.Storage | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
| 	testClients      map[string]*oauth.Client | ||||
| 	testApplications map[string]*gtsmodel.Application | ||||
| 	testUsers        map[string]*gtsmodel.User | ||||
| 	testAccounts     map[string]*gtsmodel.Account | ||||
| 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||
| 	testStatuses     map[string]*gtsmodel.Status | ||||
| 
 | ||||
| 	// module being tested | ||||
| 	statusModule *status.Module | ||||
| } | ||||
							
								
								
									
										130
									
								
								internal/api/client/status/statuscreate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								internal/api/client/status/statuscreate.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,130 @@ | |||
| /* | ||||
|    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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| ) | ||||
| 
 | ||||
| // StatusCreatePOSTHandler deals with the creation of new statuses | ||||
| func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "statusCreatePOSTHandler") | ||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything* | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// First check this user/account is permitted to post new statuses. | ||||
| 	// There's no point continuing otherwise. | ||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// extract the status create form from the request context | ||||
| 	l.Tracef("parsing request form: %s", c.Request.Form) | ||||
| 	form := &model.AdvancedStatusCreateForm{} | ||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||
| 		l.Debugf("could not parse form from request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | ||||
| 		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 := validateCreateStatus(form, m.config.StatusesConfig); err != nil { | ||||
| 		l.Debugf("error validating form: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.processor.StatusCreate(authed, form) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error processing status create: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error":"bad request"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
| 
 | ||||
| func validateCreateStatus(form *model.AdvancedStatusCreateForm, config *config.StatusesConfig) error { | ||||
| 	// validate that, structurally, we have a valid status/post | ||||
| 	if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { | ||||
| 		return errors.New("no status, media, or poll provided") | ||||
| 	} | ||||
| 
 | ||||
| 	if form.MediaIDs != nil && form.Poll != nil { | ||||
| 		return errors.New("can't post media + poll in same status") | ||||
| 	} | ||||
| 
 | ||||
| 	// validate status | ||||
| 	if form.Status != "" { | ||||
| 		if len(form.Status) > config.MaxChars { | ||||
| 			return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// validate media attachments | ||||
| 	if len(form.MediaIDs) > config.MaxMediaFiles { | ||||
| 		return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles) | ||||
| 	} | ||||
| 
 | ||||
| 	// validate poll | ||||
| 	if form.Poll != nil { | ||||
| 		if form.Poll.Options == nil { | ||||
| 			return errors.New("poll with no options") | ||||
| 		} | ||||
| 		if len(form.Poll.Options) > config.PollMaxOptions { | ||||
| 			return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), config.PollMaxOptions) | ||||
| 		} | ||||
| 		for _, p := range form.Poll.Options { | ||||
| 			if len(p) > config.PollOptionMaxChars { | ||||
| 				return fmt.Errorf("poll option too long, %d characters provided but limit is %d", len(p), config.PollOptionMaxChars) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// validate spoiler text/cw | ||||
| 	if form.SpoilerText != "" { | ||||
| 		if len(form.SpoilerText) > config.CWMaxChars { | ||||
| 			return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), config.CWMaxChars) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// validate post language | ||||
| 	if form.Language != "" { | ||||
| 		if err := util.ValidateLanguage(form.Language); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -28,95 +28,45 @@ import ( | |||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||
| ) | ||||
| 
 | ||||
| type StatusCreateTestSuite struct { | ||||
| 	// standard suite interfaces | ||||
| 	suite.Suite | ||||
| 	config       *config.Config | ||||
| 	db           db.DB | ||||
| 	log          *logrus.Logger | ||||
| 	storage      storage.Storage | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	mediaHandler media.Handler | ||||
| 	oauthServer  oauth.Server | ||||
| 	distributor  distributor.Distributor | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
| 	testClients      map[string]*oauth.Client | ||||
| 	testApplications map[string]*gtsmodel.Application | ||||
| 	testUsers        map[string]*gtsmodel.User | ||||
| 	testAccounts     map[string]*gtsmodel.Account | ||||
| 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||
| 
 | ||||
| 	// module being tested | ||||
| 	statusModule *status.Module | ||||
| 	StatusStandardTestSuite | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	TEST INFRASTRUCTURE | ||||
| */ | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *StatusCreateTestSuite) SetupSuite() { | ||||
| 	// setup standard items | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.distributor = testrig.NewTestDistributor() | ||||
| 
 | ||||
| 	// setup module being tested | ||||
| 	suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.tc, suite.distributor, suite.log).(*status.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusCreateTestSuite) TearDownSuite() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusCreateTestSuite) SetupTest() { | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") | ||||
| 	suite.testTokens = testrig.NewTestTokens() | ||||
| 	suite.testClients = testrig.NewTestClients() | ||||
| 	suite.testApplications = testrig.NewTestApplications() | ||||
| 	suite.testUsers = testrig.NewTestUsers() | ||||
| 	suite.testAccounts = testrig.NewTestAccounts() | ||||
| 	suite.testAttachments = testrig.NewTestAttachments() | ||||
| 	suite.testStatuses = testrig.NewTestStatuses() | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusCreateTestSuite) SetupTest() { | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) | ||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops tables to make sure there's no data in the db | ||||
| func (suite *StatusCreateTestSuite) TearDownTest() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	ACTUAL TESTS | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
| 	TESTING: StatusCreatePOSTHandler | ||||
| */ | ||||
| 
 | ||||
| // Post a new status with some custom visibility settings | ||||
| func (suite *StatusCreateTestSuite) TestPostNewStatus() { | ||||
| 
 | ||||
|  | @ -152,16 +102,16 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { | |||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	statusReply := &mastotypes.Status{} | ||||
| 	statusReply := &model.Status{} | ||||
| 	err = json.Unmarshal(b, statusReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	assert.Equal(suite.T(), "hello hello", statusReply.SpoilerText) | ||||
| 	assert.Equal(suite.T(), "this is a brand new status! #helloworld", statusReply.Content) | ||||
| 	assert.True(suite.T(), statusReply.Sensitive) | ||||
| 	assert.Equal(suite.T(), mastotypes.VisibilityPrivate, statusReply.Visibility) | ||||
| 	assert.Equal(suite.T(), model.VisibilityPrivate, statusReply.Visibility) | ||||
| 	assert.Len(suite.T(), statusReply.Tags, 1) | ||||
| 	assert.Equal(suite.T(), mastotypes.Tag{ | ||||
| 	assert.Equal(suite.T(), model.Tag{ | ||||
| 		Name: "helloworld", | ||||
| 		URL:  "http://localhost:8080/tags/helloworld", | ||||
| 	}, statusReply.Tags[0]) | ||||
|  | @ -197,7 +147,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() { | |||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	statusReply := &mastotypes.Status{} | ||||
| 	statusReply := &model.Status{} | ||||
| 	err = json.Unmarshal(b, statusReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
|  | @ -241,7 +191,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() { | |||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b)) | ||||
| 	assert.Equal(suite.T(), `{"error":"bad request"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| // Post a reply to the status of a local user that allows replies. | ||||
|  | @ -271,14 +221,14 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() { | |||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	statusReply := &mastotypes.Status{} | ||||
| 	statusReply := &model.Status{} | ||||
| 	err = json.Unmarshal(b, statusReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	assert.Equal(suite.T(), "", statusReply.SpoilerText) | ||||
| 	assert.Equal(suite.T(), fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username), statusReply.Content) | ||||
| 	assert.False(suite.T(), statusReply.Sensitive) | ||||
| 	assert.Equal(suite.T(), mastotypes.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.Equal(suite.T(), testrig.NewTestStatuses()["local_account_2_status_1"].ID, statusReply.InReplyToID) | ||||
| 	assert.Equal(suite.T(), testrig.NewTestAccounts()["local_account_2"].ID, statusReply.InReplyToAccountID) | ||||
| 	assert.Len(suite.T(), statusReply.Mentions, 1) | ||||
|  | @ -313,14 +263,14 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() { | |||
| 
 | ||||
| 	fmt.Println(string(b)) | ||||
| 
 | ||||
| 	statusReply := &mastotypes.Status{} | ||||
| 	statusReply := &model.Status{} | ||||
| 	err = json.Unmarshal(b, statusReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	assert.Equal(suite.T(), "", statusReply.SpoilerText) | ||||
| 	assert.Equal(suite.T(), "here's an image attachment", statusReply.Content) | ||||
| 	assert.False(suite.T(), statusReply.Sensitive) | ||||
| 	assert.Equal(suite.T(), mastotypes.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) | ||||
| 
 | ||||
| 	// there should be one media attachment | ||||
| 	assert.Len(suite.T(), statusReply.MediaAttachments, 1) | ||||
							
								
								
									
										60
									
								
								internal/api/client/status/statusdelete.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/api/client/status/statusdelete.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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusDELETEHandler verifies and handles deletion of a status | ||||
| func (m *Module) StatusDELETEHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "StatusDELETEHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed so can't delete status") | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.processor.StatusDelete(authed, targetStatusID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error processing status delete: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
							
								
								
									
										60
									
								
								internal/api/client/status/statusfave.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/api/client/status/statusfave.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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusFavePOSTHandler handles fave requests against a given status ID | ||||
| func (m *Module) StatusFavePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "StatusFavePOSTHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed so can't fave status") | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.processor.StatusFave(authed, targetStatusID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error processing status fave: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
|  | @ -28,75 +28,19 @@ import ( | |||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||
| ) | ||||
| 
 | ||||
| type StatusFaveTestSuite struct { | ||||
| 	// standard suite interfaces | ||||
| 	suite.Suite | ||||
| 	config       *config.Config | ||||
| 	db           db.DB | ||||
| 	log          *logrus.Logger | ||||
| 	storage      storage.Storage | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	mediaHandler media.Handler | ||||
| 	oauthServer  oauth.Server | ||||
| 	distributor  distributor.Distributor | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
| 	testClients      map[string]*oauth.Client | ||||
| 	testApplications map[string]*gtsmodel.Application | ||||
| 	testUsers        map[string]*gtsmodel.User | ||||
| 	testAccounts     map[string]*gtsmodel.Account | ||||
| 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||
| 	testStatuses     map[string]*gtsmodel.Status | ||||
| 
 | ||||
| 	// module being tested | ||||
| 	statusModule *status.Module | ||||
| 	StatusStandardTestSuite | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	TEST INFRASTRUCTURE | ||||
| */ | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *StatusFaveTestSuite) SetupSuite() { | ||||
| 	// setup standard items | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.distributor = testrig.NewTestDistributor() | ||||
| 
 | ||||
| 	// setup module being tested | ||||
| 	suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.tc, suite.distributor, suite.log).(*status.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusFaveTestSuite) TearDownSuite() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusFaveTestSuite) SetupTest() { | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| 	suite.testTokens = testrig.NewTestTokens() | ||||
| 	suite.testClients = testrig.NewTestClients() | ||||
| 	suite.testApplications = testrig.NewTestApplications() | ||||
|  | @ -106,16 +50,22 @@ func (suite *StatusFaveTestSuite) SetupTest() { | |||
| 	suite.testStatuses = testrig.NewTestStatuses() | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops tables to make sure there's no data in the db | ||||
| func (suite *StatusFaveTestSuite) SetupTest() { | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) | ||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusFaveTestSuite) TearDownTest() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	ACTUAL TESTS | ||||
| */ | ||||
| 
 | ||||
| // fave a status | ||||
| func (suite *StatusFaveTestSuite) TestPostFave() { | ||||
| 
 | ||||
|  | @ -152,14 +102,14 @@ func (suite *StatusFaveTestSuite) TestPostFave() { | |||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	statusReply := &mastotypes.Status{} | ||||
| 	statusReply := &model.Status{} | ||||
| 	err = json.Unmarshal(b, statusReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) | ||||
| 	assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) | ||||
| 	assert.True(suite.T(), statusReply.Sensitive) | ||||
| 	assert.Equal(suite.T(), mastotypes.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.True(suite.T(), statusReply.Favourited) | ||||
| 	assert.Equal(suite.T(), 1, statusReply.FavouritesCount) | ||||
| } | ||||
|  | @ -193,13 +143,13 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() { | |||
| 	suite.statusModule.StatusFavePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 	suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unlikeable statuses | ||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), fmt.Sprintf(`{"error":"status %s not faveable"}`, targetStatus.ID), string(b)) | ||||
| 	assert.Equal(suite.T(), `{"error":"bad request"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func TestStatusFaveTestSuite(t *testing.T) { | ||||
							
								
								
									
										60
									
								
								internal/api/client/status/statusfavedby.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/api/client/status/statusfavedby.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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusFavedByGETHandler is for serving a list of accounts that have faved a given status | ||||
| func (m *Module) StatusFavedByGETHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "statusGETHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.Authed(c, false, false, false, false) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error authing status faved by request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoAccounts, err := m.processor.StatusFavedBy(authed, targetStatusID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error processing status faved by request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoAccounts) | ||||
| } | ||||
|  | @ -28,71 +28,19 @@ import ( | |||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||
| ) | ||||
| 
 | ||||
| type StatusFavedByTestSuite struct { | ||||
| 	// standard suite interfaces | ||||
| 	suite.Suite | ||||
| 	config         *config.Config | ||||
| 	db             db.DB | ||||
| 	log            *logrus.Logger | ||||
| 	storage        storage.Storage | ||||
| 	mastoConverter typeutils.TypeConverter | ||||
| 	mediaHandler   media.Handler | ||||
| 	oauthServer    oauth.Server | ||||
| 	distributor    distributor.Distributor | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
| 	testClients      map[string]*oauth.Client | ||||
| 	testApplications map[string]*gtsmodel.Application | ||||
| 	testUsers        map[string]*gtsmodel.User | ||||
| 	testAccounts     map[string]*gtsmodel.Account | ||||
| 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||
| 	testStatuses     map[string]*gtsmodel.Status | ||||
| 
 | ||||
| 	// module being tested | ||||
| 	statusModule *status.Module | ||||
| 	StatusStandardTestSuite | ||||
| } | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *StatusFavedByTestSuite) SetupSuite() { | ||||
| 	// setup standard items | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.mastoConverter = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.distributor = testrig.NewTestDistributor() | ||||
| 
 | ||||
| 	// setup module being tested | ||||
| 	suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusFavedByTestSuite) TearDownSuite() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusFavedByTestSuite) SetupTest() { | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| 	suite.testTokens = testrig.NewTestTokens() | ||||
| 	suite.testClients = testrig.NewTestClients() | ||||
| 	suite.testApplications = testrig.NewTestApplications() | ||||
|  | @ -102,16 +50,22 @@ func (suite *StatusFavedByTestSuite) SetupTest() { | |||
| 	suite.testStatuses = testrig.NewTestStatuses() | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops tables to make sure there's no data in the db | ||||
| func (suite *StatusFavedByTestSuite) SetupTest() { | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) | ||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusFavedByTestSuite) TearDownTest() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	ACTUAL TESTS | ||||
| */ | ||||
| 
 | ||||
| func (suite *StatusFavedByTestSuite) TestGetFavedBy() { | ||||
| 	t := suite.testTokens["local_account_2"] | ||||
| 	oauthToken := oauth.TokenToOauthToken(t) | ||||
|  | @ -146,7 +100,7 @@ func (suite *StatusFavedByTestSuite) TestGetFavedBy() { | |||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	accts := []mastotypes.Account{} | ||||
| 	accts := []model.Account{} | ||||
| 	err = json.Unmarshal(b, &accts) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
							
								
								
									
										60
									
								
								internal/api/client/status/statusget.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/api/client/status/statusget.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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusGETHandler is for handling requests to just get one status based on its ID | ||||
| func (m *Module) StatusGETHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "statusGETHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.Authed(c, false, false, false, false) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error authing status faved by request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.processor.StatusGet(authed, targetStatusID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error processing status get: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
|  | @ -21,92 +21,41 @@ package status_test | |||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||
| ) | ||||
| 
 | ||||
| type StatusGetTestSuite struct { | ||||
| 	// standard suite interfaces | ||||
| 	suite.Suite | ||||
| 	config       *config.Config | ||||
| 	db           db.DB | ||||
| 	log          *logrus.Logger | ||||
| 	storage      storage.Storage | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	mediaHandler media.Handler | ||||
| 	oauthServer  oauth.Server | ||||
| 	distributor  distributor.Distributor | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
| 	testClients      map[string]*oauth.Client | ||||
| 	testApplications map[string]*gtsmodel.Application | ||||
| 	testUsers        map[string]*gtsmodel.User | ||||
| 	testAccounts     map[string]*gtsmodel.Account | ||||
| 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||
| 
 | ||||
| 	// module being tested | ||||
| 	statusModule *status.Module | ||||
| 	StatusStandardTestSuite | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	TEST INFRASTRUCTURE | ||||
| */ | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *StatusGetTestSuite) SetupSuite() { | ||||
| 	// setup standard items | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.distributor = testrig.NewTestDistributor() | ||||
| 
 | ||||
| 	// setup module being tested | ||||
| 	suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.tc, suite.distributor, suite.log).(*status.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusGetTestSuite) TearDownSuite() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusGetTestSuite) SetupTest() { | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") | ||||
| 	suite.testTokens = testrig.NewTestTokens() | ||||
| 	suite.testClients = testrig.NewTestClients() | ||||
| 	suite.testApplications = testrig.NewTestApplications() | ||||
| 	suite.testUsers = testrig.NewTestUsers() | ||||
| 	suite.testAccounts = testrig.NewTestAccounts() | ||||
| 	suite.testAttachments = testrig.NewTestAttachments() | ||||
| 	suite.testStatuses = testrig.NewTestStatuses() | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusGetTestSuite) SetupTest() { | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) | ||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops tables to make sure there's no data in the db | ||||
| func (suite *StatusGetTestSuite) TearDownTest() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	ACTUAL TESTS | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
| 	TESTING: StatusGetPOSTHandler | ||||
| */ | ||||
| 
 | ||||
| // Post a new status with some custom visibility settings | ||||
| func (suite *StatusGetTestSuite) TestPostNewStatus() { | ||||
							
								
								
									
										60
									
								
								internal/api/client/status/statusunfave.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/api/client/status/statusunfave.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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusUnfavePOSTHandler is for undoing a fave on a status with a given ID | ||||
| func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "StatusUnfavePOSTHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed so can't unfave status") | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.processor.StatusUnfave(authed, targetStatusID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error processing status unfave: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
|  | @ -28,75 +28,19 @@ import ( | |||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||
| ) | ||||
| 
 | ||||
| type StatusUnfaveTestSuite struct { | ||||
| 	// standard suite interfaces | ||||
| 	suite.Suite | ||||
| 	config       *config.Config | ||||
| 	db           db.DB | ||||
| 	log          *logrus.Logger | ||||
| 	storage      storage.Storage | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	mediaHandler media.Handler | ||||
| 	oauthServer  oauth.Server | ||||
| 	distributor  distributor.Distributor | ||||
| 
 | ||||
| 	// standard suite models | ||||
| 	testTokens       map[string]*oauth.Token | ||||
| 	testClients      map[string]*oauth.Client | ||||
| 	testApplications map[string]*gtsmodel.Application | ||||
| 	testUsers        map[string]*gtsmodel.User | ||||
| 	testAccounts     map[string]*gtsmodel.Account | ||||
| 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||
| 	testStatuses     map[string]*gtsmodel.Status | ||||
| 
 | ||||
| 	// module being tested | ||||
| 	statusModule *status.Module | ||||
| 	StatusStandardTestSuite | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	TEST INFRASTRUCTURE | ||||
| */ | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *StatusUnfaveTestSuite) SetupSuite() { | ||||
| 	// setup standard items | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.distributor = testrig.NewTestDistributor() | ||||
| 
 | ||||
| 	// setup module being tested | ||||
| 	suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.tc, suite.distributor, suite.log).(*status.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusUnfaveTestSuite) TearDownSuite() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusUnfaveTestSuite) SetupTest() { | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| 	suite.testTokens = testrig.NewTestTokens() | ||||
| 	suite.testClients = testrig.NewTestClients() | ||||
| 	suite.testApplications = testrig.NewTestApplications() | ||||
|  | @ -106,16 +50,22 @@ func (suite *StatusUnfaveTestSuite) SetupTest() { | |||
| 	suite.testStatuses = testrig.NewTestStatuses() | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops tables to make sure there's no data in the db | ||||
| func (suite *StatusUnfaveTestSuite) SetupTest() { | ||||
| 	suite.config = testrig.NewTestConfig() | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.log = testrig.NewTestLog() | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) | ||||
| 	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) | ||||
| 	testrig.StandardDBSetup(suite.db) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||
| } | ||||
| 
 | ||||
| func (suite *StatusUnfaveTestSuite) TearDownTest() { | ||||
| 	testrig.StandardDBTeardown(suite.db) | ||||
| 	testrig.StandardStorageTeardown(suite.storage) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	ACTUAL TESTS | ||||
| */ | ||||
| 
 | ||||
| // unfave a status | ||||
| func (suite *StatusUnfaveTestSuite) TestPostUnfave() { | ||||
| 
 | ||||
|  | @ -153,14 +103,14 @@ func (suite *StatusUnfaveTestSuite) TestPostUnfave() { | |||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	statusReply := &mastotypes.Status{} | ||||
| 	statusReply := &model.Status{} | ||||
| 	err = json.Unmarshal(b, statusReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) | ||||
| 	assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) | ||||
| 	assert.False(suite.T(), statusReply.Sensitive) | ||||
| 	assert.Equal(suite.T(), mastotypes.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.False(suite.T(), statusReply.Favourited) | ||||
| 	assert.Equal(suite.T(), 0, statusReply.FavouritesCount) | ||||
| } | ||||
|  | @ -202,14 +152,14 @@ func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() { | |||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	statusReply := &mastotypes.Status{} | ||||
| 	statusReply := &model.Status{} | ||||
| 	err = json.Unmarshal(b, statusReply) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) | ||||
| 	assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) | ||||
| 	assert.True(suite.T(), statusReply.Sensitive) | ||||
| 	assert.Equal(suite.T(), mastotypes.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) | ||||
| 	assert.False(suite.T(), statusReply.Favourited) | ||||
| 	assert.Equal(suite.T(), 0, statusReply.FavouritesCount) | ||||
| } | ||||
|  | @ -22,7 +22,7 @@ import ( | |||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation" | ||||
|  | @ -59,7 +59,7 @@ type Module struct { | |||
| } | ||||
| 
 | ||||
| // New returns a new auth module | ||||
| func New(db db.DB, federator federation.Federator, tc typeutils.TypeConverter, config *config.Config, log *logrus.Logger) apimodule.FederationAPIModule { | ||||
| func New(db db.DB, federator federation.Federator, tc typeutils.TypeConverter, config *config.Config, log *logrus.Logger) api.FederationModule { | ||||
| 	return &Module{ | ||||
| 		federator: federator, | ||||
| 		config:    config, | ||||
|  | @ -74,17 +74,3 @@ func (m *Module) Route(s router.Router) error { | |||
| 	s.AttachHandler(http.MethodGet, UsersBasePathWithID, m.UsersGETHandler) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateTables populates necessary tables in the given DB | ||||
| func (m *Module) CreateTables(db db.DB) error { | ||||
| 	// models := []interface{}{ | ||||
| 	// 	>smodel.MediaAttachment{}, | ||||
| 	// } | ||||
| 
 | ||||
| 	// for _, m := range models { | ||||
| 	// 	if err := db.CreateTable(m); err != nil { | ||||
| 	// 		return fmt.Errorf("error creating table: %s", err) | ||||
| 	// 	} | ||||
| 	// } | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										113
									
								
								internal/api/federation/users.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								internal/api/federation/users.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | |||
| /* | ||||
|    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 federation | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
| 
 | ||||
| // UsersGETHandler should be served at https://example.org/users/:username. | ||||
| // | ||||
| // The goal here is to return the activitypub representation of an account | ||||
| // in the form of a vocab.ActivityStreamsPerson. This should only be served | ||||
| // to REMOTE SERVERS that present a valid signature on the GET request, on | ||||
| // behalf of a user, otherwise we risk leaking information about users publicly. | ||||
| // | ||||
| // And of course, the request should be refused if the account or server making the | ||||
| // request is blocked. | ||||
| func (m *Module) UsersGETHandler(c *gin.Context) { | ||||
| 	// l := m.log.WithFields(logrus.Fields{ | ||||
| 	// 	"func": "UsersGETHandler", | ||||
| 	// 	"url":  c.Request.RequestURI, | ||||
| 	// }) | ||||
| 	// requestedUsername := c.Param(UsernameKey) | ||||
| 	// if requestedUsername == "" { | ||||
| 	// 	err := errors.New("no username specified in request") | ||||
| 	// 	l.Debug(err) | ||||
| 	// 	c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	// // make sure this actually an AP request | ||||
| 	// format := c.NegotiateFormat(ActivityPubAcceptHeaders...) | ||||
| 	// if format == "" { | ||||
| 	// 	err := errors.New("could not negotiate format with given Accept header(s)") | ||||
| 	// 	l.Debug(err) | ||||
| 	// 	c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 	// l.Tracef("negotiated format: %s", format) | ||||
| 
 | ||||
| 	// // get the account the request is referring to | ||||
| 	// requestedAccount := >smodel.Account{} | ||||
| 	// if err := m.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | ||||
| 	// 	l.Errorf("database error getting account with username %s: %s", requestedUsername, err) | ||||
| 	// 	// we'll just return not authorized here to avoid giving anything away | ||||
| 	// 	c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	// // and create a transport for it | ||||
| 	// transport, err := m.federator.TransportController().NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey) | ||||
| 	// if err != nil { | ||||
| 	// 	l.Errorf("error creating transport for username %s: %s", requestedUsername, err) | ||||
| 	// 	// we'll just return not authorized here to avoid giving anything away | ||||
| 	// 	c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	// // authenticate the request | ||||
| 	// authentication, err := federation.AuthenticateFederatedRequest(transport, c.Request) | ||||
| 	// if err != nil { | ||||
| 	// 	l.Errorf("error authenticating GET user request: %s", err) | ||||
| 	// 	c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	// if !authentication.Authenticated { | ||||
| 	// 	l.Debug("request not authorized") | ||||
| 	// 	c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	// requestingAccount := >smodel.Account{} | ||||
| 	// if authentication.RequestingPublicKeyID != nil { | ||||
| 	// 	if err := m.db.GetWhere("public_key_uri", authentication.RequestingPublicKeyID.String(), requestingAccount); err != nil { | ||||
| 
 | ||||
| 	// 	} | ||||
| 	// } | ||||
| 
 | ||||
| 	// authorization, err := federation.AuthorizeFederatedRequest | ||||
| 
 | ||||
| 	// person, err := m.tc.AccountToAS(requestedAccount) | ||||
| 	// if err != nil { | ||||
| 	// 	l.Errorf("error converting account to ap person: %s", err) | ||||
| 	// 	c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	// data, err := person.Serialize() | ||||
| 	// if err != nil { | ||||
| 	// 	l.Errorf("error serializing user: %s", err) | ||||
| 	// 	c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	// c.JSON(http.StatusOK, data) | ||||
| } | ||||
|  | @ -16,9 +16,12 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| import "mime/multipart" | ||||
| import ( | ||||
| 	"mime/multipart" | ||||
| 	"net" | ||||
| ) | ||||
| 
 | ||||
| // Account represents a mastodon-api Account object, as described here: https://docs.joinmastodon.org/entities/account/ | ||||
| type Account struct { | ||||
|  | @ -86,6 +89,8 @@ type AccountCreateRequest struct { | |||
| 	Agreement bool `form:"agreement" binding:"required"` | ||||
| 	// The language of the confirmation email that will be sent | ||||
| 	Locale string `form:"locale" binding:"required"` | ||||
| 	// The IP of the sign up request, will not be parsed from the form but must be added manually | ||||
| 	IP net.IP `form:"-"` | ||||
| } | ||||
| 
 | ||||
| // UpdateCredentialsRequest represents the form submitted during a PATCH request to /api/v1/accounts/update_credentials. | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Activity represents the mastodon-api Activity type. See here: https://docs.joinmastodon.org/entities/activity/ | ||||
| type Activity struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // AdminAccountInfo represents the *admin* view of an account's details. See here: https://docs.joinmastodon.org/entities/admin-account/ | ||||
| type AdminAccountInfo struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Announcement represents an admin/moderator announcement for local users. See here: https://docs.joinmastodon.org/entities/announcement/ | ||||
| type Announcement struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // AnnouncementReaction represents a user reaction to admin/moderator announcement. See here: https://docs.joinmastodon.org/entities/announcementreaction/ | ||||
| type AnnouncementReaction struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Application represents a mastodon-api Application, as defined here: https://docs.joinmastodon.org/entities/application/. | ||||
| // Primarily, application is used for allowing apps like Tusky etc to connect to Mastodon on behalf of a user. | ||||
|  | @ -38,10 +38,10 @@ type Application struct { | |||
| 	VapidKey string `json:"vapid_key,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // ApplicationPOSTRequest represents a POST request to https://example.org/api/v1/apps. | ||||
| // ApplicationCreateRequest represents a POST request to https://example.org/api/v1/apps. | ||||
| // See here: https://docs.joinmastodon.org/methods/apps/ | ||||
| // And here: https://docs.joinmastodon.org/client/token/ | ||||
| type ApplicationPOSTRequest struct { | ||||
| type ApplicationCreateRequest struct { | ||||
| 	// A name for your application | ||||
| 	ClientName string `form:"client_name" binding:"required"` | ||||
| 	// Where the user should be redirected after authorization. | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| import "mime/multipart" | ||||
| 
 | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Card represents a rich preview card that is generated using OpenGraph tags from a URL. See here: https://docs.joinmastodon.org/entities/card/ | ||||
| type Card struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Context represents the tree around a given status. Used for reconstructing threads of statuses. See: https://docs.joinmastodon.org/entities/context/ | ||||
| type Context struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Conversation represents a conversation with "direct message" visibility. See https://docs.joinmastodon.org/entities/conversation/ | ||||
| type Conversation struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| import "mime/multipart" | ||||
| 
 | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Error represents an error message returned from the API. See https://docs.joinmastodon.org/entities/error/ | ||||
| type Error struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // FeaturedTag represents a hashtag that is featured on a profile. See https://docs.joinmastodon.org/entities/featuredtag/ | ||||
| type FeaturedTag struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Field represents a profile field as a name-value pair with optional verification. See https://docs.joinmastodon.org/entities/field/ | ||||
| type Field struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Filter represents a user-defined filter for determining which statuses should not be shown to the user. See https://docs.joinmastodon.org/entities/filter/ | ||||
| // If whole_word is true , client app should do: | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // History represents daily usage history of a hashtag. See https://docs.joinmastodon.org/entities/history/ | ||||
| type History struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // IdentityProof represents a proof from an external identity provider. See https://docs.joinmastodon.org/entities/identityproof/ | ||||
| type IdentityProof struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Instance represents the software instance of Mastodon running on this domain. See https://docs.joinmastodon.org/entities/instance/ | ||||
| type Instance struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // List represents a list of some users that the authenticated user follows. See https://docs.joinmastodon.org/entities/list/ | ||||
| type List struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Marker represents the last read position within a user's timelines. See https://docs.joinmastodon.org/entities/marker/ | ||||
| type Marker struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Mention represents the mastodon-api mention type, as documented here: https://docs.joinmastodon.org/entities/mention/ | ||||
| type Mention struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Notification represents a notification of an event relevant to the user. See https://docs.joinmastodon.org/entities/notification/ | ||||
| type Notification struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // OAuthAuthorize represents a request sent to https://example.org/oauth/authorize | ||||
| // See here: https://docs.joinmastodon.org/methods/apps/oauth/ | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Poll represents the mastodon-api poll type, as described here: https://docs.joinmastodon.org/entities/poll/ | ||||
| type Poll struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Preferences represents a user's preferences. See https://docs.joinmastodon.org/entities/preferences/ | ||||
| type Preferences struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // PushSubscription represents a subscription to the push streaming server. See https://docs.joinmastodon.org/entities/pushsubscription/ | ||||
| type PushSubscription struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Relationship represents a relationship between accounts. See https://docs.joinmastodon.org/entities/relationship/ | ||||
| type Relationship struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Results represents the results of a search. See https://docs.joinmastodon.org/entities/results/ | ||||
| type Results struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // ScheduledStatus represents a status that will be published at a future scheduled date. See https://docs.joinmastodon.org/entities/scheduledstatus/ | ||||
| type ScheduledStatus struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Source represents display or publishing preferences of user's own account. | ||||
| // Returned as an additional entity when verifying and updated credentials, as an attribute of Account. | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Status represents a mastodon-api Status type, as defined here: https://docs.joinmastodon.org/entities/status/ | ||||
| type Status struct { | ||||
|  | @ -118,3 +118,21 @@ const ( | |||
| 	// VisibilityDirect means visible only to tagged recipients | ||||
| 	VisibilityDirect Visibility = "direct" | ||||
| ) | ||||
| 
 | ||||
| type AdvancedStatusCreateForm struct { | ||||
| 	StatusCreateRequest | ||||
| 	AdvancedVisibilityFlagsForm | ||||
| } | ||||
| 
 | ||||
| type AdvancedVisibilityFlagsForm struct { | ||||
| 	// The gotosocial visibility model | ||||
| 	VisibilityAdvanced *string `form:"visibility_advanced"` | ||||
| 	// This status will be federated beyond the local timeline(s) | ||||
| 	Federated *bool `form:"federated"` | ||||
| 	// This status can be boosted/reblogged | ||||
| 	Boostable *bool `form:"boostable"` | ||||
| 	// This status can be replied to | ||||
| 	Replyable *bool `form:"replyable"` | ||||
| 	// This status can be liked/faved | ||||
| 	Likeable *bool `form:"likeable"` | ||||
| } | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Tag represents a hashtag used within the content of a status. See https://docs.joinmastodon.org/entities/tag/ | ||||
| type Tag struct { | ||||
|  | @ -16,7 +16,7 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| package model | ||||
| 
 | ||||
| // Token represents an OAuth token used for authenticating with the API and performing actions.. See https://docs.joinmastodon.org/entities/token/ | ||||
| type Token struct { | ||||
|  | @ -20,9 +20,8 @@ package security | |||
| 
 | ||||
| import ( | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| ) | ||||
| 
 | ||||
|  | @ -33,7 +32,7 @@ type Module struct { | |||
| } | ||||
| 
 | ||||
| // New returns a new security module | ||||
| func New(config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule { | ||||
| func New(config *config.Config, log *logrus.Logger) api.ClientModule { | ||||
| 	return &Module{ | ||||
| 		config: config, | ||||
| 		log:    log, | ||||
|  | @ -45,8 +44,3 @@ func (m *Module) Route(s router.Router) error { | |||
| 	s.AttachMiddleware(m.FlocBlock) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateTables doesn't do diddly squat at the moment, it's just for fulfilling the interface | ||||
| func (m *Module) CreateTables(db db.DB) error { | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,551 +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 account_test | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"mime/multipart" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/account" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 
 | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/oauth2/v4" | ||||
| 	"github.com/superseriousbusiness/oauth2/v4/models" | ||||
| 	oauthmodels "github.com/superseriousbusiness/oauth2/v4/models" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
| 
 | ||||
| type AccountCreateTestSuite struct { | ||||
| 	suite.Suite | ||||
| 	config               *config.Config | ||||
| 	log                  *logrus.Logger | ||||
| 	testAccountLocal     *gtsmodel.Account | ||||
| 	testApplication      *gtsmodel.Application | ||||
| 	testToken            oauth2.TokenInfo | ||||
| 	mockOauthServer      *oauth.MockServer | ||||
| 	mockStorage          *storage.MockStorage | ||||
| 	mediaHandler         media.Handler | ||||
| 	mastoConverter       typeutils.TypeConverter | ||||
| 	db                   db.DB | ||||
| 	accountModule        *account.Module | ||||
| 	newUserFormHappyPath url.Values | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	TEST INFRASTRUCTURE | ||||
| */ | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *AccountCreateTestSuite) SetupSuite() { | ||||
| 	// some of our subsequent entities need a log so create this here | ||||
| 	log := logrus.New() | ||||
| 	log.SetLevel(logrus.TraceLevel) | ||||
| 	suite.log = log | ||||
| 
 | ||||
| 	suite.testAccountLocal = >smodel.Account{ | ||||
| 		ID:       uuid.NewString(), | ||||
| 		Username: "test_user", | ||||
| 	} | ||||
| 
 | ||||
| 	// can use this test application throughout | ||||
| 	suite.testApplication = >smodel.Application{ | ||||
| 		ID:           "weeweeeeeeeeeeeeee", | ||||
| 		Name:         "a test application", | ||||
| 		Website:      "https://some-application-website.com", | ||||
| 		RedirectURI:  "http://localhost:8080", | ||||
| 		ClientID:     "a-known-client-id", | ||||
| 		ClientSecret: "some-secret", | ||||
| 		Scopes:       "read", | ||||
| 		VapidKey:     "aaaaaa-aaaaaaaa-aaaaaaaaaaa", | ||||
| 	} | ||||
| 
 | ||||
| 	// can use this test token throughout | ||||
| 	suite.testToken = &oauthmodels.Token{ | ||||
| 		ClientID:      "a-known-client-id", | ||||
| 		RedirectURI:   "http://localhost:8080", | ||||
| 		Scope:         "read", | ||||
| 		Code:          "123456789", | ||||
| 		CodeCreateAt:  time.Now(), | ||||
| 		CodeExpiresIn: time.Duration(10 * time.Minute), | ||||
| 	} | ||||
| 
 | ||||
| 	// Direct config to local postgres instance | ||||
| 	c := config.Empty() | ||||
| 	c.Protocol = "http" | ||||
| 	c.Host = "localhost" | ||||
| 	c.DBConfig = &config.DBConfig{ | ||||
| 		Type:            "postgres", | ||||
| 		Address:         "localhost", | ||||
| 		Port:            5432, | ||||
| 		User:            "postgres", | ||||
| 		Password:        "postgres", | ||||
| 		Database:        "postgres", | ||||
| 		ApplicationName: "gotosocial", | ||||
| 	} | ||||
| 	c.MediaConfig = &config.MediaConfig{ | ||||
| 		MaxImageSize: 2 << 20, | ||||
| 	} | ||||
| 	c.StorageConfig = &config.StorageConfig{ | ||||
| 		Backend:       "local", | ||||
| 		BasePath:      "/tmp", | ||||
| 		ServeProtocol: "http", | ||||
| 		ServeHost:     "localhost", | ||||
| 		ServeBasePath: "/fileserver/media", | ||||
| 	} | ||||
| 	suite.config = c | ||||
| 
 | ||||
| 	// use an actual database for this, because it's just easier than mocking one out | ||||
| 	database, err := db.New(context.Background(), c, log) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 	suite.db = database | ||||
| 
 | ||||
| 	// we need to mock the oauth server because account creation needs it to create a new token | ||||
| 	suite.mockOauthServer = &oauth.MockServer{} | ||||
| 	suite.mockOauthServer.On("GenerateUserAccessToken", suite.testToken, suite.testApplication.ClientSecret, mock.AnythingOfType("string")).Run(func(args mock.Arguments) { | ||||
| 		l := suite.log.WithField("func", "GenerateUserAccessToken") | ||||
| 		token := args.Get(0).(oauth2.TokenInfo) | ||||
| 		l.Infof("received token %+v", token) | ||||
| 		clientSecret := args.Get(1).(string) | ||||
| 		l.Infof("received clientSecret %+v", clientSecret) | ||||
| 		userID := args.Get(2).(string) | ||||
| 		l.Infof("received userID %+v", userID) | ||||
| 	}).Return(&models.Token{ | ||||
| 		Access: "we're authorized now!", | ||||
| 	}, nil) | ||||
| 
 | ||||
| 	suite.mockStorage = &storage.MockStorage{} | ||||
| 	// We don't need storage to do anything for these tests, so just simulate a success and do nothing -- we won't need to return anything from storage | ||||
| 	suite.mockStorage.On("StoreFileAt", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil) | ||||
| 
 | ||||
| 	// set a media handler because some handlers (eg update credentials) need to upload media (new header/avatar) | ||||
| 	suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log) | ||||
| 
 | ||||
| 	suite.mastoConverter = typeutils.NewConverter(suite.config, suite.db) | ||||
| 
 | ||||
| 	// and finally here's the thing we're actually testing! | ||||
| 	suite.accountModule = account.New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*account.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *AccountCreateTestSuite) TearDownSuite() { | ||||
| 	if err := suite.db.Stop(context.Background()); err != nil { | ||||
| 		logrus.Panicf("error closing db connection: %s", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // SetupTest creates a db connection and creates necessary tables before each test | ||||
| func (suite *AccountCreateTestSuite) SetupTest() { | ||||
| 	// create all the tables we might need in thie suite | ||||
| 	models := []interface{}{ | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Follow{}, | ||||
| 		>smodel.FollowRequest{}, | ||||
| 		>smodel.Status{}, | ||||
| 		>smodel.Application{}, | ||||
| 		>smodel.EmailDomainBlock{}, | ||||
| 		>smodel.MediaAttachment{}, | ||||
| 	} | ||||
| 	for _, m := range models { | ||||
| 		if err := suite.db.CreateTable(m); err != nil { | ||||
| 			logrus.Panicf("db connection error: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// form to submit for happy path account create requests -- this will be changed inside tests so it's better to set it before each test | ||||
| 	suite.newUserFormHappyPath = url.Values{ | ||||
| 		"reason":    []string{"a very good reason that's at least 40 characters i swear"}, | ||||
| 		"username":  []string{"test_user"}, | ||||
| 		"email":     []string{"user@example.org"}, | ||||
| 		"password":  []string{"very-strong-password"}, | ||||
| 		"agreement": []string{"true"}, | ||||
| 		"locale":    []string{"en"}, | ||||
| 	} | ||||
| 
 | ||||
| 	// same with accounts config | ||||
| 	suite.config.AccountsConfig = &config.AccountsConfig{ | ||||
| 		OpenRegistration: true, | ||||
| 		RequireApproval:  true, | ||||
| 		ReasonRequired:   true, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops tables to make sure there's no data in the db | ||||
| func (suite *AccountCreateTestSuite) TearDownTest() { | ||||
| 
 | ||||
| 	// remove all the tables we might have used so it's clear for the next test | ||||
| 	models := []interface{}{ | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Follow{}, | ||||
| 		>smodel.FollowRequest{}, | ||||
| 		>smodel.Status{}, | ||||
| 		>smodel.Application{}, | ||||
| 		>smodel.EmailDomainBlock{}, | ||||
| 		>smodel.MediaAttachment{}, | ||||
| 	} | ||||
| 	for _, m := range models { | ||||
| 		if err := suite.db.DropTable(m); err != nil { | ||||
| 			logrus.Panicf("error dropping table: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	ACTUAL TESTS | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
| 	TESTING: AccountCreatePOSTHandler | ||||
| */ | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerSuccessful checks the happy path for an account creation request: all the fields provided are valid, | ||||
| // and at the end of it a new user and account should be added into the database. | ||||
| // | ||||
| // This is the handler served at /api/v1/accounts as POST | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 
 | ||||
| 	// 1. we should have OK from our call to the function | ||||
| 	suite.EqualValues(http.StatusOK, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have a token in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	t := &mastotypes.Token{} | ||||
| 	err = json.Unmarshal(b, t) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), "we're authorized now!", t.AccessToken) | ||||
| 
 | ||||
| 	// check new account | ||||
| 
 | ||||
| 	// 1. we should be able to get the new account from the db | ||||
| 	acct := >smodel.Account{} | ||||
| 	err = suite.db.GetLocalAccountByUsername("test_user", acct) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.NotNil(suite.T(), acct) | ||||
| 	// 2. reason should be set | ||||
| 	assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("reason"), acct.Reason) | ||||
| 	// 3. display name should be equal to username by default | ||||
| 	assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("username"), acct.DisplayName) | ||||
| 	// 4. domain should be nil because this is a local account | ||||
| 	assert.Nil(suite.T(), nil, acct.Domain) | ||||
| 	// 5. id should be set and parseable as a uuid | ||||
| 	assert.NotNil(suite.T(), acct.ID) | ||||
| 	_, err = uuid.Parse(acct.ID) | ||||
| 	assert.Nil(suite.T(), err) | ||||
| 	// 6. private and public key should be set | ||||
| 	assert.NotNil(suite.T(), acct.PrivateKey) | ||||
| 	assert.NotNil(suite.T(), acct.PublicKey) | ||||
| 
 | ||||
| 	// check new user | ||||
| 
 | ||||
| 	// 1. we should be able to get the new user from the db | ||||
| 	usr := >smodel.User{} | ||||
| 	err = suite.db.GetWhere("unconfirmed_email", suite.newUserFormHappyPath.Get("email"), usr) | ||||
| 	assert.Nil(suite.T(), err) | ||||
| 	assert.NotNil(suite.T(), usr) | ||||
| 
 | ||||
| 	// 2. user should have account id set to account we got above | ||||
| 	assert.Equal(suite.T(), acct.ID, usr.AccountID) | ||||
| 
 | ||||
| 	// 3. id should be set and parseable as a uuid | ||||
| 	assert.NotNil(suite.T(), usr.ID) | ||||
| 	_, err = uuid.Parse(usr.ID) | ||||
| 	assert.Nil(suite.T(), err) | ||||
| 
 | ||||
| 	// 4. locale should be equal to what we requested | ||||
| 	assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("locale"), usr.Locale) | ||||
| 
 | ||||
| 	// 5. created by application id should be equal to the app id | ||||
| 	assert.Equal(suite.T(), suite.testApplication.ID, usr.CreatedByApplicationID) | ||||
| 
 | ||||
| 	// 6. password should be matcheable to what we set above | ||||
| 	err = bcrypt.CompareHashAndPassword([]byte(usr.EncryptedPassword), []byte(suite.newUserFormHappyPath.Get("password"))) | ||||
| 	assert.Nil(suite.T(), err) | ||||
| } | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no authorization is provided: | ||||
| // only registered applications can create accounts, and we don't provide one here. | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoAuth() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 
 | ||||
| 	// 1. we should have forbidden from our call to the function because we didn't auth | ||||
| 	suite.EqualValues(http.StatusForbidden, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no form is provided at all. | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoForm() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"missing one or more required form values"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerWeakPassword makes sure that the handler fails when a weak password is provided | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeakPassword() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 	// set a weak password | ||||
| 	ctx.Request.Form.Set("password", "weak") | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerWeirdLocale makes sure that the handler fails when a weird locale is provided | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeirdLocale() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 	// set an invalid locale | ||||
| 	ctx.Request.Form.Set("locale", "neverneverland") | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"language: tag is not well-formed"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerRegistrationsClosed makes sure that the handler fails when registrations are closed | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerRegistrationsClosed() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 
 | ||||
| 	// close registrations | ||||
| 	suite.config.AccountsConfig.OpenRegistration = false | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"registration is not open for this server"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when no reason is provided but one is required | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerReasonNotProvided() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 
 | ||||
| 	// remove reason | ||||
| 	ctx.Request.Form.Set("reason", "") | ||||
| 
 | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"no reason provided"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| // TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when a crappy reason is presented but a good one is required | ||||
| func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerInsufficientReason() { | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting | ||||
| 	ctx.Request.Form = suite.newUserFormHappyPath | ||||
| 
 | ||||
| 	// remove reason | ||||
| 	ctx.Request.Form.Set("reason", "just cuz") | ||||
| 
 | ||||
| 	suite.accountModule.AccountCreatePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	b, err := ioutil.ReadAll(result.Body) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	assert.Equal(suite.T(), `{"error":"reason should be at least 40 chars but 'just cuz' was 8"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	TESTING: AccountUpdateCredentialsPATCHHandler | ||||
| */ | ||||
| 
 | ||||
| func (suite *AccountCreateTestSuite) TestAccountUpdateCredentialsPATCHHandler() { | ||||
| 
 | ||||
| 	// put test local account in db | ||||
| 	err := suite.db.Put(suite.testAccountLocal) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	// attach avatar to request | ||||
| 	aviFile, err := os.Open("../../media/test/test-jpeg.jpg") | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	body := &bytes.Buffer{} | ||||
| 	writer := multipart.NewWriter(body) | ||||
| 
 | ||||
| 	part, err := writer.CreateFormFile("avatar", "test-jpeg.jpg") | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	_, err = io.Copy(part, aviFile) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	err = aviFile.Close() | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	err = writer.Close() | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), body) // the endpoint we're hitting | ||||
| 	ctx.Request.Header.Set("Content-Type", writer.FormDataContentType()) | ||||
| 	suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 
 | ||||
| 	// 1. we should have OK because our request was valid | ||||
| 	suite.EqualValues(http.StatusOK, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	// TODO: implement proper checks here | ||||
| 	// | ||||
| 	// b, err := ioutil.ReadAll(result.Body) | ||||
| 	// assert.NoError(suite.T(), err) | ||||
| 	// assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func TestAccountCreateTestSuite(t *testing.T) { | ||||
| 	suite.Run(t, new(AccountCreateTestSuite)) | ||||
| } | ||||
|  | @ -1,260 +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 account | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"mime/multipart" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| ) | ||||
| 
 | ||||
| // AccountUpdateCredentialsPATCHHandler allows a user to modify their account/profile settings. | ||||
| // It should be served as a PATCH at /api/v1/accounts/update_credentials | ||||
| // | ||||
| // TODO: this can be optimized massively by building up a picture of what we want the new account | ||||
| // details to be, and then inserting it all in the database at once. As it is, we do queries one-by-one | ||||
| // which is not gonna make the database very happy when lots of requests are going through. | ||||
| // This way it would also be safer because the update won't happen until *all* the fields are validated. | ||||
| // Otherwise we risk doing a partial update and that's gonna cause probllleeemmmsss. | ||||
| func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "accountUpdateCredentialsPATCHHandler") | ||||
| 	authed, err := oauth.MustAuth(c, true, false, false, true) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 	l.Tracef("retrieved account %+v", authed.Account.ID) | ||||
| 
 | ||||
| 	l.Trace("parsing request form") | ||||
| 	form := &mastotypes.UpdateCredentialsRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||
| 		l.Debugf("could not parse form from request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// if everything on the form is nil, then nothing has been set and we shouldn't continue | ||||
| 	if form.Discoverable == nil && form.Bot == nil && form.DisplayName == nil && form.Note == nil && form.Avatar == nil && form.Header == nil && form.Locked == nil && form.Source == nil && form.FieldsAttributes == nil { | ||||
| 		l.Debugf("could not parse form from request") | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Discoverable != nil { | ||||
| 		if err := m.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil { | ||||
| 			l.Debugf("error updating discoverable: %s", err) | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Bot != nil { | ||||
| 		if err := m.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil { | ||||
| 			l.Debugf("error updating bot: %s", err) | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if form.DisplayName != nil { | ||||
| 		if err := util.ValidateDisplayName(*form.DisplayName); err != nil { | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		if err := m.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil { | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Note != nil { | ||||
| 		if err := util.ValidateNote(*form.Note); err != nil { | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		if err := m.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, >smodel.Account{}); err != nil { | ||||
| 			l.Debugf("error updating note: %s", err) | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Avatar != nil && form.Avatar.Size != 0 { | ||||
| 		avatarInfo, err := m.UpdateAccountAvatar(form.Avatar, authed.Account.ID) | ||||
| 		if err != nil { | ||||
| 			l.Debugf("could not update avatar for account %s: %s", authed.Account.ID, err) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo) | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Header != nil && form.Header.Size != 0 { | ||||
| 		headerInfo, err := m.UpdateAccountHeader(form.Header, authed.Account.ID) | ||||
| 		if err != nil { | ||||
| 			l.Debugf("could not update header for account %s: %s", authed.Account.ID, err) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo) | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Locked != nil { | ||||
| 		if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil { | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Source != nil { | ||||
| 		if form.Source.Language != nil { | ||||
| 			if err := util.ValidateLanguage(*form.Source.Language); err != nil { | ||||
| 				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 				return | ||||
| 			} | ||||
| 			if err := m.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil { | ||||
| 				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Source.Sensitive != nil { | ||||
| 			if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil { | ||||
| 				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Source.Privacy != nil { | ||||
| 			if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil { | ||||
| 				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 				return | ||||
| 			} | ||||
| 			if err := m.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil { | ||||
| 				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// if form.FieldsAttributes != nil { | ||||
| 	// 	// TODO: parse fields attributes nicely and update | ||||
| 	// } | ||||
| 
 | ||||
| 	// fetch the account with all updated values set | ||||
| 	updatedAccount := >smodel.Account{} | ||||
| 	if err := m.db.GetByID(authed.Account.ID, updatedAccount); err != nil { | ||||
| 		l.Debugf("could not fetch updated account %s: %s", authed.Account.ID, err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	acctSensitive, err := m.tc.AccountToMastoSensitive(updatedAccount) | ||||
| 	if err != nil { | ||||
| 		l.Tracef("could not convert account into mastosensitive account: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("conversion successful, returning OK and mastosensitive account %+v", acctSensitive) | ||||
| 	c.JSON(http.StatusOK, acctSensitive) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	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 (m *Module) UpdateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { | ||||
| 	var err error | ||||
| 	if int(avatar.Size) > m.config.MediaConfig.MaxImageSize { | ||||
| 		err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, m.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 := m.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.MediaAvatar) | ||||
| 	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 (m *Module) UpdateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { | ||||
| 	var err error | ||||
| 	if int(header.Size) > m.config.MediaConfig.MaxImageSize { | ||||
| 		err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, m.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 := m.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.MediaHeader) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error processing header: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return headerInfo, f.Close() | ||||
| } | ||||
|  | @ -1,303 +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 account_test | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"mime/multipart" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/apimodule/account" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||
| 	"github.com/superseriousbusiness/oauth2/v4" | ||||
| 	"github.com/superseriousbusiness/oauth2/v4/models" | ||||
| 	oauthmodels "github.com/superseriousbusiness/oauth2/v4/models" | ||||
| ) | ||||
| 
 | ||||
| type AccountUpdateTestSuite struct { | ||||
| 	suite.Suite | ||||
| 	config               *config.Config | ||||
| 	log                  *logrus.Logger | ||||
| 	testAccountLocal     *gtsmodel.Account | ||||
| 	testApplication      *gtsmodel.Application | ||||
| 	testToken            oauth2.TokenInfo | ||||
| 	mockOauthServer      *oauth.MockServer | ||||
| 	mockStorage          *storage.MockStorage | ||||
| 	mediaHandler         media.Handler | ||||
| 	mastoConverter       typeutils.TypeConverter | ||||
| 	db                   db.DB | ||||
| 	accountModule        *account.Module | ||||
| 	newUserFormHappyPath url.Values | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	TEST INFRASTRUCTURE | ||||
| */ | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *AccountUpdateTestSuite) SetupSuite() { | ||||
| 	// some of our subsequent entities need a log so create this here | ||||
| 	log := logrus.New() | ||||
| 	log.SetLevel(logrus.TraceLevel) | ||||
| 	suite.log = log | ||||
| 
 | ||||
| 	suite.testAccountLocal = >smodel.Account{ | ||||
| 		ID:       uuid.NewString(), | ||||
| 		Username: "test_user", | ||||
| 	} | ||||
| 
 | ||||
| 	// can use this test application throughout | ||||
| 	suite.testApplication = >smodel.Application{ | ||||
| 		ID:           "weeweeeeeeeeeeeeee", | ||||
| 		Name:         "a test application", | ||||
| 		Website:      "https://some-application-website.com", | ||||
| 		RedirectURI:  "http://localhost:8080", | ||||
| 		ClientID:     "a-known-client-id", | ||||
| 		ClientSecret: "some-secret", | ||||
| 		Scopes:       "read", | ||||
| 		VapidKey:     "aaaaaa-aaaaaaaa-aaaaaaaaaaa", | ||||
| 	} | ||||
| 
 | ||||
| 	// can use this test token throughout | ||||
| 	suite.testToken = &oauthmodels.Token{ | ||||
| 		ClientID:      "a-known-client-id", | ||||
| 		RedirectURI:   "http://localhost:8080", | ||||
| 		Scope:         "read", | ||||
| 		Code:          "123456789", | ||||
| 		CodeCreateAt:  time.Now(), | ||||
| 		CodeExpiresIn: time.Duration(10 * time.Minute), | ||||
| 	} | ||||
| 
 | ||||
| 	// Direct config to local postgres instance | ||||
| 	c := config.Empty() | ||||
| 	c.Protocol = "http" | ||||
| 	c.Host = "localhost" | ||||
| 	c.DBConfig = &config.DBConfig{ | ||||
| 		Type:            "postgres", | ||||
| 		Address:         "localhost", | ||||
| 		Port:            5432, | ||||
| 		User:            "postgres", | ||||
| 		Password:        "postgres", | ||||
| 		Database:        "postgres", | ||||
| 		ApplicationName: "gotosocial", | ||||
| 	} | ||||
| 	c.MediaConfig = &config.MediaConfig{ | ||||
| 		MaxImageSize: 2 << 20, | ||||
| 	} | ||||
| 	c.StorageConfig = &config.StorageConfig{ | ||||
| 		Backend:       "local", | ||||
| 		BasePath:      "/tmp", | ||||
| 		ServeProtocol: "http", | ||||
| 		ServeHost:     "localhost", | ||||
| 		ServeBasePath: "/fileserver/media", | ||||
| 	} | ||||
| 	suite.config = c | ||||
| 
 | ||||
| 	// use an actual database for this, because it's just easier than mocking one out | ||||
| 	database, err := db.New(context.Background(), c, log) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 	suite.db = database | ||||
| 
 | ||||
| 	// we need to mock the oauth server because account creation needs it to create a new token | ||||
| 	suite.mockOauthServer = &oauth.MockServer{} | ||||
| 	suite.mockOauthServer.On("GenerateUserAccessToken", suite.testToken, suite.testApplication.ClientSecret, mock.AnythingOfType("string")).Run(func(args mock.Arguments) { | ||||
| 		l := suite.log.WithField("func", "GenerateUserAccessToken") | ||||
| 		token := args.Get(0).(oauth2.TokenInfo) | ||||
| 		l.Infof("received token %+v", token) | ||||
| 		clientSecret := args.Get(1).(string) | ||||
| 		l.Infof("received clientSecret %+v", clientSecret) | ||||
| 		userID := args.Get(2).(string) | ||||
| 		l.Infof("received userID %+v", userID) | ||||
| 	}).Return(&models.Token{ | ||||
| 		Code: "we're authorized now!", | ||||
| 	}, nil) | ||||
| 
 | ||||
| 	suite.mockStorage = &storage.MockStorage{} | ||||
| 	// We don't need storage to do anything for these tests, so just simulate a success and do nothing -- we won't need to return anything from storage | ||||
| 	suite.mockStorage.On("StoreFileAt", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil) | ||||
| 
 | ||||
| 	// set a media handler because some handlers (eg update credentials) need to upload media (new header/avatar) | ||||
| 	suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log) | ||||
| 
 | ||||
| 	suite.mastoConverter = typeutils.NewConverter(suite.config, suite.db) | ||||
| 
 | ||||
| 	// and finally here's the thing we're actually testing! | ||||
| 	suite.accountModule = account.New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*account.Module) | ||||
| } | ||||
| 
 | ||||
| func (suite *AccountUpdateTestSuite) TearDownSuite() { | ||||
| 	if err := suite.db.Stop(context.Background()); err != nil { | ||||
| 		logrus.Panicf("error closing db connection: %s", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // SetupTest creates a db connection and creates necessary tables before each test | ||||
| func (suite *AccountUpdateTestSuite) SetupTest() { | ||||
| 	// create all the tables we might need in thie suite | ||||
| 	models := []interface{}{ | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Follow{}, | ||||
| 		>smodel.FollowRequest{}, | ||||
| 		>smodel.Status{}, | ||||
| 		>smodel.Application{}, | ||||
| 		>smodel.EmailDomainBlock{}, | ||||
| 		>smodel.MediaAttachment{}, | ||||
| 	} | ||||
| 	for _, m := range models { | ||||
| 		if err := suite.db.CreateTable(m); err != nil { | ||||
| 			logrus.Panicf("db connection error: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// form to submit for happy path account create requests -- this will be changed inside tests so it's better to set it before each test | ||||
| 	suite.newUserFormHappyPath = url.Values{ | ||||
| 		"reason":    []string{"a very good reason that's at least 40 characters i swear"}, | ||||
| 		"username":  []string{"test_user"}, | ||||
| 		"email":     []string{"user@example.org"}, | ||||
| 		"password":  []string{"very-strong-password"}, | ||||
| 		"agreement": []string{"true"}, | ||||
| 		"locale":    []string{"en"}, | ||||
| 	} | ||||
| 
 | ||||
| 	// same with accounts config | ||||
| 	suite.config.AccountsConfig = &config.AccountsConfig{ | ||||
| 		OpenRegistration: true, | ||||
| 		RequireApproval:  true, | ||||
| 		ReasonRequired:   true, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops tables to make sure there's no data in the db | ||||
| func (suite *AccountUpdateTestSuite) TearDownTest() { | ||||
| 
 | ||||
| 	// remove all the tables we might have used so it's clear for the next test | ||||
| 	models := []interface{}{ | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Follow{}, | ||||
| 		>smodel.FollowRequest{}, | ||||
| 		>smodel.Status{}, | ||||
| 		>smodel.Application{}, | ||||
| 		>smodel.EmailDomainBlock{}, | ||||
| 		>smodel.MediaAttachment{}, | ||||
| 	} | ||||
| 	for _, m := range models { | ||||
| 		if err := suite.db.DropTable(m); err != nil { | ||||
| 			logrus.Panicf("error dropping table: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	ACTUAL TESTS | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
| 	TESTING: AccountUpdateCredentialsPATCHHandler | ||||
| */ | ||||
| 
 | ||||
| func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() { | ||||
| 
 | ||||
| 	// put test local account in db | ||||
| 	err := suite.db.Put(suite.testAccountLocal) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	// attach avatar to request form | ||||
| 	avatarFile, err := os.Open("../../media/test/test-jpeg.jpg") | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 	body := &bytes.Buffer{} | ||||
| 	writer := multipart.NewWriter(body) | ||||
| 
 | ||||
| 	avatarPart, err := writer.CreateFormFile("avatar", "test-jpeg.jpg") | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	_, err = io.Copy(avatarPart, avatarFile) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	err = avatarFile.Close() | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	// set display name to a new value | ||||
| 	displayNamePart, err := writer.CreateFormField("display_name") | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	_, err = io.Copy(displayNamePart, bytes.NewBufferString("test_user_wohoah")) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	// set locked to true | ||||
| 	lockedPart, err := writer.CreateFormField("locked") | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	_, err = io.Copy(lockedPart, bytes.NewBufferString("true")) | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	// close the request writer, the form is now prepared | ||||
| 	err = writer.Close() | ||||
| 	assert.NoError(suite.T(), err) | ||||
| 
 | ||||
| 	// setup | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, _ := gin.CreateTestContext(recorder) | ||||
| 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal) | ||||
| 	ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) | ||||
| 	ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), body) // the endpoint we're hitting | ||||
| 	ctx.Request.Header.Set("Content-Type", writer.FormDataContentType()) | ||||
| 	suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) | ||||
| 
 | ||||
| 	// check response | ||||
| 
 | ||||
| 	// 1. we should have OK because our request was valid | ||||
| 	suite.EqualValues(http.StatusOK, recorder.Code) | ||||
| 
 | ||||
| 	// 2. we should have an error message in the result body | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 	// TODO: implement proper checks here | ||||
| 	// | ||||
| 	// b, err := ioutil.ReadAll(result.Body) | ||||
| 	// assert.NoError(suite.T(), err) | ||||
| 	// assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func TestAccountUpdateTestSuite(t *testing.T) { | ||||
| 	suite.Run(t, new(AccountUpdateTestSuite)) | ||||
| } | ||||
|  | @ -1,119 +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 app | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // AppsPOSTHandler should be served at https://example.org/api/v1/apps | ||||
| // It is equivalent to: https://docs.joinmastodon.org/methods/apps/ | ||||
| func (m *Module) AppsPOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "AppsPOSTHandler") | ||||
| 	l.Trace("entering AppsPOSTHandler") | ||||
| 
 | ||||
| 	form := &mastotypes.ApplicationPOSTRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil { | ||||
| 		c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// permitted length for most fields | ||||
| 	permittedLength := 64 | ||||
| 	// redirect can be a bit bigger because we probably need to encode data in the redirect uri | ||||
| 	permittedRedirect := 256 | ||||
| 
 | ||||
| 	// check lengths of fields before proceeding so the user can't spam huge entries into the database | ||||
| 	if len(form.ClientName) > permittedLength { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", permittedLength)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.Website) > permittedLength { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", permittedLength)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.RedirectURIs) > permittedRedirect { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", permittedRedirect)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.Scopes) > permittedLength { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", permittedLength)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// set default 'read' for scopes if it's not set, this follows the default of the mastodon api https://docs.joinmastodon.org/methods/apps/ | ||||
| 	var scopes string | ||||
| 	if form.Scopes == "" { | ||||
| 		scopes = "read" | ||||
| 	} else { | ||||
| 		scopes = form.Scopes | ||||
| 	} | ||||
| 
 | ||||
| 	// generate new IDs for this application and its associated client | ||||
| 	clientID := uuid.NewString() | ||||
| 	clientSecret := uuid.NewString() | ||||
| 	vapidKey := uuid.NewString() | ||||
| 
 | ||||
| 	// generate the application to put in the database | ||||
| 	app := >smodel.Application{ | ||||
| 		Name:         form.ClientName, | ||||
| 		Website:      form.Website, | ||||
| 		RedirectURI:  form.RedirectURIs, | ||||
| 		ClientID:     clientID, | ||||
| 		ClientSecret: clientSecret, | ||||
| 		Scopes:       scopes, | ||||
| 		VapidKey:     vapidKey, | ||||
| 	} | ||||
| 
 | ||||
| 	// chuck it in the db | ||||
| 	if err := m.db.Put(app); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// now we need to model an oauth client from the application that the oauth library can use | ||||
| 	oc := &oauth.Client{ | ||||
| 		ID:     clientID, | ||||
| 		Secret: clientSecret, | ||||
| 		Domain: form.RedirectURIs, | ||||
| 		UserID: "", // This client isn't yet associated with a specific user,  it's just an app client right now | ||||
| 	} | ||||
| 
 | ||||
| 	// chuck it in the db | ||||
| 	if err := m.db.Put(oc); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	mastoApp, err := m.tc.AppToMastoSensitive(app) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/ | ||||
| 	c.JSON(http.StatusOK, mastoApp) | ||||
| } | ||||
|  | @ -1,122 +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 federation | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation" | ||||
| ) | ||||
| 
 | ||||
| // UsersGETHandler should be served at https://example.org/users/:username. | ||||
| // | ||||
| // The goal here is to return the activitypub representation of an account | ||||
| // in the form of a vocab.ActivityStreamsPerson. This should only be served | ||||
| // to REMOTE SERVERS that present a valid signature on the GET request, on | ||||
| // behalf of a user, otherwise we risk leaking information about users publicly. | ||||
| // | ||||
| // And of course, the request should be refused if the account or server making the | ||||
| // request is blocked. | ||||
| func (m *Module) UsersGETHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func": "UsersGETHandler", | ||||
| 		"url":  c.Request.RequestURI, | ||||
| 	}) | ||||
| 	requestedUsername := c.Param(UsernameKey) | ||||
| 	if requestedUsername == "" { | ||||
| 		err := errors.New("no username specified in request") | ||||
| 		l.Debug(err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// make sure this actually an AP request | ||||
| 	format := c.NegotiateFormat(ActivityPubAcceptHeaders...) | ||||
| 	if format == "" { | ||||
| 		err := errors.New("could not negotiate format with given Accept header(s)") | ||||
| 		l.Debug(err) | ||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 	l.Tracef("negotiated format: %s", format) | ||||
| 
 | ||||
| 	// get the account the request is referring to | ||||
| 	requestedAccount := >smodel.Account{} | ||||
| 	if err := m.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | ||||
| 		l.Errorf("database error getting account with username %s: %s", requestedUsername, err) | ||||
| 		// we'll just return not authorized here to avoid giving anything away | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// and create a transport for it | ||||
| 	transport, err := m.federator.TransportController().NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error creating transport for username %s: %s", requestedUsername, err) | ||||
| 		// we'll just return not authorized here to avoid giving anything away | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// authenticate the request | ||||
| 	authentication, err := federation.AuthenticateFederatedRequest(transport, c.Request) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error authenticating GET user request: %s", err) | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !authentication.Authenticated { | ||||
| 		l.Debug("request not authorized") | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	 | ||||
| 	requestingAccount := >smodel.Account{} | ||||
| 	if authentication.RequestingPublicKeyID != nil { | ||||
| 		if err := m.db.GetWhere("public_key_uri", authentication.RequestingPublicKeyID.String(), requestingAccount); err != nil { | ||||
| 
 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	authorization, err := federation.AuthorizeFederatedRequest | ||||
| 
 | ||||
| 	person, err := m.tc.AccountToAS(requestedAccount) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error converting account to ap person: %s", err) | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	data, err := person.Serialize() | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error serializing user: %s", err) | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, data) | ||||
| } | ||||
|  | @ -1,193 +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 media | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // MediaCreatePOSTHandler handles requests to create/upload media attachments | ||||
| func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "statusCreatePOSTHandler") | ||||
| 	authed, err := oauth.MustAuth(c, true, true, true, true) // posting new media is serious business so we want *everything* | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// First check this user/account is permitted to create media | ||||
| 	// There's no point continuing otherwise. | ||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// extract the media create form from the request context | ||||
| 	l.Tracef("parsing request form: %s", c.Request.Form) | ||||
| 	form := &mastotypes.AttachmentRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||
| 		l.Debugf("could not parse form from request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | ||||
| 		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 := validateCreateMedia(form, m.config.MediaConfig); err != nil { | ||||
| 		l.Debugf("error validating form: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// open the attachment and extract the bytes from it | ||||
| 	f, err := form.File.Open() | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error opening attachment: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not open provided attachment: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 	buf := new(bytes.Buffer) | ||||
| 	size, err := io.Copy(buf, f) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error reading attachment: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not read provided attachment: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if size == 0 { | ||||
| 		l.Debug("could not read provided attachment: size 0 bytes") | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "could not read provided attachment: size 0 bytes"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using | ||||
| 	attachment, err := m.mediaHandler.ProcessLocalAttachment(buf.Bytes(), authed.Account.ID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error reading attachment: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not process attachment: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// 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 | ||||
| 	// TODO: tidy this up into a separate function and just return an error so all the c.JSON and return calls are obviated | ||||
| 	var focusx, focusy float32 | ||||
| 	if form.Focus != "" { | ||||
| 		spl := strings.Split(form.Focus, ",") | ||||
| 		if len(spl) != 2 { | ||||
| 			l.Debugf("improperly formatted focus %s", form.Focus) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)}) | ||||
| 			return | ||||
| 		} | ||||
| 		xStr := spl[0] | ||||
| 		yStr := spl[1] | ||||
| 		if xStr == "" || yStr == "" { | ||||
| 			l.Debugf("improperly formatted focus %s", form.Focus) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)}) | ||||
| 			return | ||||
| 		} | ||||
| 		fx, err := strconv.ParseFloat(xStr, 32) | ||||
| 		if err != nil { | ||||
| 			l.Debugf("improperly formatted focus %s: %s", form.Focus, err) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)}) | ||||
| 			return | ||||
| 		} | ||||
| 		if fx > 1 || fx < -1 { | ||||
| 			l.Debugf("improperly formatted focus %s", form.Focus) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)}) | ||||
| 			return | ||||
| 		} | ||||
| 		focusx = float32(fx) | ||||
| 		fy, err := strconv.ParseFloat(yStr, 32) | ||||
| 		if err != nil { | ||||
| 			l.Debugf("improperly formatted focus %s: %s", form.Focus, err) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)}) | ||||
| 			return | ||||
| 		} | ||||
| 		if fy > 1 || fy < -1 { | ||||
| 			l.Debugf("improperly formatted focus %s", form.Focus) | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)}) | ||||
| 			return | ||||
| 		} | ||||
| 		focusy = float32(fy) | ||||
| 	} | ||||
| 	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 := m.tc.AttachmentToMasto(attachment) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error parsing media attachment to frontend type: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error parsing media attachment to frontend type: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// now we can confidently put the attachment in the database | ||||
| 	if err := m.db.Put(attachment); err != nil { | ||||
| 		l.Debugf("error storing media attachment in db: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error storing media attachment in db: %s", err)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// and return its frontend representation | ||||
| 	c.JSON(http.StatusAccepted, mastoAttachment) | ||||
| } | ||||
| 
 | ||||
| func validateCreateMedia(form *mastotypes.AttachmentRequest, config *config.MediaConfig) error { | ||||
| 	// check there actually is a file attached and it's not size 0 | ||||
| 	if form.File == nil || form.File.Size == 0 { | ||||
| 		return errors.New("no attachment given") | ||||
| 	} | ||||
| 
 | ||||
| 	// a very superficial check to see if no size limits are exceeded | ||||
| 	// we still don't actually know which media types we're dealing with but the other handlers will go into more detail there | ||||
| 	maxSize := config.MaxVideoSize | ||||
| 	if config.MaxImageSize > maxSize { | ||||
| 		maxSize = config.MaxImageSize | ||||
| 	} | ||||
| 	if form.File.Size > int64(maxSize) { | ||||
| 		return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(form.Description) < config.MinDescriptionChars || len(form.Description) > config.MaxDescriptionChars { | ||||
| 		return fmt.Errorf("image description length must be between %d and %d characters (inclusive), but provided image description was %d chars", config.MinDescriptionChars, config.MaxDescriptionChars, len(form.Description)) | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: validate focus here | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,43 +0,0 @@ | |||
| // Code generated by mockery v2.7.4. DO NOT EDIT. | ||||
| 
 | ||||
| package apimodule | ||||
| 
 | ||||
| import ( | ||||
| 	mock "github.com/stretchr/testify/mock" | ||||
| 	db "github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 
 | ||||
| 	router "github.com/superseriousbusiness/gotosocial/internal/router" | ||||
| ) | ||||
| 
 | ||||
| // MockClientAPIModule is an autogenerated mock type for the ClientAPIModule type | ||||
| type MockClientAPIModule struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| // CreateTables provides a mock function with given fields: _a0 | ||||
| func (_m *MockClientAPIModule) CreateTables(_a0 db.DB) error { | ||||
| 	ret := _m.Called(_a0) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(db.DB) error); ok { | ||||
| 		r0 = rf(_a0) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // Route provides a mock function with given fields: s | ||||
| func (_m *MockClientAPIModule) Route(s router.Router) error { | ||||
| 	ret := _m.Called(s) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(router.Router) error); ok { | ||||
| 		r0 = rf(s) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
|  | @ -1,462 +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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| ) | ||||
| 
 | ||||
| type advancedStatusCreateForm struct { | ||||
| 	mastotypes.StatusCreateRequest | ||||
| 	advancedVisibilityFlagsForm | ||||
| } | ||||
| 
 | ||||
| type advancedVisibilityFlagsForm struct { | ||||
| 	// The gotosocial visibility model | ||||
| 	VisibilityAdvanced *gtsmodel.Visibility `form:"visibility_advanced"` | ||||
| 	// This status will be federated beyond the local timeline(s) | ||||
| 	Federated *bool `form:"federated"` | ||||
| 	// This status can be boosted/reblogged | ||||
| 	Boostable *bool `form:"boostable"` | ||||
| 	// This status can be replied to | ||||
| 	Replyable *bool `form:"replyable"` | ||||
| 	// This status can be liked/faved | ||||
| 	Likeable *bool `form:"likeable"` | ||||
| } | ||||
| 
 | ||||
| // StatusCreatePOSTHandler deals with the creation of new statuses | ||||
| func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithField("func", "statusCreatePOSTHandler") | ||||
| 	authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything* | ||||
| 	if err != nil { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// First check this user/account is permitted to post new statuses. | ||||
| 	// There's no point continuing otherwise. | ||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { | ||||
| 		l.Debugf("couldn't auth: %s", err) | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// extract the status create form from the request context | ||||
| 	l.Tracef("parsing request form: %s", c.Request.Form) | ||||
| 	form := &advancedStatusCreateForm{} | ||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||
| 		l.Debugf("could not parse form from request: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | ||||
| 		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 := m.validateCreateStatus(form, m.config.StatusesConfig); err != nil { | ||||
| 		l.Debugf("error validating form: %s", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// At this point we know the account is permitted to post, and we know the request form | ||||
| 	// is valid (at least according to the API specifications and the instance configuration). | ||||
| 	// So now we can start digging a bit deeper into the form and building up the new status from it. | ||||
| 
 | ||||
| 	// first we create a new status and add some basic info to it | ||||
| 	uris := util.GenerateURIsForAccount(authed.Account.Username, m.config.Protocol, m.config.Host) | ||||
| 	thisStatusID := uuid.NewString() | ||||
| 	thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID) | ||||
| 	thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID) | ||||
| 	newStatus := >smodel.Status{ | ||||
| 		ID:                       thisStatusID, | ||||
| 		URI:                      thisStatusURI, | ||||
| 		URL:                      thisStatusURL, | ||||
| 		Content:                  util.HTMLFormat(form.Status), | ||||
| 		CreatedAt:                time.Now(), | ||||
| 		UpdatedAt:                time.Now(), | ||||
| 		Local:                    true, | ||||
| 		AccountID:                authed.Account.ID, | ||||
| 		ContentWarning:           form.SpoilerText, | ||||
| 		ActivityStreamsType:      gtsmodel.ActivityStreamsNote, | ||||
| 		Sensitive:                form.Sensitive, | ||||
| 		Language:                 form.Language, | ||||
| 		CreatedWithApplicationID: authed.Application.ID, | ||||
| 		Text:                     form.Status, | ||||
| 	} | ||||
| 
 | ||||
| 	// check if replyToID is ok | ||||
| 	if err := m.parseReplyToID(form, authed.Account.ID, newStatus); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// check if mediaIDs are ok | ||||
| 	if err := m.parseMediaIDs(form, authed.Account.ID, newStatus); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// check if visibility settings are ok | ||||
| 	if err := m.parseVisibility(form, authed.Account.Privacy, newStatus); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// handle language settings | ||||
| 	if err := parseLanguage(form, authed.Account.Language, newStatus); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// handle mentions | ||||
| 	if err := m.parseMentions(form, authed.Account.ID, newStatus); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := m.parseTags(form, authed.Account.ID, newStatus); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := m.parseEmojis(form, authed.Account.ID, newStatus); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	/* | ||||
| 		FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it | ||||
| 	*/ | ||||
| 
 | ||||
| 	// put the new status in the database, generating an ID for it in the process | ||||
| 	if err := m.db.Put(newStatus); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// change the status ID of the media attachments to the new status | ||||
| 	for _, a := range newStatus.GTSMediaAttachments { | ||||
| 		a.StatusID = newStatus.ID | ||||
| 		a.UpdatedAt = time.Now() | ||||
| 		if err := m.db.UpdateByID(a.ID, a); err != nil { | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// pass to the distributor to take care of side effects asynchronously -- federation, mentions, updating metadata, etc, etc | ||||
| 	m.distributor.FromClientAPI() <- distributor.FromClientAPI{ | ||||
| 		APObjectType:   gtsmodel.ActivityStreamsNote, | ||||
| 		APActivityType: gtsmodel.ActivityStreamsCreate, | ||||
| 		Activity:       newStatus, | ||||
| 	} | ||||
| 
 | ||||
| 	// return the frontend representation of the new status to the submitter | ||||
| 	mastoStatus, err := m.tc.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, nil) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
| 
 | ||||
| func (m *Module) validateCreateStatus(form *advancedStatusCreateForm, config *config.StatusesConfig) error { | ||||
| 	// validate that, structurally, we have a valid status/post | ||||
| 	if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { | ||||
| 		return errors.New("no status, media, or poll provided") | ||||
| 	} | ||||
| 
 | ||||
| 	if form.MediaIDs != nil && form.Poll != nil { | ||||
| 		return errors.New("can't post media + poll in same status") | ||||
| 	} | ||||
| 
 | ||||
| 	// validate status | ||||
| 	if form.Status != "" { | ||||
| 		if len(form.Status) > config.MaxChars { | ||||
| 			return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// validate media attachments | ||||
| 	if len(form.MediaIDs) > config.MaxMediaFiles { | ||||
| 		return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles) | ||||
| 	} | ||||
| 
 | ||||
| 	// validate poll | ||||
| 	if form.Poll != nil { | ||||
| 		if form.Poll.Options == nil { | ||||
| 			return errors.New("poll with no options") | ||||
| 		} | ||||
| 		if len(form.Poll.Options) > config.PollMaxOptions { | ||||
| 			return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), config.PollMaxOptions) | ||||
| 		} | ||||
| 		for _, p := range form.Poll.Options { | ||||
| 			if len(p) > config.PollOptionMaxChars { | ||||
| 				return fmt.Errorf("poll option too long, %d characters provided but limit is %d", len(p), config.PollOptionMaxChars) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// validate spoiler text/cw | ||||
| 	if form.SpoilerText != "" { | ||||
| 		if len(form.SpoilerText) > config.CWMaxChars { | ||||
| 			return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), config.CWMaxChars) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// validate post language | ||||
| 	if form.Language != "" { | ||||
| 		if err := util.ValidateLanguage(form.Language); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Module) parseVisibility(form *advancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error { | ||||
| 	// by default all flags are set to true | ||||
| 	gtsAdvancedVis := >smodel.VisibilityAdvanced{ | ||||
| 		Federated: true, | ||||
| 		Boostable: true, | ||||
| 		Replyable: true, | ||||
| 		Likeable:  true, | ||||
| 	} | ||||
| 
 | ||||
| 	var gtsBasicVis gtsmodel.Visibility | ||||
| 	// Advanced takes priority if it's set. | ||||
| 	// If it's not set, take whatever masto visibility is set. | ||||
| 	// If *that's* not set either, then just take the account default. | ||||
| 	// If that's also not set, take the default for the whole instance. | ||||
| 	if form.VisibilityAdvanced != nil { | ||||
| 		gtsBasicVis = *form.VisibilityAdvanced | ||||
| 	} else if form.Visibility != "" { | ||||
| 		gtsBasicVis = m.tc.MastoVisToVis(form.Visibility) | ||||
| 	} else if accountDefaultVis != "" { | ||||
| 		gtsBasicVis = accountDefaultVis | ||||
| 	} else { | ||||
| 		gtsBasicVis = gtsmodel.VisibilityDefault | ||||
| 	} | ||||
| 
 | ||||
| 	switch gtsBasicVis { | ||||
| 	case gtsmodel.VisibilityPublic: | ||||
| 		// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out | ||||
| 		break | ||||
| 	case gtsmodel.VisibilityUnlocked: | ||||
| 		// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them | ||||
| 		if form.Federated != nil { | ||||
| 			gtsAdvancedVis.Federated = *form.Federated | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Boostable != nil { | ||||
| 			gtsAdvancedVis.Boostable = *form.Boostable | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Replyable != nil { | ||||
| 			gtsAdvancedVis.Replyable = *form.Replyable | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Likeable != nil { | ||||
| 			gtsAdvancedVis.Likeable = *form.Likeable | ||||
| 		} | ||||
| 
 | ||||
| 	case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: | ||||
| 		// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them | ||||
| 		gtsAdvancedVis.Boostable = false | ||||
| 
 | ||||
| 		if form.Federated != nil { | ||||
| 			gtsAdvancedVis.Federated = *form.Federated | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Replyable != nil { | ||||
| 			gtsAdvancedVis.Replyable = *form.Replyable | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Likeable != nil { | ||||
| 			gtsAdvancedVis.Likeable = *form.Likeable | ||||
| 		} | ||||
| 
 | ||||
| 	case gtsmodel.VisibilityDirect: | ||||
| 		// direct is pretty easy: there's only one possible setting so return it | ||||
| 		gtsAdvancedVis.Federated = true | ||||
| 		gtsAdvancedVis.Boostable = false | ||||
| 		gtsAdvancedVis.Federated = true | ||||
| 		gtsAdvancedVis.Likeable = true | ||||
| 	} | ||||
| 
 | ||||
| 	status.Visibility = gtsBasicVis | ||||
| 	status.VisibilityAdvanced = gtsAdvancedVis | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Module) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { | ||||
| 	if form.InReplyToID == "" { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted: | ||||
| 	// | ||||
| 	// 1. Does the replied status exist in the database? | ||||
| 	// 2. Is the replied status marked as replyable? | ||||
| 	// 3. Does a block exist between either the current account or the account that posted the status it's replying to? | ||||
| 	// | ||||
| 	// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing. | ||||
| 	repliedStatus := >smodel.Status{} | ||||
| 	repliedAccount := >smodel.Account{} | ||||
| 	// check replied status exists + is replyable | ||||
| 	if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil { | ||||
| 		if _, ok := err.(db.ErrNoEntries); ok { | ||||
| 			return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID) | ||||
| 		} | ||||
| 		return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) | ||||
| 	} | ||||
| 
 | ||||
| 	if !repliedStatus.VisibilityAdvanced.Replyable { | ||||
| 		return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) | ||||
| 	} | ||||
| 
 | ||||
| 	// check replied account is known to us | ||||
| 	if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { | ||||
| 		if _, ok := err.(db.ErrNoEntries); ok { | ||||
| 			return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID) | ||||
| 		} | ||||
| 		return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) | ||||
| 	} | ||||
| 	// check if a block exists | ||||
| 	if blocked, err := m.db.Blocked(thisAccountID, repliedAccount.ID); err != nil { | ||||
| 		if _, ok := err.(db.ErrNoEntries); !ok { | ||||
| 			return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) | ||||
| 		} | ||||
| 	} else if blocked { | ||||
| 		return fmt.Errorf("status with id %s not replyable", form.InReplyToID) | ||||
| 	} | ||||
| 	status.InReplyToID = repliedStatus.ID | ||||
| 	status.InReplyToAccountID = repliedAccount.ID | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Module) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { | ||||
| 	if form.MediaIDs == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	gtsMediaAttachments := []*gtsmodel.MediaAttachment{} | ||||
| 	attachments := []string{} | ||||
| 	for _, mediaID := range form.MediaIDs { | ||||
| 		// check these attachments exist | ||||
| 		a := >smodel.MediaAttachment{} | ||||
| 		if err := m.db.GetByID(mediaID, a); err != nil { | ||||
| 			return fmt.Errorf("invalid media type or media not found for media id %s", mediaID) | ||||
| 		} | ||||
| 		// check they belong to the requesting account id | ||||
| 		if a.AccountID != thisAccountID { | ||||
| 			return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID) | ||||
| 		} | ||||
| 		// check they're not already used in a status | ||||
| 		if a.StatusID != "" || a.ScheduledStatusID != "" { | ||||
| 			return fmt.Errorf("media with id %s is already attached to a status", mediaID) | ||||
| 		} | ||||
| 		gtsMediaAttachments = append(gtsMediaAttachments, a) | ||||
| 		attachments = append(attachments, a.ID) | ||||
| 	} | ||||
| 	status.GTSMediaAttachments = gtsMediaAttachments | ||||
| 	status.Attachments = attachments | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error { | ||||
| 	if form.Language != "" { | ||||
| 		status.Language = form.Language | ||||
| 	} else { | ||||
| 		status.Language = accountDefaultLanguage | ||||
| 	} | ||||
| 	if status.Language == "" { | ||||
| 		return errors.New("no language given either in status create form or account default") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Module) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { | ||||
| 	menchies := []string{} | ||||
| 	gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error generating mentions from status: %s", err) | ||||
| 	} | ||||
| 	for _, menchie := range gtsMenchies { | ||||
| 		if err := m.db.Put(menchie); err != nil { | ||||
| 			return fmt.Errorf("error putting mentions in db: %s", err) | ||||
| 		} | ||||
| 		menchies = append(menchies, menchie.TargetAccountID) | ||||
| 	} | ||||
| 	// add full populated gts menchies to the status for passing them around conveniently | ||||
| 	status.GTSMentions = gtsMenchies | ||||
| 	// add just the ids of the mentioned accounts to the status for putting in the db | ||||
| 	status.Mentions = menchies | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Module) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { | ||||
| 	tags := []string{} | ||||
| 	gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error generating hashtags from status: %s", err) | ||||
| 	} | ||||
| 	for _, tag := range gtsTags { | ||||
| 		if err := m.db.Upsert(tag, "name"); err != nil { | ||||
| 			return fmt.Errorf("error putting tags in db: %s", err) | ||||
| 		} | ||||
| 		tags = append(tags, tag.ID) | ||||
| 	} | ||||
| 	// add full populated gts tags to the status for passing them around conveniently | ||||
| 	status.GTSTags = gtsTags | ||||
| 	// add just the ids of the used tags to the status for putting in the db | ||||
| 	status.Tags = tags | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Module) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { | ||||
| 	emojis := []string{} | ||||
| 	gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error generating emojis from status: %s", err) | ||||
| 	} | ||||
| 	for _, e := range gtsEmojis { | ||||
| 		emojis = append(emojis, e.ID) | ||||
| 	} | ||||
| 	// add full populated gts emojis to the status for passing them around conveniently | ||||
| 	status.GTSEmojis = gtsEmojis | ||||
| 	// add just the ids of the used emojis to the status for putting in the db | ||||
| 	status.Emojis = emojis | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,107 +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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusDELETEHandler verifies and handles deletion of a status | ||||
| func (m *Module) StatusDELETEHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "StatusDELETEHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed so can't delete status") | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target status %s", targetStatusID) | ||||
| 	targetStatus := >smodel.Status{} | ||||
| 	if err := m.db.GetByID(targetStatusID, targetStatus); err != nil { | ||||
| 		l.Errorf("error fetching status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if targetStatus.AccountID != authed.Account.ID { | ||||
| 		l.Debug("status doesn't belong to requesting account") | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not allowed"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to get relevant accounts") | ||||
| 	relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var boostOfStatus *gtsmodel.Status | ||||
| 	if targetStatus.BoostOfID != "" { | ||||
| 		boostOfStatus = >smodel.Status{} | ||||
| 		if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { | ||||
| 			l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) | ||||
| 			c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.tc.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := m.db.DeleteByID(targetStatus.ID, targetStatus); err != nil { | ||||
| 		l.Errorf("error deleting status from the database: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	m.distributor.FromClientAPI() <- distributor.FromClientAPI{ | ||||
| 		APObjectType:   gtsmodel.ActivityStreamsNote, | ||||
| 		APActivityType: gtsmodel.ActivityStreamsDelete, | ||||
| 		Activity:       targetStatus, | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
|  | @ -1,137 +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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusFavePOSTHandler handles fave requests against a given status ID | ||||
| func (m *Module) StatusFavePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "StatusFavePOSTHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed so can't fave status") | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target status %s", targetStatusID) | ||||
| 	targetStatus := >smodel.Status{} | ||||
| 	if err := m.db.GetByID(targetStatusID, targetStatus); err != nil { | ||||
| 		l.Errorf("error fetching status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target account %s", targetStatus.AccountID) | ||||
| 	targetAccount := >smodel.Account{} | ||||
| 	if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { | ||||
| 		l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to get relevant accounts") | ||||
| 	relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to see if status is visible") | ||||
| 	visible, err := m.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !visible { | ||||
| 		l.Trace("status is not visible") | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// is the status faveable? | ||||
| 	if !targetStatus.VisibilityAdvanced.Likeable { | ||||
| 		l.Debug("status is not faveable") | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("status %s not faveable", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// it's visible! it's faveable! so let's fave the FUCK out of it | ||||
| 	fave, err := m.db.FaveStatus(targetStatus, authed.Account.ID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error faveing status: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var boostOfStatus *gtsmodel.Status | ||||
| 	if targetStatus.BoostOfID != "" { | ||||
| 		boostOfStatus = >smodel.Status{} | ||||
| 		if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { | ||||
| 			l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) | ||||
| 			c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// if the targeted status was already faved, faved will be nil | ||||
| 	// only put the fave in the distributor if something actually changed | ||||
| 	if fave != nil { | ||||
| 		fave.FavedStatus = targetStatus // attach the status pointer to the fave for easy retrieval in the distributor | ||||
| 		m.distributor.FromClientAPI() <- distributor.FromClientAPI{ | ||||
| 			APObjectType:   gtsmodel.ActivityStreamsNote, // status is a note | ||||
| 			APActivityType: gtsmodel.ActivityStreamsLike, // we're creating a like/fave on the note | ||||
| 			Activity:       fave,                         // pass the fave along for processing | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
|  | @ -1,129 +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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/mastotypes" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusFavedByGETHandler is for serving a list of accounts that have faved a given status | ||||
| func (m *Module) StatusFavedByGETHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "statusGETHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	var requestingAccount *gtsmodel.Account | ||||
| 	authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed but will continue to serve anyway if public status") | ||||
| 		requestingAccount = nil | ||||
| 	} else { | ||||
| 		requestingAccount = authed.Account | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target status %s", targetStatusID) | ||||
| 	targetStatus := >smodel.Status{} | ||||
| 	if err := m.db.GetByID(targetStatusID, targetStatus); err != nil { | ||||
| 		l.Errorf("error fetching status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target account %s", targetStatus.AccountID) | ||||
| 	targetAccount := >smodel.Account{} | ||||
| 	if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { | ||||
| 		l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to get relevant accounts") | ||||
| 	relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to see if status is visible") | ||||
| 	visible, err := m.db.StatusVisible(targetStatus, targetAccount, requestingAccount, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !visible { | ||||
| 		l.Trace("status is not visible") | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff | ||||
| 	favingAccounts, err := m.db.WhoFavedStatus(targetStatus) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// filter the list so the user doesn't see accounts they blocked or which blocked them | ||||
| 	filteredAccounts := []*gtsmodel.Account{} | ||||
| 	for _, acc := range favingAccounts { | ||||
| 		blocked, err := m.db.Blocked(authed.Account.ID, acc.ID) | ||||
| 		if err != nil { | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		if !blocked { | ||||
| 			filteredAccounts = append(filteredAccounts, acc) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: filter other things here? suspended? muted? silenced? | ||||
| 
 | ||||
| 	// now we can return the masto representation of those accounts | ||||
| 	mastoAccounts := []*mastotypes.Account{} | ||||
| 	for _, acc := range filteredAccounts { | ||||
| 		mastoAccount, err := m.tc.AccountToMastoPublic(acc) | ||||
| 		if err != nil { | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		mastoAccounts = append(mastoAccounts, mastoAccount) | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoAccounts) | ||||
| } | ||||
|  | @ -1,112 +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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusGETHandler is for handling requests to just get one status based on its ID | ||||
| func (m *Module) StatusGETHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "statusGETHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	var requestingAccount *gtsmodel.Account | ||||
| 	authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed but will continue to serve anyway if public status") | ||||
| 		requestingAccount = nil | ||||
| 	} else { | ||||
| 		requestingAccount = authed.Account | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target status %s", targetStatusID) | ||||
| 	targetStatus := >smodel.Status{} | ||||
| 	if err := m.db.GetByID(targetStatusID, targetStatus); err != nil { | ||||
| 		l.Errorf("error fetching status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target account %s", targetStatus.AccountID) | ||||
| 	targetAccount := >smodel.Account{} | ||||
| 	if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { | ||||
| 		l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to get relevant accounts") | ||||
| 	relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to see if status is visible") | ||||
| 	visible, err := m.db.StatusVisible(targetStatus, targetAccount, requestingAccount, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !visible { | ||||
| 		l.Trace("status is not visible") | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var boostOfStatus *gtsmodel.Status | ||||
| 	if targetStatus.BoostOfID != "" { | ||||
| 		boostOfStatus = >smodel.Status{} | ||||
| 		if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { | ||||
| 			l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) | ||||
| 			c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.tc.StatusToMasto(targetStatus, targetAccount, requestingAccount, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
|  | @ -1,137 +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 status | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // StatusUnfavePOSTHandler is for undoing a fave on a status with a given ID | ||||
| func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { | ||||
| 	l := m.log.WithFields(logrus.Fields{ | ||||
| 		"func":        "StatusUnfavePOSTHandler", | ||||
| 		"request_uri": c.Request.RequestURI, | ||||
| 		"user_agent":  c.Request.UserAgent(), | ||||
| 		"origin_ip":   c.ClientIP(), | ||||
| 	}) | ||||
| 	l.Debugf("entering function") | ||||
| 
 | ||||
| 	authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else | ||||
| 	if err != nil { | ||||
| 		l.Debug("not authed so can't unfave status") | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	targetStatusID := c.Param(IDKey) | ||||
| 	if targetStatusID == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target status %s", targetStatusID) | ||||
| 	targetStatus := >smodel.Status{} | ||||
| 	if err := m.db.GetByID(targetStatusID, targetStatus); err != nil { | ||||
| 		l.Errorf("error fetching status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Tracef("going to search for target account %s", targetStatus.AccountID) | ||||
| 	targetAccount := >smodel.Account{} | ||||
| 	if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { | ||||
| 		l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to get relevant accounts") | ||||
| 	relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("going to see if status is visible") | ||||
| 	visible, err := m.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !visible { | ||||
| 		l.Trace("status is not visible") | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// is the status faveable? | ||||
| 	if !targetStatus.VisibilityAdvanced.Likeable { | ||||
| 		l.Debug("status is not faveable") | ||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("status %s not faveable so therefore not unfave-able", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// it's visible! it's faveable! so let's unfave the FUCK out of it | ||||
| 	fave, err := m.db.UnfaveStatus(targetStatus, authed.Account.ID) | ||||
| 	if err != nil { | ||||
| 		l.Debugf("error unfaveing status: %s", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var boostOfStatus *gtsmodel.Status | ||||
| 	if targetStatus.BoostOfID != "" { | ||||
| 		boostOfStatus = >smodel.Status{} | ||||
| 		if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { | ||||
| 			l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) | ||||
| 			c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	mastoStatus, err := m.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) | ||||
| 	if err != nil { | ||||
| 		l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// fave might be nil if this status wasn't faved in the first place | ||||
| 	// we only want to pass the message to the distributor if something actually changed | ||||
| 	if fave != nil { | ||||
| 		fave.FavedStatus = targetStatus // attach the status pointer to the fave for easy retrieval in the distributor | ||||
| 		m.distributor.FromClientAPI() <- distributor.FromClientAPI{ | ||||
| 			APObjectType:   gtsmodel.ActivityStreamsNote, // status is a note | ||||
| 			APActivityType: gtsmodel.ActivityStreamsUndo, // undo the fave | ||||
| 			Activity:       fave,                         // pass the undone fave along | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, mastoStatus) | ||||
| } | ||||
							
								
								
									
										47
									
								
								internal/cache/mock_Cache.go
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										47
									
								
								internal/cache/mock_Cache.go
									
										
									
									
										vendored
									
									
								
							|  | @ -1,47 +0,0 @@ | |||
| // Code generated by mockery v2.7.4. DO NOT EDIT. | ||||
| 
 | ||||
| package cache | ||||
| 
 | ||||
| import mock "github.com/stretchr/testify/mock" | ||||
| 
 | ||||
| // MockCache is an autogenerated mock type for the Cache type | ||||
| type MockCache struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| // Fetch provides a mock function with given fields: k | ||||
| func (_m *MockCache) Fetch(k string) (interface{}, error) { | ||||
| 	ret := _m.Called(k) | ||||
| 
 | ||||
| 	var r0 interface{} | ||||
| 	if rf, ok := ret.Get(0).(func(string) interface{}); ok { | ||||
| 		r0 = rf(k) | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).(interface{}) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var r1 error | ||||
| 	if rf, ok := ret.Get(1).(func(string) error); ok { | ||||
| 		r1 = rf(k) | ||||
| 	} else { | ||||
| 		r1 = ret.Error(1) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0, r1 | ||||
| } | ||||
| 
 | ||||
| // Store provides a mock function with given fields: k, v | ||||
| func (_m *MockCache) Store(k string, v interface{}) error { | ||||
| 	ret := _m.Called(k, v) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { | ||||
| 		r0 = rf(k, v) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
|  | @ -1,66 +0,0 @@ | |||
| // Code generated by mockery v2.7.4. DO NOT EDIT. | ||||
| 
 | ||||
| package config | ||||
| 
 | ||||
| import mock "github.com/stretchr/testify/mock" | ||||
| 
 | ||||
| // MockKeyedFlags is an autogenerated mock type for the KeyedFlags type | ||||
| type MockKeyedFlags struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| // Bool provides a mock function with given fields: k | ||||
| func (_m *MockKeyedFlags) Bool(k string) bool { | ||||
| 	ret := _m.Called(k) | ||||
| 
 | ||||
| 	var r0 bool | ||||
| 	if rf, ok := ret.Get(0).(func(string) bool); ok { | ||||
| 		r0 = rf(k) | ||||
| 	} else { | ||||
| 		r0 = ret.Get(0).(bool) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // Int provides a mock function with given fields: k | ||||
| func (_m *MockKeyedFlags) Int(k string) int { | ||||
| 	ret := _m.Called(k) | ||||
| 
 | ||||
| 	var r0 int | ||||
| 	if rf, ok := ret.Get(0).(func(string) int); ok { | ||||
| 		r0 = rf(k) | ||||
| 	} else { | ||||
| 		r0 = ret.Get(0).(int) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // IsSet provides a mock function with given fields: k | ||||
| func (_m *MockKeyedFlags) IsSet(k string) bool { | ||||
| 	ret := _m.Called(k) | ||||
| 
 | ||||
| 	var r0 bool | ||||
| 	if rf, ok := ret.Get(0).(func(string) bool); ok { | ||||
| 		r0 = rf(k) | ||||
| 	} else { | ||||
| 		r0 = ret.Get(0).(bool) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // String provides a mock function with given fields: k | ||||
| func (_m *MockKeyedFlags) String(k string) string { | ||||
| 	ret := _m.Called(k) | ||||
| 
 | ||||
| 	var r0 string | ||||
| 	if rf, ok := ret.Get(0).(func(string) string); ok { | ||||
| 		r0 = rf(k) | ||||
| 	} else { | ||||
| 		r0 = ret.Get(0).(string) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
|  | @ -20,17 +20,13 @@ package db | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/go-fed/activity/pub" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| ) | ||||
| 
 | ||||
| const dbTypePostgres string = "POSTGRES" | ||||
| const DBTypePostgres string = "POSTGRES" | ||||
| 
 | ||||
| // ErrNoEntries is to be returned from the DB interface when no entries are found for a given query. | ||||
| type ErrNoEntries struct{} | ||||
|  | @ -283,14 +279,3 @@ type DB interface { | |||
| 	// if they exist in the db and conveniently returning them if they do. | ||||
| 	EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) | ||||
| } | ||||
| 
 | ||||
| // New returns a new database service that satisfies the DB interface and, by extension, | ||||
| // the go-fed database interface described here: https://github.com/go-fed/activity/blob/master/pub/database.go | ||||
| func New(ctx context.Context, c *config.Config, log *logrus.Logger) (DB, error) { | ||||
| 	switch strings.ToUpper(c.DBConfig.Type) { | ||||
| 	case dbTypePostgres: | ||||
| 		return newPostgresService(ctx, c, log.WithField("service", "db")) | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("database type %s not supported", c.DBConfig.Type) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ import ( | |||
| 	"github.com/go-fed/activity/streams/vocab" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| ) | ||||
| 
 | ||||
|  | @ -39,10 +39,10 @@ type federatingDB struct { | |||
| 	locks  *sync.Map | ||||
| 	db     DB | ||||
| 	config *config.Config | ||||
| 	log    *logrus.Entry | ||||
| 	log    *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| func newFederatingDB(db DB, config *config.Config, log *logrus.Entry) pub.Database { | ||||
| func NewFederatingDB(db DB, config *config.Config, log *logrus.Logger) pub.Database { | ||||
| 	return &federatingDB{ | ||||
| 		locks:  new(sync.Map), | ||||
| 		db:     db, | ||||
|  |  | |||
|  | @ -1,484 +0,0 @@ | |||
| // Code generated by mockery v2.7.4. DO NOT EDIT. | ||||
| 
 | ||||
| package db | ||||
| 
 | ||||
| import ( | ||||
| 	context "context" | ||||
| 
 | ||||
| 	mock "github.com/stretchr/testify/mock" | ||||
| 	gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 
 | ||||
| 	net "net" | ||||
| 
 | ||||
| 	pub "github.com/go-fed/activity/pub" | ||||
| ) | ||||
| 
 | ||||
| // MockDB is an autogenerated mock type for the DB type | ||||
| type MockDB struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| // Blocked provides a mock function with given fields: account1, account2 | ||||
| func (_m *MockDB) Blocked(account1 string, account2 string) (bool, error) { | ||||
| 	ret := _m.Called(account1, account2) | ||||
| 
 | ||||
| 	var r0 bool | ||||
| 	if rf, ok := ret.Get(0).(func(string, string) bool); ok { | ||||
| 		r0 = rf(account1, account2) | ||||
| 	} else { | ||||
| 		r0 = ret.Get(0).(bool) | ||||
| 	} | ||||
| 
 | ||||
| 	var r1 error | ||||
| 	if rf, ok := ret.Get(1).(func(string, string) error); ok { | ||||
| 		r1 = rf(account1, account2) | ||||
| 	} else { | ||||
| 		r1 = ret.Error(1) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0, r1 | ||||
| } | ||||
| 
 | ||||
| // CreateTable provides a mock function with given fields: i | ||||
| func (_m *MockDB) CreateTable(i interface{}) error { | ||||
| 	ret := _m.Called(i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(interface{}) error); ok { | ||||
| 		r0 = rf(i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // DeleteByID provides a mock function with given fields: id, i | ||||
| func (_m *MockDB) DeleteByID(id string, i interface{}) error { | ||||
| 	ret := _m.Called(id, i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { | ||||
| 		r0 = rf(id, i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // DeleteWhere provides a mock function with given fields: key, value, i | ||||
| func (_m *MockDB) DeleteWhere(key string, value interface{}, i interface{}) error { | ||||
| 	ret := _m.Called(key, value, i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) error); ok { | ||||
| 		r0 = rf(key, value, i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // DropTable provides a mock function with given fields: i | ||||
| func (_m *MockDB) DropTable(i interface{}) error { | ||||
| 	ret := _m.Called(i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(interface{}) error); ok { | ||||
| 		r0 = rf(i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // EmojiStringsToEmojis provides a mock function with given fields: emojis, originAccountID, statusID | ||||
| func (_m *MockDB) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) { | ||||
| 	ret := _m.Called(emojis, originAccountID, statusID) | ||||
| 
 | ||||
| 	var r0 []*gtsmodel.Emoji | ||||
| 	if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Emoji); ok { | ||||
| 		r0 = rf(emojis, originAccountID, statusID) | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).([]*gtsmodel.Emoji) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var r1 error | ||||
| 	if rf, ok := ret.Get(1).(func([]string, string, string) error); ok { | ||||
| 		r1 = rf(emojis, originAccountID, statusID) | ||||
| 	} else { | ||||
| 		r1 = ret.Error(1) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0, r1 | ||||
| } | ||||
| 
 | ||||
| // Federation provides a mock function with given fields: | ||||
| func (_m *MockDB) Federation() pub.Database { | ||||
| 	ret := _m.Called() | ||||
| 
 | ||||
| 	var r0 pub.Database | ||||
| 	if rf, ok := ret.Get(0).(func() pub.Database); ok { | ||||
| 		r0 = rf() | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).(pub.Database) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetAccountByUserID provides a mock function with given fields: userID, account | ||||
| func (_m *MockDB) GetAccountByUserID(userID string, account *gtsmodel.Account) error { | ||||
| 	ret := _m.Called(userID, account) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, *gtsmodel.Account) error); ok { | ||||
| 		r0 = rf(userID, account) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetAll provides a mock function with given fields: i | ||||
| func (_m *MockDB) GetAll(i interface{}) error { | ||||
| 	ret := _m.Called(i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(interface{}) error); ok { | ||||
| 		r0 = rf(i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetAvatarForAccountID provides a mock function with given fields: avatar, accountID | ||||
| func (_m *MockDB) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { | ||||
| 	ret := _m.Called(avatar, accountID) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { | ||||
| 		r0 = rf(avatar, accountID) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetByID provides a mock function with given fields: id, i | ||||
| func (_m *MockDB) GetByID(id string, i interface{}) error { | ||||
| 	ret := _m.Called(id, i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { | ||||
| 		r0 = rf(id, i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetFollowRequestsForAccountID provides a mock function with given fields: accountID, followRequests | ||||
| func (_m *MockDB) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { | ||||
| 	ret := _m.Called(accountID, followRequests) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.FollowRequest) error); ok { | ||||
| 		r0 = rf(accountID, followRequests) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetFollowersByAccountID provides a mock function with given fields: accountID, followers | ||||
| func (_m *MockDB) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error { | ||||
| 	ret := _m.Called(accountID, followers) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Follow) error); ok { | ||||
| 		r0 = rf(accountID, followers) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetFollowingByAccountID provides a mock function with given fields: accountID, following | ||||
| func (_m *MockDB) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { | ||||
| 	ret := _m.Called(accountID, following) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Follow) error); ok { | ||||
| 		r0 = rf(accountID, following) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetHeaderForAccountID provides a mock function with given fields: header, accountID | ||||
| func (_m *MockDB) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { | ||||
| 	ret := _m.Called(header, accountID) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { | ||||
| 		r0 = rf(header, accountID) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetLastStatusForAccountID provides a mock function with given fields: accountID, status | ||||
| func (_m *MockDB) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { | ||||
| 	ret := _m.Called(accountID, status) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, *gtsmodel.Status) error); ok { | ||||
| 		r0 = rf(accountID, status) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetStatusesByAccountID provides a mock function with given fields: accountID, statuses | ||||
| func (_m *MockDB) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error { | ||||
| 	ret := _m.Called(accountID, statuses) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Status) error); ok { | ||||
| 		r0 = rf(accountID, statuses) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetStatusesByTimeDescending provides a mock function with given fields: accountID, statuses, limit | ||||
| func (_m *MockDB) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error { | ||||
| 	ret := _m.Called(accountID, statuses, limit) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Status, int) error); ok { | ||||
| 		r0 = rf(accountID, statuses, limit) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // GetWhere provides a mock function with given fields: key, value, i | ||||
| func (_m *MockDB) GetWhere(key string, value interface{}, i interface{}) error { | ||||
| 	ret := _m.Called(key, value, i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) error); ok { | ||||
| 		r0 = rf(key, value, i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // IsEmailAvailable provides a mock function with given fields: email | ||||
| func (_m *MockDB) IsEmailAvailable(email string) error { | ||||
| 	ret := _m.Called(email) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string) error); ok { | ||||
| 		r0 = rf(email) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // IsHealthy provides a mock function with given fields: ctx | ||||
| func (_m *MockDB) IsHealthy(ctx context.Context) error { | ||||
| 	ret := _m.Called(ctx) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(context.Context) error); ok { | ||||
| 		r0 = rf(ctx) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // IsUsernameAvailable provides a mock function with given fields: username | ||||
| func (_m *MockDB) IsUsernameAvailable(username string) error { | ||||
| 	ret := _m.Called(username) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string) error); ok { | ||||
| 		r0 = rf(username) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // MentionStringsToMentions provides a mock function with given fields: targetAccounts, originAccountID, statusID | ||||
| func (_m *MockDB) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { | ||||
| 	ret := _m.Called(targetAccounts, originAccountID, statusID) | ||||
| 
 | ||||
| 	var r0 []*gtsmodel.Mention | ||||
| 	if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Mention); ok { | ||||
| 		r0 = rf(targetAccounts, originAccountID, statusID) | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).([]*gtsmodel.Mention) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var r1 error | ||||
| 	if rf, ok := ret.Get(1).(func([]string, string, string) error); ok { | ||||
| 		r1 = rf(targetAccounts, originAccountID, statusID) | ||||
| 	} else { | ||||
| 		r1 = ret.Error(1) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0, r1 | ||||
| } | ||||
| 
 | ||||
| // NewSignup provides a mock function with given fields: username, reason, requireApproval, email, password, signUpIP, locale, appID | ||||
| func (_m *MockDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) { | ||||
| 	ret := _m.Called(username, reason, requireApproval, email, password, signUpIP, locale, appID) | ||||
| 
 | ||||
| 	var r0 *gtsmodel.User | ||||
| 	if rf, ok := ret.Get(0).(func(string, string, bool, string, string, net.IP, string, string) *gtsmodel.User); ok { | ||||
| 		r0 = rf(username, reason, requireApproval, email, password, signUpIP, locale, appID) | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).(*gtsmodel.User) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var r1 error | ||||
| 	if rf, ok := ret.Get(1).(func(string, string, bool, string, string, net.IP, string, string) error); ok { | ||||
| 		r1 = rf(username, reason, requireApproval, email, password, signUpIP, locale, appID) | ||||
| 	} else { | ||||
| 		r1 = ret.Error(1) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0, r1 | ||||
| } | ||||
| 
 | ||||
| // Put provides a mock function with given fields: i | ||||
| func (_m *MockDB) Put(i interface{}) error { | ||||
| 	ret := _m.Called(i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(interface{}) error); ok { | ||||
| 		r0 = rf(i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // SetHeaderOrAvatarForAccountID provides a mock function with given fields: mediaAttachment, accountID | ||||
| func (_m *MockDB) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { | ||||
| 	ret := _m.Called(mediaAttachment, accountID) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { | ||||
| 		r0 = rf(mediaAttachment, accountID) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // Stop provides a mock function with given fields: ctx | ||||
| func (_m *MockDB) Stop(ctx context.Context) error { | ||||
| 	ret := _m.Called(ctx) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(context.Context) error); ok { | ||||
| 		r0 = rf(ctx) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // TagStringsToTags provides a mock function with given fields: tags, originAccountID, statusID | ||||
| func (_m *MockDB) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) { | ||||
| 	ret := _m.Called(tags, originAccountID, statusID) | ||||
| 
 | ||||
| 	var r0 []*gtsmodel.Tag | ||||
| 	if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Tag); ok { | ||||
| 		r0 = rf(tags, originAccountID, statusID) | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).([]*gtsmodel.Tag) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var r1 error | ||||
| 	if rf, ok := ret.Get(1).(func([]string, string, string) error); ok { | ||||
| 		r1 = rf(tags, originAccountID, statusID) | ||||
| 	} else { | ||||
| 		r1 = ret.Error(1) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0, r1 | ||||
| } | ||||
| 
 | ||||
| // UpdateByID provides a mock function with given fields: id, i | ||||
| func (_m *MockDB) UpdateByID(id string, i interface{}) error { | ||||
| 	ret := _m.Called(id, i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { | ||||
| 		r0 = rf(id, i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // UpdateOneByID provides a mock function with given fields: id, key, value, i | ||||
| func (_m *MockDB) UpdateOneByID(id string, key string, value interface{}, i interface{}) error { | ||||
| 	ret := _m.Called(id, key, value, i) | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, string, interface{}, interface{}) error); ok { | ||||
| 		r0 = rf(id, key, value, i) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
|  | @ -37,7 +37,7 @@ import ( | |||
| 	"github.com/google/uuid" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | @ -46,14 +46,14 @@ import ( | |||
| type postgresService struct { | ||||
| 	config       *config.Config | ||||
| 	conn         *pg.DB | ||||
| 	log          *logrus.Entry | ||||
| 	log          *logrus.Logger | ||||
| 	cancel       context.CancelFunc | ||||
| 	federationDB pub.Database | ||||
| } | ||||
| 
 | ||||
| // newPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. | ||||
| // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. | ||||
| // Under the hood, it uses https://github.com/go-pg/pg to create and maintain a database connection. | ||||
| func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry) (DB, error) { | ||||
| func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logger) (DB, error) { | ||||
| 	opts, err := derivePGOptions(c) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not create postgres service: %s", err) | ||||
|  | @ -67,7 +67,7 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry | |||
| 	// this will break the logfmt format we normally log in, | ||||
| 	// since we can't choose where pg outputs to and it defaults to | ||||
| 	// stdout. So use this option with care! | ||||
| 	if log.Logger.GetLevel() >= logrus.TraceLevel { | ||||
| 	if log.GetLevel() >= logrus.TraceLevel { | ||||
| 		conn.AddQueryHook(pgdebug.DebugHook{ | ||||
| 			// Print all queries. | ||||
| 			Verbose: true, | ||||
|  | @ -95,7 +95,7 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry | |||
| 		cancel: cancel, | ||||
| 	} | ||||
| 
 | ||||
| 	federatingDB := newFederatingDB(ps, c, log) | ||||
| 	federatingDB := NewFederatingDB(ps, c, log) | ||||
| 	ps.federationDB = federatingDB | ||||
| 
 | ||||
| 	// we can confidently return this useable postgres service now | ||||
|  | @ -109,8 +109,8 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry | |||
| // derivePGOptions takes an application config and returns either a ready-to-use *pg.Options | ||||
| // with sensible defaults, or an error if it's not satisfied by the provided config. | ||||
| func derivePGOptions(c *config.Config) (*pg.Options, error) { | ||||
| 	if strings.ToUpper(c.DBConfig.Type) != dbTypePostgres { | ||||
| 		return nil, fmt.Errorf("expected db type of %s but got %s", dbTypePostgres, c.DBConfig.Type) | ||||
| 	if strings.ToUpper(c.DBConfig.Type) != DBTypePostgres { | ||||
| 		return nil, fmt.Errorf("expected db type of %s but got %s", DBTypePostgres, c.DBConfig.Type) | ||||
| 	} | ||||
| 
 | ||||
| 	// validate port | ||||
|  |  | |||
|  | @ -16,6 +16,6 @@ | |||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package db | ||||
| package db_test | ||||
| 
 | ||||
| // TODO: write tests for postgres | ||||
|  |  | |||
|  | @ -1,143 +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 distributor | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| ) | ||||
| 
 | ||||
| // Distributor should be passed to api modules (see internal/apimodule/...). It is used for | ||||
| // passing messages back and forth from the client API and the federating interface, via channels. | ||||
| // It also contains logic for filtering which messages should end up where. | ||||
| // It is designed to be used asynchronously: the client API and the federating API should just be able to | ||||
| // fire messages into the distributor and not wait for a reply before proceeding with other work. This allows | ||||
| // for clean distribution of messages without slowing down the client API and harming the user experience. | ||||
| type Distributor interface { | ||||
| 	// FromClientAPI returns a channel for accepting messages that come from the gts client API. | ||||
| 	FromClientAPI() chan FromClientAPI | ||||
| 	// ToClientAPI returns a channel for putting in messages that need to go to the gts client API. | ||||
| 	ToClientAPI() chan ToClientAPI | ||||
| 	// FromFederator returns a channel for accepting messages that come from the federator (activitypub). | ||||
| 	FromFederator() chan FromFederator | ||||
| 	// ToFederator returns a channel for putting in messages that need to go to the federator (activitypub). | ||||
| 	ToFederator() chan ToFederator | ||||
| 
 | ||||
| 	// Start starts the Distributor, reading from its channels and passing messages back and forth. | ||||
| 	Start() error | ||||
| 	// Stop stops the distributor cleanly, finishing handling any remaining messages before closing down. | ||||
| 	Stop() error | ||||
| } | ||||
| 
 | ||||
| // distributor just implements the Distributor interface | ||||
| type distributor struct { | ||||
| 	// federator     pub.FederatingActor | ||||
| 	fromClientAPI chan FromClientAPI | ||||
| 	toClientAPI   chan ToClientAPI | ||||
| 	fromFederator chan FromFederator | ||||
| 	toFederator   chan ToFederator | ||||
| 	stop          chan interface{} | ||||
| 	log           *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new Distributor that uses the given federator and logger | ||||
| func New(log *logrus.Logger) Distributor { | ||||
| 	return &distributor{ | ||||
| 		// federator:     federator, | ||||
| 		fromClientAPI: make(chan FromClientAPI, 100), | ||||
| 		toClientAPI:   make(chan ToClientAPI, 100), | ||||
| 		fromFederator: make(chan FromFederator, 100), | ||||
| 		toFederator:   make(chan ToFederator, 100), | ||||
| 		stop:          make(chan interface{}), | ||||
| 		log:           log, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (d *distributor) FromClientAPI() chan FromClientAPI { | ||||
| 	return d.fromClientAPI | ||||
| } | ||||
| 
 | ||||
| func (d *distributor) ToClientAPI() chan ToClientAPI { | ||||
| 	return d.toClientAPI | ||||
| } | ||||
| 
 | ||||
| func (d *distributor) FromFederator() chan FromFederator { | ||||
| 	return d.fromFederator | ||||
| } | ||||
| 
 | ||||
| func (d *distributor) ToFederator() chan ToFederator { | ||||
| 	return d.toFederator | ||||
| } | ||||
| 
 | ||||
| // Start starts the Distributor, reading from its channels and passing messages back and forth. | ||||
| func (d *distributor) Start() error { | ||||
| 	go func() { | ||||
| 	DistLoop: | ||||
| 		for { | ||||
| 			select { | ||||
| 			case clientMsg := <-d.fromClientAPI: | ||||
| 				d.log.Infof("received message FROM client API: %+v", clientMsg) | ||||
| 			case clientMsg := <-d.toClientAPI: | ||||
| 				d.log.Infof("received message TO client API: %+v", clientMsg) | ||||
| 			case federatorMsg := <-d.fromFederator: | ||||
| 				d.log.Infof("received message FROM federator: %+v", federatorMsg) | ||||
| 			case federatorMsg := <-d.toFederator: | ||||
| 				d.log.Infof("received message TO federator: %+v", federatorMsg) | ||||
| 			case <-d.stop: | ||||
| 				break DistLoop | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Stop stops the distributor cleanly, finishing handling any remaining messages before closing down. | ||||
| // TODO: empty message buffer properly before stopping otherwise we'll lose federating messages. | ||||
| func (d *distributor) Stop() error { | ||||
| 	close(d.stop) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // FromClientAPI wraps a message that travels from the client API into the distributor | ||||
| type FromClientAPI struct { | ||||
| 	APObjectType   gtsmodel.ActivityStreamsObject | ||||
| 	APActivityType gtsmodel.ActivityStreamsActivity | ||||
| 	Activity       interface{} | ||||
| } | ||||
| 
 | ||||
| // ToClientAPI wraps a message that travels from the distributor into the client API | ||||
| type ToClientAPI struct { | ||||
| 	APObjectType   gtsmodel.ActivityStreamsObject | ||||
| 	APActivityType gtsmodel.ActivityStreamsActivity | ||||
| 	Activity       interface{} | ||||
| } | ||||
| 
 | ||||
| // FromFederator wraps a message that travels from the federator into the distributor | ||||
| type FromFederator struct { | ||||
| 	APObjectType   gtsmodel.ActivityStreamsObject | ||||
| 	APActivityType gtsmodel.ActivityStreamsActivity | ||||
| 	Activity       interface{} | ||||
| } | ||||
| 
 | ||||
| // ToFederator wraps a message that travels from the distributor into the federator | ||||
| type ToFederator struct { | ||||
| 	APObjectType   gtsmodel.ActivityStreamsObject | ||||
| 	APActivityType gtsmodel.ActivityStreamsActivity | ||||
| 	Activity       interface{} | ||||
| } | ||||
|  | @ -1,70 +0,0 @@ | |||
| // Code generated by mockery v2.7.4. DO NOT EDIT. | ||||
| 
 | ||||
| package distributor | ||||
| 
 | ||||
| import mock "github.com/stretchr/testify/mock" | ||||
| 
 | ||||
| // MockDistributor is an autogenerated mock type for the Distributor type | ||||
| type MockDistributor struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| // FromClientAPI provides a mock function with given fields: | ||||
| func (_m *MockDistributor) FromClientAPI() chan FromClientAPI { | ||||
| 	ret := _m.Called() | ||||
| 
 | ||||
| 	var r0 chan FromClientAPI | ||||
| 	if rf, ok := ret.Get(0).(func() chan FromClientAPI); ok { | ||||
| 		r0 = rf() | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).(chan FromClientAPI) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // Start provides a mock function with given fields: | ||||
| func (_m *MockDistributor) Start() error { | ||||
| 	ret := _m.Called() | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func() error); ok { | ||||
| 		r0 = rf() | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // Stop provides a mock function with given fields: | ||||
| func (_m *MockDistributor) Stop() error { | ||||
| 	ret := _m.Called() | ||||
| 
 | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func() error); ok { | ||||
| 		r0 = rf() | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
| 
 | ||||
| // ToClientAPI provides a mock function with given fields: | ||||
| func (_m *MockDistributor) ToClientAPI() chan ToClientAPI { | ||||
| 	ret := _m.Called() | ||||
| 
 | ||||
| 	var r0 chan ToClientAPI | ||||
| 	if rf, ok := ret.Get(0).(func() chan ToClientAPI); ok { | ||||
| 		r0 = rf() | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).(chan ToClientAPI) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return r0 | ||||
| } | ||||
|  | @ -29,7 +29,7 @@ import ( | |||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| ) | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue