diff --git a/internal/apimodule/apimodule.go b/internal/api/apimodule.go similarity index 79% rename from internal/apimodule/apimodule.go rename to internal/api/apimodule.go index 52304fc45..5d9fc8d59 100644 --- a/internal/apimodule/apimodule.go +++ b/internal/api/apimodule.go @@ -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 } diff --git a/internal/apimodule/account/account.go b/internal/api/client/account/account.go similarity index 62% rename from internal/apimodule/account/account.go rename to internal/api/client/account/account.go index ce2dc4e2c..dce810202 100644 --- a/internal/apimodule/account/account.go +++ b/internal/api/client/account/account.go @@ -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" ) @@ -51,23 +46,17 @@ 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 - log *logrus.Logger + config *config.Config + 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, - log: log, + config: config, + 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 { diff --git a/internal/api/client/account/account_test.go b/internal/api/client/account/account_test.go new file mode 100644 index 000000000..203e2ecf9 --- /dev/null +++ b/internal/api/client/account/account_test.go @@ -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 +} diff --git a/internal/apimodule/account/accountcreate.go b/internal/api/client/account/accountcreate.go similarity index 59% rename from internal/apimodule/account/accountcreate.go rename to internal/api/client/account/accountcreate.go index 9a092fa69..b53d8c412 100644 --- a/internal/apimodule/account/accountcreate.go +++ b/internal/api/client/account/accountcreate.go @@ -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 } diff --git a/internal/api/client/account/accountcreate_test.go b/internal/api/client/account/accountcreate_test.go new file mode 100644 index 000000000..6f4c14aa7 --- /dev/null +++ b/internal/api/client/account/accountcreate_test.go @@ -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 . +// */ + +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)) +// } diff --git a/internal/apimodule/account/accountget.go b/internal/api/client/account/accountget.go similarity index 70% rename from internal/apimodule/account/accountget.go rename to internal/api/client/account/accountget.go index e39623f84..5ca17a167 100644 --- a/internal/apimodule/account/accountget.go +++ b/internal/api/client/account/accountget.go @@ -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 } diff --git a/internal/api/client/account/accountupdate.go b/internal/api/client/account/accountupdate.go new file mode 100644 index 000000000..8c43f0225 --- /dev/null +++ b/internal/api/client/account/accountupdate.go @@ -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 . +*/ + +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) +} diff --git a/internal/api/client/account/accountupdate_test.go b/internal/api/client/account/accountupdate_test.go new file mode 100644 index 000000000..6ceab6db6 --- /dev/null +++ b/internal/api/client/account/accountupdate_test.go @@ -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 . +// */ + +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)) +// } diff --git a/internal/apimodule/account/accountverify.go b/internal/api/client/account/accountverify.go similarity index 75% rename from internal/apimodule/account/accountverify.go rename to internal/api/client/account/accountverify.go index 3c7f9b9ac..4c62ff705 100644 --- a/internal/apimodule/account/accountverify.go +++ b/internal/api/client/account/accountverify.go @@ -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) } diff --git a/internal/apimodule/account/accountverify_test.go b/internal/api/client/account/accountverify_test.go similarity index 100% rename from internal/apimodule/account/accountverify_test.go rename to internal/api/client/account/accountverify_test.go diff --git a/internal/apimodule/admin/admin.go b/internal/api/client/admin/admin.go similarity index 54% rename from internal/apimodule/admin/admin.go rename to internal/api/client/admin/admin.go index 703f38257..7ce5311eb 100644 --- a/internal/apimodule/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -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 ( @@ -41,21 +37,17 @@ 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 - log *logrus.Logger + config *config.Config + 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, - log: log, + config: config, + 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 -} diff --git a/internal/apimodule/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go similarity index 60% rename from internal/apimodule/admin/emojicreate.go rename to internal/api/client/admin/emojicreate.go index 1da83a4c4..0e60db65f 100644 --- a/internal/apimodule/admin/emojicreate.go +++ b/internal/api/client/admin/emojicreate.go @@ -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") diff --git a/internal/apimodule/app/app.go b/internal/api/client/app/app.go similarity index 56% rename from internal/apimodule/app/app.go rename to internal/api/client/app/app.go index 2a1c7752f..d1e732a8c 100644 --- a/internal/apimodule/app/app.go +++ b/internal/api/client/app/app.go @@ -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,19 +33,17 @@ 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 - log *logrus.Logger + 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, - log: log, + 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 -} diff --git a/internal/apimodule/app/app_test.go b/internal/api/client/app/app_test.go similarity index 100% rename from internal/apimodule/app/app_test.go rename to internal/api/client/app/app_test.go diff --git a/internal/api/client/app/appcreate.go b/internal/api/client/app/appcreate.go new file mode 100644 index 000000000..fd42482d4 --- /dev/null +++ b/internal/api/client/app/appcreate.go @@ -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 . +*/ + +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) +} diff --git a/internal/apimodule/auth/auth.go b/internal/api/client/auth/auth.go similarity index 69% rename from internal/apimodule/auth/auth.go rename to internal/api/client/auth/auth.go index 0a6788c37..471383a75 100644 --- a/internal/apimodule/auth/auth.go +++ b/internal/api/client/auth/auth.go @@ -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,17 +39,17 @@ const ( // Module implements the ClientAPIModule interface for type Module struct { - server oauth.Server - db db.DB - log *logrus.Logger + 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, - log: log, + 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 -} diff --git a/internal/apimodule/auth/auth_test.go b/internal/api/client/auth/auth_test.go similarity index 97% rename from internal/apimodule/auth/auth_test.go rename to internal/api/client/auth/auth_test.go index 95ae0e751..7ec788a0e 100644 --- a/internal/apimodule/auth/auth_test.go +++ b/internal/api/client/auth/auth_test.go @@ -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) } diff --git a/internal/apimodule/auth/authorize.go b/internal/api/client/auth/authorize.go similarity index 97% rename from internal/apimodule/auth/authorize.go rename to internal/api/client/auth/authorize.go index 394780374..d5f8ee214 100644 --- a/internal/apimodule/auth/authorize.go +++ b/internal/api/client/auth/authorize.go @@ -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 } diff --git a/internal/apimodule/auth/middleware.go b/internal/api/client/auth/middleware.go similarity index 97% rename from internal/apimodule/auth/middleware.go rename to internal/api/client/auth/middleware.go index 1d9a85993..8ae3b49ca 100644 --- a/internal/apimodule/auth/middleware.go +++ b/internal/api/client/auth/middleware.go @@ -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" ) diff --git a/internal/apimodule/auth/signin.go b/internal/api/client/auth/signin.go similarity index 98% rename from internal/apimodule/auth/signin.go rename to internal/api/client/auth/signin.go index 44de0891c..79d9b300e 100644 --- a/internal/apimodule/auth/signin.go +++ b/internal/api/client/auth/signin.go @@ -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" ) diff --git a/internal/apimodule/auth/token.go b/internal/api/client/auth/token.go similarity index 100% rename from internal/apimodule/auth/token.go rename to internal/api/client/auth/token.go diff --git a/internal/apimodule/fileserver/fileserver.go b/internal/api/client/fileserver/fileserver.go similarity index 86% rename from internal/apimodule/fileserver/fileserver.go rename to internal/api/client/fileserver/fileserver.go index 55b8781b6..63d323a01 100644 --- a/internal/apimodule/fileserver/fileserver.go +++ b/internal/api/client/fileserver/fileserver.go @@ -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, } diff --git a/internal/apimodule/fileserver/servefile.go b/internal/api/client/fileserver/servefile.go similarity index 99% rename from internal/apimodule/fileserver/servefile.go rename to internal/api/client/fileserver/servefile.go index 59b06177f..180988288 100644 --- a/internal/apimodule/fileserver/servefile.go +++ b/internal/api/client/fileserver/servefile.go @@ -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" ) diff --git a/internal/apimodule/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go similarity index 92% rename from internal/apimodule/fileserver/servefile_test.go rename to internal/api/client/fileserver/servefile_test.go index 08800071e..ff86b5fb8 100644 --- a/internal/apimodule/fileserver/servefile_test.go +++ b/internal/api/client/fileserver/servefile_test.go @@ -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() { diff --git a/internal/apimodule/media/media.go b/internal/api/client/media/media.go similarity index 72% rename from internal/apimodule/media/media.go rename to internal/api/client/media/media.go index 89161e8df..2826783d6 100644 --- a/internal/apimodule/media/media.go +++ b/internal/api/client/media/media.go @@ -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,21 +36,17 @@ 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 - log *logrus.Logger + config *config.Config + 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, - log: log, + config: config, + processor: processor, + log: log, } } diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go new file mode 100644 index 000000000..db57e2052 --- /dev/null +++ b/internal/api/client/media/mediacreate.go @@ -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 . +*/ + +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 +} diff --git a/internal/apimodule/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go similarity index 91% rename from internal/apimodule/media/mediacreate_test.go rename to internal/api/client/media/mediacreate_test.go index 64b532dce..18823ad6c 100644 --- a/internal/apimodule/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -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, }, diff --git a/internal/apimodule/status/status.go b/internal/api/client/status/status.go similarity index 69% rename from internal/apimodule/status/status.go rename to internal/api/client/status/status.go index e370420e5..ba9295623 100644 --- a/internal/apimodule/status/status.go +++ b/internal/api/client/status/status.go @@ -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 ( @@ -79,23 +74,17 @@ 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 - log *logrus.Logger + config *config.Config + 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, - log: log, + config: config, + 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") diff --git a/internal/api/client/status/status_test.go b/internal/api/client/status/status_test.go new file mode 100644 index 000000000..1cc3231a0 --- /dev/null +++ b/internal/api/client/status/status_test.go @@ -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 +} diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/status/statuscreate.go new file mode 100644 index 000000000..daf8922b5 --- /dev/null +++ b/internal/api/client/status/statuscreate.go @@ -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 . +*/ + +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 +} diff --git a/internal/apimodule/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go similarity index 80% rename from internal/apimodule/status/statuscreate_test.go rename to internal/api/client/status/statuscreate_test.go index 891e0a9f9..58e301a04 100644 --- a/internal/apimodule/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -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) diff --git a/internal/api/client/status/statusdelete.go b/internal/api/client/status/statusdelete.go new file mode 100644 index 000000000..e55416522 --- /dev/null +++ b/internal/api/client/status/statusdelete.go @@ -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 . +*/ + +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) +} diff --git a/internal/api/client/status/statusfave.go b/internal/api/client/status/statusfave.go new file mode 100644 index 000000000..888589a8a --- /dev/null +++ b/internal/api/client/status/statusfave.go @@ -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 . +*/ + +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) +} diff --git a/internal/apimodule/status/statusfave_test.go b/internal/api/client/status/statusfave_test.go similarity index 67% rename from internal/apimodule/status/statusfave_test.go rename to internal/api/client/status/statusfave_test.go index 76285d6dd..6c6403988 100644 --- a/internal/apimodule/status/statusfave_test.go +++ b/internal/api/client/status/statusfave_test.go @@ -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) { diff --git a/internal/api/client/status/statusfavedby.go b/internal/api/client/status/statusfavedby.go new file mode 100644 index 000000000..799acb7d2 --- /dev/null +++ b/internal/api/client/status/statusfavedby.go @@ -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 . +*/ + +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) +} diff --git a/internal/apimodule/status/statusfavedby_test.go b/internal/api/client/status/statusfavedby_test.go similarity index 62% rename from internal/apimodule/status/statusfavedby_test.go rename to internal/api/client/status/statusfavedby_test.go index dae76a8fc..62c027a39 100644 --- a/internal/apimodule/status/statusfavedby_test.go +++ b/internal/api/client/status/statusfavedby_test.go @@ -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) diff --git a/internal/api/client/status/statusget.go b/internal/api/client/status/statusget.go new file mode 100644 index 000000000..c6239cb36 --- /dev/null +++ b/internal/api/client/status/statusget.go @@ -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 . +*/ + +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) +} diff --git a/internal/apimodule/status/statusget_test.go b/internal/api/client/status/statusget_test.go similarity index 65% rename from internal/apimodule/status/statusget_test.go rename to internal/api/client/status/statusget_test.go index fb0d9dd7d..9bc12d250 100644 --- a/internal/apimodule/status/statusget_test.go +++ b/internal/api/client/status/statusget_test.go @@ -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() { diff --git a/internal/api/client/status/statusunfave.go b/internal/api/client/status/statusunfave.go new file mode 100644 index 000000000..94fd662de --- /dev/null +++ b/internal/api/client/status/statusunfave.go @@ -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 . +*/ + +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) +} diff --git a/internal/apimodule/status/statusunfave_test.go b/internal/api/client/status/statusunfave_test.go similarity index 70% rename from internal/apimodule/status/statusunfave_test.go rename to internal/api/client/status/statusunfave_test.go index 51d2b39ac..daf4b0696 100644 --- a/internal/apimodule/status/statusunfave_test.go +++ b/internal/api/client/status/statusunfave_test.go @@ -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) } diff --git a/internal/apimodule/federation/federation.go b/internal/api/federation/federation.go similarity index 83% rename from internal/apimodule/federation/federation.go rename to internal/api/federation/federation.go index 82744085d..cc3b33599 100644 --- a/internal/apimodule/federation/federation.go +++ b/internal/api/federation/federation.go @@ -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 -} diff --git a/internal/api/federation/users.go b/internal/api/federation/users.go new file mode 100644 index 000000000..f43842664 --- /dev/null +++ b/internal/api/federation/users.go @@ -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 . +*/ + +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) +} diff --git a/internal/mastotypes/account.go b/internal/api/model/account.go similarity index 97% rename from internal/mastotypes/account.go rename to internal/api/model/account.go index bbcf9c90f..efb69d6fd 100644 --- a/internal/mastotypes/account.go +++ b/internal/api/model/account.go @@ -16,9 +16,12 @@ along with this program. If not, see . */ -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. diff --git a/internal/mastotypes/activity.go b/internal/api/model/activity.go similarity index 98% rename from internal/mastotypes/activity.go rename to internal/api/model/activity.go index b8dbf2c1b..c1736a8d6 100644 --- a/internal/mastotypes/activity.go +++ b/internal/api/model/activity.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Activity represents the mastodon-api Activity type. See here: https://docs.joinmastodon.org/entities/activity/ type Activity struct { diff --git a/internal/mastotypes/admin.go b/internal/api/model/admin.go similarity index 99% rename from internal/mastotypes/admin.go rename to internal/api/model/admin.go index 71c2bb309..036218f77 100644 --- a/internal/mastotypes/admin.go +++ b/internal/api/model/admin.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/announcement.go b/internal/api/model/announcement.go similarity index 98% rename from internal/mastotypes/announcement.go rename to internal/api/model/announcement.go index 882d6bb9b..eeb4b8720 100644 --- a/internal/mastotypes/announcement.go +++ b/internal/api/model/announcement.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Announcement represents an admin/moderator announcement for local users. See here: https://docs.joinmastodon.org/entities/announcement/ type Announcement struct { diff --git a/internal/mastotypes/announcementreaction.go b/internal/api/model/announcementreaction.go similarity index 98% rename from internal/mastotypes/announcementreaction.go rename to internal/api/model/announcementreaction.go index 444c57e2c..81118fef0 100644 --- a/internal/mastotypes/announcementreaction.go +++ b/internal/api/model/announcementreaction.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // AnnouncementReaction represents a user reaction to admin/moderator announcement. See here: https://docs.joinmastodon.org/entities/announcementreaction/ type AnnouncementReaction struct { diff --git a/internal/mastotypes/application.go b/internal/api/model/application.go similarity index 94% rename from internal/mastotypes/application.go rename to internal/api/model/application.go index 6140a0127..a796c88ea 100644 --- a/internal/mastotypes/application.go +++ b/internal/api/model/application.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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. diff --git a/internal/mastotypes/attachment.go b/internal/api/model/attachment.go similarity index 99% rename from internal/mastotypes/attachment.go rename to internal/api/model/attachment.go index bda79a8ee..d90247f83 100644 --- a/internal/mastotypes/attachment.go +++ b/internal/api/model/attachment.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model import "mime/multipart" diff --git a/internal/mastotypes/card.go b/internal/api/model/card.go similarity index 99% rename from internal/mastotypes/card.go rename to internal/api/model/card.go index d1147e04b..ffa6d53e5 100644 --- a/internal/mastotypes/card.go +++ b/internal/api/model/card.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/context.go b/internal/api/model/context.go similarity index 98% rename from internal/mastotypes/context.go rename to internal/api/model/context.go index 397522dc7..d0979319b 100644 --- a/internal/mastotypes/context.go +++ b/internal/api/model/context.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/conversation.go b/internal/api/model/conversation.go similarity index 98% rename from internal/mastotypes/conversation.go rename to internal/api/model/conversation.go index ed95c124c..b0568c17e 100644 --- a/internal/mastotypes/conversation.go +++ b/internal/api/model/conversation.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Conversation represents a conversation with "direct message" visibility. See https://docs.joinmastodon.org/entities/conversation/ type Conversation struct { diff --git a/internal/mastotypes/emoji.go b/internal/api/model/emoji.go similarity index 98% rename from internal/mastotypes/emoji.go rename to internal/api/model/emoji.go index c50ca6343..c2834718f 100644 --- a/internal/mastotypes/emoji.go +++ b/internal/api/model/emoji.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model import "mime/multipart" diff --git a/internal/mastotypes/error.go b/internal/api/model/error.go similarity index 98% rename from internal/mastotypes/error.go rename to internal/api/model/error.go index 394085724..f145d69f2 100644 --- a/internal/mastotypes/error.go +++ b/internal/api/model/error.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Error represents an error message returned from the API. See https://docs.joinmastodon.org/entities/error/ type Error struct { diff --git a/internal/mastotypes/featuredtag.go b/internal/api/model/featuredtag.go similarity index 98% rename from internal/mastotypes/featuredtag.go rename to internal/api/model/featuredtag.go index 0e0bbe802..3df3fe4c9 100644 --- a/internal/mastotypes/featuredtag.go +++ b/internal/api/model/featuredtag.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // FeaturedTag represents a hashtag that is featured on a profile. See https://docs.joinmastodon.org/entities/featuredtag/ type FeaturedTag struct { diff --git a/internal/mastotypes/field.go b/internal/api/model/field.go similarity index 98% rename from internal/mastotypes/field.go rename to internal/api/model/field.go index 29b5a1803..2e7662b2b 100644 --- a/internal/mastotypes/field.go +++ b/internal/api/model/field.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/filter.go b/internal/api/model/filter.go similarity index 99% rename from internal/mastotypes/filter.go rename to internal/api/model/filter.go index 86d9795a3..519922ba3 100644 --- a/internal/mastotypes/filter.go +++ b/internal/api/model/filter.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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: diff --git a/internal/mastotypes/history.go b/internal/api/model/history.go similarity index 98% rename from internal/mastotypes/history.go rename to internal/api/model/history.go index 235761378..d8b4d6b4f 100644 --- a/internal/mastotypes/history.go +++ b/internal/api/model/history.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // History represents daily usage history of a hashtag. See https://docs.joinmastodon.org/entities/history/ type History struct { diff --git a/internal/mastotypes/identityproof.go b/internal/api/model/identityproof.go similarity index 98% rename from internal/mastotypes/identityproof.go rename to internal/api/model/identityproof.go index 7265d46e3..400835fca 100644 --- a/internal/mastotypes/identityproof.go +++ b/internal/api/model/identityproof.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // IdentityProof represents a proof from an external identity provider. See https://docs.joinmastodon.org/entities/identityproof/ type IdentityProof struct { diff --git a/internal/mastotypes/instance.go b/internal/api/model/instance.go similarity index 99% rename from internal/mastotypes/instance.go rename to internal/api/model/instance.go index 10e626a8e..857a8acc5 100644 --- a/internal/mastotypes/instance.go +++ b/internal/api/model/instance.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/list.go b/internal/api/model/list.go similarity index 98% rename from internal/mastotypes/list.go rename to internal/api/model/list.go index 5b704367b..220cde59e 100644 --- a/internal/mastotypes/list.go +++ b/internal/api/model/list.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/marker.go b/internal/api/model/marker.go similarity index 98% rename from internal/mastotypes/marker.go rename to internal/api/model/marker.go index 790322313..1e39f1516 100644 --- a/internal/mastotypes/marker.go +++ b/internal/api/model/marker.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/mention.go b/internal/api/model/mention.go similarity index 98% rename from internal/mastotypes/mention.go rename to internal/api/model/mention.go index 81a593d99..a7985af24 100644 --- a/internal/mastotypes/mention.go +++ b/internal/api/model/mention.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Mention represents the mastodon-api mention type, as documented here: https://docs.joinmastodon.org/entities/mention/ type Mention struct { diff --git a/internal/mastotypes/notification.go b/internal/api/model/notification.go similarity index 98% rename from internal/mastotypes/notification.go rename to internal/api/model/notification.go index 26d361b43..c8d080e2a 100644 --- a/internal/mastotypes/notification.go +++ b/internal/api/model/notification.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/oauth.go b/internal/api/model/oauth.go similarity index 98% rename from internal/mastotypes/oauth.go rename to internal/api/model/oauth.go index d93ea079f..250d2218f 100644 --- a/internal/mastotypes/oauth.go +++ b/internal/api/model/oauth.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // OAuthAuthorize represents a request sent to https://example.org/oauth/authorize // See here: https://docs.joinmastodon.org/methods/apps/oauth/ diff --git a/internal/mastotypes/poll.go b/internal/api/model/poll.go similarity index 99% rename from internal/mastotypes/poll.go rename to internal/api/model/poll.go index bedaebec2..b00e7680a 100644 --- a/internal/mastotypes/poll.go +++ b/internal/api/model/poll.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Poll represents the mastodon-api poll type, as described here: https://docs.joinmastodon.org/entities/poll/ type Poll struct { diff --git a/internal/mastotypes/preferences.go b/internal/api/model/preferences.go similarity index 98% rename from internal/mastotypes/preferences.go rename to internal/api/model/preferences.go index c28f5d5ab..9e410091e 100644 --- a/internal/mastotypes/preferences.go +++ b/internal/api/model/preferences.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Preferences represents a user's preferences. See https://docs.joinmastodon.org/entities/preferences/ type Preferences struct { diff --git a/internal/mastotypes/pushsubscription.go b/internal/api/model/pushsubscription.go similarity index 99% rename from internal/mastotypes/pushsubscription.go rename to internal/api/model/pushsubscription.go index 4d7535100..f34c63374 100644 --- a/internal/mastotypes/pushsubscription.go +++ b/internal/api/model/pushsubscription.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // PushSubscription represents a subscription to the push streaming server. See https://docs.joinmastodon.org/entities/pushsubscription/ type PushSubscription struct { diff --git a/internal/mastotypes/relationship.go b/internal/api/model/relationship.go similarity index 99% rename from internal/mastotypes/relationship.go rename to internal/api/model/relationship.go index 1e0bbab46..6e71023e2 100644 --- a/internal/mastotypes/relationship.go +++ b/internal/api/model/relationship.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Relationship represents a relationship between accounts. See https://docs.joinmastodon.org/entities/relationship/ type Relationship struct { diff --git a/internal/mastotypes/results.go b/internal/api/model/results.go similarity index 98% rename from internal/mastotypes/results.go rename to internal/api/model/results.go index 3fa7c7abb..1b2625a0d 100644 --- a/internal/mastotypes/results.go +++ b/internal/api/model/results.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package mastotypes +package model // Results represents the results of a search. See https://docs.joinmastodon.org/entities/results/ type Results struct { diff --git a/internal/mastotypes/scheduledstatus.go b/internal/api/model/scheduledstatus.go similarity index 98% rename from internal/mastotypes/scheduledstatus.go rename to internal/api/model/scheduledstatus.go index ff45eaade..deafd22aa 100644 --- a/internal/mastotypes/scheduledstatus.go +++ b/internal/api/model/scheduledstatus.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/source.go b/internal/api/model/source.go similarity index 98% rename from internal/mastotypes/source.go rename to internal/api/model/source.go index 0445a1ffb..441af71de 100644 --- a/internal/mastotypes/source.go +++ b/internal/api/model/source.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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. diff --git a/internal/mastotypes/status.go b/internal/api/model/status.go similarity index 90% rename from internal/mastotypes/status.go rename to internal/api/model/status.go index f5cc07a06..faf88ae84 100644 --- a/internal/mastotypes/status.go +++ b/internal/api/model/status.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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"` +} diff --git a/internal/mastotypes/tag.go b/internal/api/model/tag.go similarity index 98% rename from internal/mastotypes/tag.go rename to internal/api/model/tag.go index 82e6e6618..f009b4cef 100644 --- a/internal/mastotypes/tag.go +++ b/internal/api/model/tag.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/mastotypes/token.go b/internal/api/model/token.go similarity index 98% rename from internal/mastotypes/token.go rename to internal/api/model/token.go index c9ac1f177..611ab214c 100644 --- a/internal/mastotypes/token.go +++ b/internal/api/model/token.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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 { diff --git a/internal/apimodule/security/flocblock.go b/internal/api/security/flocblock.go similarity index 100% rename from internal/apimodule/security/flocblock.go rename to internal/api/security/flocblock.go diff --git a/internal/apimodule/security/security.go b/internal/api/security/security.go similarity index 78% rename from internal/apimodule/security/security.go rename to internal/api/security/security.go index 8f805bc93..c80b568b3 100644 --- a/internal/apimodule/security/security.go +++ b/internal/api/security/security.go @@ -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 -} diff --git a/internal/apimodule/account/accountcreate_test.go b/internal/apimodule/account/accountcreate_test.go deleted file mode 100644 index e84b02583..000000000 --- a/internal/apimodule/account/accountcreate_test.go +++ /dev/null @@ -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 . -*/ - -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)) -} diff --git a/internal/apimodule/account/accountupdate.go b/internal/apimodule/account/accountupdate.go deleted file mode 100644 index 5e3760baa..000000000 --- a/internal/apimodule/account/accountupdate.go +++ /dev/null @@ -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 . -*/ - -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() -} diff --git a/internal/apimodule/account/accountupdate_test.go b/internal/apimodule/account/accountupdate_test.go deleted file mode 100644 index 4b6bd50f6..000000000 --- a/internal/apimodule/account/accountupdate_test.go +++ /dev/null @@ -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 . -*/ - -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)) -} diff --git a/internal/apimodule/app/appcreate.go b/internal/apimodule/app/appcreate.go deleted file mode 100644 index dd78dddfc..000000000 --- a/internal/apimodule/app/appcreate.go +++ /dev/null @@ -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 . -*/ - -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) -} diff --git a/internal/apimodule/federation/users.go b/internal/apimodule/federation/users.go deleted file mode 100644 index 668e51f82..000000000 --- a/internal/apimodule/federation/users.go +++ /dev/null @@ -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 . -*/ - -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) -} diff --git a/internal/apimodule/media/mediacreate.go b/internal/apimodule/media/mediacreate.go deleted file mode 100644 index 75d80b966..000000000 --- a/internal/apimodule/media/mediacreate.go +++ /dev/null @@ -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 . -*/ - -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 -} diff --git a/internal/apimodule/mock_ClientAPIModule.go b/internal/apimodule/mock_ClientAPIModule.go deleted file mode 100644 index 2d4293d0e..000000000 --- a/internal/apimodule/mock_ClientAPIModule.go +++ /dev/null @@ -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 -} diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go deleted file mode 100644 index 79b6fb2a0..000000000 --- a/internal/apimodule/status/statuscreate.go +++ /dev/null @@ -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 . -*/ - -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 -} diff --git a/internal/apimodule/status/statusdelete.go b/internal/apimodule/status/statusdelete.go deleted file mode 100644 index cb49a2b63..000000000 --- a/internal/apimodule/status/statusdelete.go +++ /dev/null @@ -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 . -*/ - -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) -} diff --git a/internal/apimodule/status/statusfave.go b/internal/apimodule/status/statusfave.go deleted file mode 100644 index 8f17315b8..000000000 --- a/internal/apimodule/status/statusfave.go +++ /dev/null @@ -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 . -*/ - -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) -} diff --git a/internal/apimodule/status/statusfavedby.go b/internal/apimodule/status/statusfavedby.go deleted file mode 100644 index 52c28069d..000000000 --- a/internal/apimodule/status/statusfavedby.go +++ /dev/null @@ -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 . -*/ - -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) -} diff --git a/internal/apimodule/status/statusget.go b/internal/apimodule/status/statusget.go deleted file mode 100644 index edaf2d53c..000000000 --- a/internal/apimodule/status/statusget.go +++ /dev/null @@ -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 . -*/ - -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) -} diff --git a/internal/apimodule/status/statusunfave.go b/internal/apimodule/status/statusunfave.go deleted file mode 100644 index b857611fe..000000000 --- a/internal/apimodule/status/statusunfave.go +++ /dev/null @@ -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 . -*/ - -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) -} diff --git a/internal/cache/mock_Cache.go b/internal/cache/mock_Cache.go deleted file mode 100644 index d8d18d68a..000000000 --- a/internal/cache/mock_Cache.go +++ /dev/null @@ -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 -} diff --git a/internal/config/mock_KeyedFlags.go b/internal/config/mock_KeyedFlags.go deleted file mode 100644 index 95057d1d3..000000000 --- a/internal/config/mock_KeyedFlags.go +++ /dev/null @@ -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 -} diff --git a/internal/db/db.go b/internal/db/db.go index 4e3bc87e2..3e085e180 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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) - } -} diff --git a/internal/db/federating_db.go b/internal/db/federating_db.go index 401a0edf5..a1746cd2d 100644 --- a/internal/db/federating_db.go +++ b/internal/db/federating_db.go @@ -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, diff --git a/internal/db/mock_DB.go b/internal/db/mock_DB.go deleted file mode 100644 index df2e41907..000000000 --- a/internal/db/mock_DB.go +++ /dev/null @@ -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 -} diff --git a/internal/db/pg.go b/internal/db/pg.go index 46ee680f8..c4f1280ea 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -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 diff --git a/internal/db/pg_test.go b/internal/db/pg_test.go index f9bd21c48..a54784022 100644 --- a/internal/db/pg_test.go +++ b/internal/db/pg_test.go @@ -16,6 +16,6 @@ along with this program. If not, see . */ -package db +package db_test // TODO: write tests for postgres diff --git a/internal/distributor/distributor.go b/internal/distributor/distributor.go deleted file mode 100644 index 09e827396..000000000 --- a/internal/distributor/distributor.go +++ /dev/null @@ -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 . -*/ - -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{} -} diff --git a/internal/distributor/mock_Distributor.go b/internal/distributor/mock_Distributor.go deleted file mode 100644 index 42248c3f2..000000000 --- a/internal/distributor/mock_Distributor.go +++ /dev/null @@ -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 -} diff --git a/internal/federation/commonbehavior.go b/internal/federation/commonbehavior.go index cb8b2b613..51db2f640 100644 --- a/internal/federation/commonbehavior.go +++ b/internal/federation/commonbehavior.go @@ -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" ) diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 841ed8153..657cd8081 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -30,7 +30,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/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" @@ -157,7 +157,7 @@ func (f *federatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.R if err != nil { return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", publicKeyOwnerURI.String(), err) } - + a, err := f.typeConverter.ASPersonToAccount(person) if err != nil { return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err) @@ -165,7 +165,7 @@ func (f *federatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.R requestingAccount = a } - return newContext, true, nil + return nil, true, nil } // Blocked should determine whether to permit a set of actors given by diff --git a/internal/federation/federator.go b/internal/federation/federator.go index a38e2d09a..4db469bde 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -23,7 +23,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/distributor" + "github.com/superseriousbusiness/gotosocial/internal/message" "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) @@ -38,7 +38,7 @@ type Federator interface { type federator struct { actor pub.FederatingActor - distributor distributor.Distributor + processor message.Processor federatingProtocol pub.FederatingProtocol commonBehavior pub.CommonBehavior clock pub.Clock @@ -46,7 +46,7 @@ type federator struct { } // NewFederator returns a new federator -func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, distributor distributor.Distributor, typeConverter typeutils.TypeConverter) Federator { +func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, processor message.Processor, typeConverter typeutils.TypeConverter) Federator { clock := &Clock{} federatingProtocol := newFederatingProtocol(db, log, config, transportController, typeConverter) @@ -55,7 +55,7 @@ func NewFederator(db db.DB, transportController transport.Controller, config *co return &federator{ actor: actor, - distributor: distributor, + processor: processor, federatingProtocol: federatingProtocol, commonBehavior: commonBehavior, clock: clock, diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go index 3081d0604..e1b2db062 100644 --- a/internal/federation/federator_test.go +++ b/internal/federation/federator_test.go @@ -37,9 +37,10 @@ import ( "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/federation" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/message" + "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" @@ -50,7 +51,8 @@ type ProtocolTestSuite struct { config *config.Config db db.DB log *logrus.Logger - distributor distributor.Distributor + processor message.Processor + storage storage.Storage typeConverter typeutils.TypeConverter accounts map[string]*gtsmodel.Account activities map[string]testrig.ActivityWithSignature @@ -62,7 +64,8 @@ func (suite *ProtocolTestSuite) SetupSuite() { suite.config = testrig.NewTestConfig() suite.db = testrig.NewTestDB() suite.log = testrig.NewTestLog() - suite.distributor = testrig.NewTestDistributor() + suite.storage = testrig.NewTestStorage() + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) suite.typeConverter = testrig.NewTestTypeConverter(suite.db) suite.accounts = testrig.NewTestAccounts() suite.activities = testrig.NewTestActivities(suite.accounts) @@ -89,7 +92,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() { return nil, nil })) // setup module being tested - federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.distributor, suite.typeConverter) + federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.processor, suite.typeConverter) // setup request ctx := context.Background() @@ -155,7 +158,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { })) // now setup module being tested, with the mock transport controller - federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.distributor, suite.typeConverter) + federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.processor, suite.typeConverter) // setup request ctx := context.Background() diff --git a/internal/federation/util.go b/internal/federation/util.go index 9bc47d90f..28fa45826 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -32,7 +32,7 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "github.com/go-fed/httpsig" - "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) /* diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index e39a697e4..d7eac3d39 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -28,21 +28,20 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/action" - "github.com/superseriousbusiness/gotosocial/internal/apimodule" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/account" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/admin" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/app" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/auth" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver" - mediaModule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/security" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/status" - "github.com/superseriousbusiness/gotosocial/internal/cache" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" + "github.com/superseriousbusiness/gotosocial/internal/api/client/app" + "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" + "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" + mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" + "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/security" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/distributor" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/message" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/storage" @@ -52,7 +51,7 @@ import ( // Run creates and starts a gotosocial server var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { - dbService, err := db.New(ctx, c, log) + dbService, err := db.NewPostgresService(ctx, c, log) if err != nil { return fmt.Errorf("error creating dbservice: %s", err) } @@ -73,24 +72,24 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr // build backend handlers mediaHandler := media.New(c, dbService, storageBackend, log) oauthServer := oauth.New(dbService, log) - distributor := distributor.New(log) - if err := distributor.Start(); err != nil { - return fmt.Errorf("error starting distributor: %s", err) + processor := message.NewProcessor(c, typeConverter, oauthServer, mediaHandler, dbService, log) + if err := processor.Start(); err != nil { + return fmt.Errorf("error starting processor: %s", err) } transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log) - federator := federation.NewFederator(dbService, transportController, c, log, distributor, typeConverter) + federator := federation.NewFederator(dbService, transportController, c, log, processor, typeConverter) // build client api modules - authModule := auth.New(oauthServer, dbService, log) - accountModule := account.New(c, dbService, oauthServer, mediaHandler, typeConverter, log) - appsModule := app.New(oauthServer, dbService, typeConverter, log) - mm := mediaModule.New(dbService, mediaHandler, typeConverter, c, log) - fileServerModule := fileserver.New(c, dbService, storageBackend, log) - adminModule := admin.New(c, dbService, mediaHandler, typeConverter, log) - statusModule := status.New(c, dbService, mediaHandler, typeConverter, distributor, log) + authModule := auth.New(c, processor, log) + accountModule := account.New(c, processor, log) + appsModule := app.New(c, processor, log) + mm := mediaModule.New(c, processor, log) + fileServerModule := fileserver.New(c, processor, log) + adminModule := admin.New(c, processor, log) + statusModule := status.New(c, processor, log) securityModule := security.New(c, log) - apiModules := []apimodule.ClientAPIModule{ + apis := []api.ClientModule{ // modules with middleware go first securityModule, authModule, @@ -104,20 +103,17 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr statusModule, } - for _, m := range apiModules { + for _, m := range apis { if err := m.Route(router); err != nil { return fmt.Errorf("routing error: %s", err) } - if err := m.CreateTables(dbService); err != nil { - return fmt.Errorf("table creation error: %s", err) - } } if err := dbService.CreateInstanceAccount(); err != nil { return fmt.Errorf("error creating instance account: %s", err) } - gts, err := New(dbService, &cache.MockCache{}, router, federator, c) + gts, err := New(dbService, router, federator, c) if err != nil { return fmt.Errorf("error creating gotosocial service: %s", err) } diff --git a/internal/gotosocial/gotosocial.go b/internal/gotosocial/gotosocial.go index 461c189ad..f20e1161d 100644 --- a/internal/gotosocial/gotosocial.go +++ b/internal/gotosocial/gotosocial.go @@ -21,7 +21,6 @@ package gotosocial import ( "context" - "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation" @@ -38,10 +37,9 @@ type Gotosocial interface { // New returns a new gotosocial server, initialized with the given configuration. // An error will be returned the caller if something goes wrong during initialization // eg., no db or storage connection, port for router already in use, etc. -func New(db db.DB, cache cache.Cache, apiRouter router.Router, federator federation.Federator, config *config.Config) (Gotosocial, error) { +func New(db db.DB, apiRouter router.Router, federator federation.Federator, config *config.Config) (Gotosocial, error) { return &gotosocial{ db: db, - cache: cache, apiRouter: apiRouter, federator: federator, config: config, @@ -51,7 +49,6 @@ func New(db db.DB, cache cache.Cache, apiRouter router.Router, federator federat // gotosocial fulfils the gotosocial interface. type gotosocial struct { db db.DB - cache cache.Cache apiRouter router.Router federator federation.Federator config *config.Config diff --git a/internal/gotosocial/mock_Gotosocial.go b/internal/gotosocial/mock_Gotosocial.go deleted file mode 100644 index 66f776e5c..000000000 --- a/internal/gotosocial/mock_Gotosocial.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated by mockery v2.7.4. DO NOT EDIT. - -package gotosocial - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockGotosocial is an autogenerated mock type for the Gotosocial type -type MockGotosocial struct { - mock.Mock -} - -// Start provides a mock function with given fields: _a0 -func (_m *MockGotosocial) Start(_a0 context.Context) error { - ret := _m.Called(_a0) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Stop provides a mock function with given fields: _a0 -func (_m *MockGotosocial) Stop(_a0 context.Context) error { - ret := _m.Called(_a0) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/internal/db/gtsmodel/README.md b/internal/gtsmodel/README.md similarity index 100% rename from internal/db/gtsmodel/README.md rename to internal/gtsmodel/README.md diff --git a/internal/db/gtsmodel/account.go b/internal/gtsmodel/account.go similarity index 100% rename from internal/db/gtsmodel/account.go rename to internal/gtsmodel/account.go diff --git a/internal/db/gtsmodel/activitystreams.go b/internal/gtsmodel/activitystreams.go similarity index 100% rename from internal/db/gtsmodel/activitystreams.go rename to internal/gtsmodel/activitystreams.go diff --git a/internal/db/gtsmodel/application.go b/internal/gtsmodel/application.go similarity index 100% rename from internal/db/gtsmodel/application.go rename to internal/gtsmodel/application.go diff --git a/internal/db/gtsmodel/block.go b/internal/gtsmodel/block.go similarity index 100% rename from internal/db/gtsmodel/block.go rename to internal/gtsmodel/block.go diff --git a/internal/db/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go similarity index 100% rename from internal/db/gtsmodel/domainblock.go rename to internal/gtsmodel/domainblock.go diff --git a/internal/db/gtsmodel/emaildomainblock.go b/internal/gtsmodel/emaildomainblock.go similarity index 100% rename from internal/db/gtsmodel/emaildomainblock.go rename to internal/gtsmodel/emaildomainblock.go diff --git a/internal/db/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go similarity index 100% rename from internal/db/gtsmodel/emoji.go rename to internal/gtsmodel/emoji.go diff --git a/internal/db/gtsmodel/follow.go b/internal/gtsmodel/follow.go similarity index 100% rename from internal/db/gtsmodel/follow.go rename to internal/gtsmodel/follow.go diff --git a/internal/db/gtsmodel/followrequest.go b/internal/gtsmodel/followrequest.go similarity index 100% rename from internal/db/gtsmodel/followrequest.go rename to internal/gtsmodel/followrequest.go diff --git a/internal/db/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go similarity index 100% rename from internal/db/gtsmodel/mediaattachment.go rename to internal/gtsmodel/mediaattachment.go diff --git a/internal/db/gtsmodel/mention.go b/internal/gtsmodel/mention.go similarity index 100% rename from internal/db/gtsmodel/mention.go rename to internal/gtsmodel/mention.go diff --git a/internal/db/gtsmodel/poll.go b/internal/gtsmodel/poll.go similarity index 100% rename from internal/db/gtsmodel/poll.go rename to internal/gtsmodel/poll.go diff --git a/internal/db/gtsmodel/status.go b/internal/gtsmodel/status.go similarity index 100% rename from internal/db/gtsmodel/status.go rename to internal/gtsmodel/status.go diff --git a/internal/db/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go similarity index 100% rename from internal/db/gtsmodel/statusbookmark.go rename to internal/gtsmodel/statusbookmark.go diff --git a/internal/db/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go similarity index 100% rename from internal/db/gtsmodel/statusfave.go rename to internal/gtsmodel/statusfave.go diff --git a/internal/db/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go similarity index 100% rename from internal/db/gtsmodel/statusmute.go rename to internal/gtsmodel/statusmute.go diff --git a/internal/db/gtsmodel/statuspin.go b/internal/gtsmodel/statuspin.go similarity index 100% rename from internal/db/gtsmodel/statuspin.go rename to internal/gtsmodel/statuspin.go diff --git a/internal/db/gtsmodel/tag.go b/internal/gtsmodel/tag.go similarity index 100% rename from internal/db/gtsmodel/tag.go rename to internal/gtsmodel/tag.go diff --git a/internal/db/gtsmodel/user.go b/internal/gtsmodel/user.go similarity index 100% rename from internal/db/gtsmodel/user.go rename to internal/gtsmodel/user.go diff --git a/internal/mastotypes/README.md b/internal/mastotypes/README.md deleted file mode 100644 index 38f9e89c4..000000000 --- a/internal/mastotypes/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Mastotypes - -This package contains Go types/structs for Mastodon's REST API. - -See [here](https://docs.joinmastodon.org/methods/apps/). diff --git a/internal/media/media.go b/internal/media/media.go index 07b1d027c..97242b5ec 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -28,7 +28,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/storage" ) diff --git a/internal/media/media_test.go b/internal/media/media_test.go index 58f2e029e..8045295d2 100644 --- a/internal/media/media_test.go +++ b/internal/media/media_test.go @@ -29,7 +29,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/storage" ) @@ -78,7 +78,7 @@ func (suite *MediaTestSuite) SetupSuite() { } 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) + database, err := db.NewPostgresService(context.Background(), c, log) if err != nil { suite.FailNow(err.Error()) } diff --git a/internal/media/mock_MediaHandler.go b/internal/media/mock_MediaHandler.go index 1f875557a..10fffbba4 100644 --- a/internal/media/mock_MediaHandler.go +++ b/internal/media/mock_MediaHandler.go @@ -4,7 +4,7 @@ package media import ( mock "github.com/stretchr/testify/mock" - gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) // MockMediaHandler is an autogenerated mock type for the MediaHandler type diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go new file mode 100644 index 000000000..e62622753 --- /dev/null +++ b/internal/message/accountprocess.go @@ -0,0 +1,172 @@ +package message + +import ( + "errors" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// accountCreate does the dirty work of making an account and user in the database. +// It then returns a token to the caller, for use with the new account, as per the +// spec here: https://docs.joinmastodon.org/methods/accounts/ +func (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) { + l := p.log.WithField("func", "accountCreate") + + if err := p.db.IsEmailAvailable(form.Email); err != nil { + return nil, err + } + + if err := p.db.IsUsernameAvailable(form.Username); err != nil { + return nil, err + } + + // don't store a reason if we don't require one + reason := form.Reason + if !p.config.AccountsConfig.ReasonRequired { + reason = "" + } + + l.Trace("creating new username and account") + user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, authed.Application.ID) + if err != nil { + return nil, fmt.Errorf("error creating new signup in the database: %s", err) + } + + l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, authed.Application.ID) + accessToken, err := p.oauthServer.GenerateUserAccessToken(authed.Token, authed.Application.ClientSecret, user.ID) + if err != nil { + return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err) + } + + return &apimodel.Token{ + AccessToken: accessToken.GetAccess(), + TokenType: "Bearer", + Scope: accessToken.GetScope(), + CreatedAt: accessToken.GetAccessCreateAt().Unix(), + }, nil +} + +func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) { + targetAccount := >smodel.Account{} + if err := p.db.GetByID(targetAccountID, targetAccount); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return nil, errors.New("account not found") + } + return nil, fmt.Errorf("db error: %s", err) + } + + var mastoAccount *apimodel.Account + var err error + if authed.Account != nil && targetAccount.ID == authed.Account.ID { + mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount) + } else { + mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount) + } + if err != nil { + return nil, fmt.Errorf("error converting account: %s", err) + } + return mastoAccount, nil +} + +func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) { + l := p.log.WithField("func", "AccountUpdate") + + if form.Discoverable != nil { + if err := p.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil { + return nil, fmt.Errorf("error updating discoverable: %s", err) + } + } + + if form.Bot != nil { + if err := p.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil { + return nil, fmt.Errorf("error updating bot: %s", err) + } + } + + if form.DisplayName != nil { + if err := util.ValidateDisplayName(*form.DisplayName); err != nil { + return nil, err + } + if err := p.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil { + return nil, err + } + } + + if form.Note != nil { + if err := util.ValidateNote(*form.Note); err != nil { + return nil, err + } + if err := p.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, >smodel.Account{}); err != nil { + return nil, err + } + } + + if form.Avatar != nil && form.Avatar.Size != 0 { + avatarInfo, err := p.updateAccountAvatar(form.Avatar, authed.Account.ID) + if err != nil { + return nil, err + } + l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo) + } + + if form.Header != nil && form.Header.Size != 0 { + headerInfo, err := p.updateAccountHeader(form.Header, authed.Account.ID) + if err != nil { + return nil, err + } + l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo) + } + + if form.Locked != nil { + if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil { + return nil, err + } + } + + if form.Source != nil { + if form.Source.Language != nil { + if err := util.ValidateLanguage(*form.Source.Language); err != nil { + return nil, err + } + if err := p.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil { + return nil, err + } + } + + if form.Source.Sensitive != nil { + if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil { + return nil, err + } + } + + if form.Source.Privacy != nil { + if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil { + return nil, err + } + if err := p.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil { + return nil, err + } + } + } + + // if form.FieldsAttributes != nil { + // // TODO: parse fields attributes nicely and update + // } + + // fetch the account with all updated values set + updatedAccount := >smodel.Account{} + if err := p.db.GetByID(authed.Account.ID, updatedAccount); err != nil { + return nil, fmt.Errorf("could not fetch updated account %s: %s", authed.Account.ID, err) + } + + acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount) + if err != nil { + return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err) + } + return acctSensitive, nil +} diff --git a/internal/message/adminprocess.go b/internal/message/adminprocess.go new file mode 100644 index 000000000..abf7b61c7 --- /dev/null +++ b/internal/message/adminprocess.go @@ -0,0 +1,48 @@ +package message + +import ( + "bytes" + "errors" + "fmt" + "io" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) { + if !authed.User.Admin { + return nil, fmt.Errorf("user %s not an admin", authed.User.ID) + } + + // open the emoji and extract the bytes from it + f, err := form.Image.Open() + if err != nil { + return nil, fmt.Errorf("error opening emoji: %s", err) + } + buf := new(bytes.Buffer) + size, err := io.Copy(buf, f) + if err != nil { + return nil, fmt.Errorf("error reading emoji: %s", err) + } + if size == 0 { + return nil, errors.New("could not read provided emoji: size 0 bytes") + } + + // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using + emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode) + if err != nil { + return nil, fmt.Errorf("error reading emoji: %s", err) + } + + mastoEmoji, err := p.tc.EmojiToMasto(emoji) + if err != nil { + return nil, fmt.Errorf("error converting emoji to mastotype: %s", err) + } + + if err := p.db.Put(emoji); err != nil { + return nil, fmt.Errorf("database error while processing emoji: %s", err) + } + + return &mastoEmoji, nil +} diff --git a/internal/message/appprocess.go b/internal/message/appprocess.go new file mode 100644 index 000000000..bf56f0874 --- /dev/null +++ b/internal/message/appprocess.go @@ -0,0 +1,59 @@ +package message + +import ( + "github.com/google/uuid" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (p *processor) AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) { + // 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 := p.db.Put(app); err != nil { + return nil, err + } + + // 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 := p.db.Put(oc); err != nil { + return nil, err + } + + mastoApp, err := p.tc.AppToMastoSensitive(app) + if err != nil { + return nil, err + } + + return mastoApp, nil +} diff --git a/internal/message/mediaprocess.go b/internal/message/mediaprocess.go new file mode 100644 index 000000000..19879d6b6 --- /dev/null +++ b/internal/message/mediaprocess.go @@ -0,0 +1,95 @@ +package message + +import ( + "bytes" + "errors" + "fmt" + "io" + "strconv" + "strings" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { + // First check this user/account is permitted to create media + // There's no point continuing otherwise. + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { + return nil, errors.New("not authorized to post new media") + } + + // open the attachment and extract the bytes from it + f, err := form.File.Open() + if err != nil { + return nil, fmt.Errorf("error opening attachment: %s", err) + } + buf := new(bytes.Buffer) + size, err := io.Copy(buf, f) + if err != nil { + return nil, fmt.Errorf("error reading attachment: %s", err) + + } + if size == 0 { + return nil, errors.New("could not read provided attachment: size 0 bytes") + } + + // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using + attachment, err := p.mediaHandler.ProcessLocalAttachment(buf.Bytes(), authed.Account.ID) + if err != nil { + return nil, fmt.Errorf("error reading attachment: %s", err) + } + + // now we need to add extra fields that the attachment processor doesn't know (from the form) + // TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it) + + // first description + attachment.Description = form.Description + + // now parse the focus parameter + // 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 { + return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) + } + xStr := spl[0] + yStr := spl[1] + if xStr == "" || yStr == "" { + return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) + } + fx, err := strconv.ParseFloat(xStr, 32) + if err != nil { + return nil, fmt.Errorf("improperly formatted focus %s: %s", form.Focus, err) + } + if fx > 1 || fx < -1 { + return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) + } + focusx = float32(fx) + fy, err := strconv.ParseFloat(yStr, 32) + if err != nil { + return nil, fmt.Errorf("improperly formatted focus %s: %s", form.Focus, err) + } + if fy > 1 || fy < -1 { + return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) + } + 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 := p.tc.AttachmentToMasto(attachment) + if err != nil { + return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err) + } + + // now we can confidently put the attachment in the database + if err := p.db.Put(attachment); err != nil { + return nil, fmt.Errorf("error storing media attachment in db: %s", err) + } + + return &mastoAttachment, nil +} diff --git a/internal/message/processor.go b/internal/message/processor.go new file mode 100644 index 000000000..d983d6921 --- /dev/null +++ b/internal/message/processor.go @@ -0,0 +1,152 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package message + +import ( + "github.com/sirupsen/logrus" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" +) + +// Processor 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 processor 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 Processor interface { + // ToClientAPI returns a channel for putting in messages that need to go to the gts client API. + ToClientAPI() chan ToClientAPI + // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub). + ToFederator() chan ToFederator + + /* + API-FACING PROCESSING FUNCTIONS + These functions are intended to be called when the API client needs an immediate (ie., synchronous) reply + to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly + formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate + response, pass work to the processor using a channel instead. + */ + + // AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful. + AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) + // AccountGet processes the given request for account information. + AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) + + AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) + + // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK. + StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) + // StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through. + StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) + StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + + MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) + + AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) + + // Start starts the Processor, reading from its channels and passing messages back and forth. + Start() error + // Stop stops the processor cleanly, finishing handling any remaining messages before closing down. + Stop() error +} + +// processor just implements the Processor interface +type processor struct { + // federator pub.FederatingActor + toClientAPI chan ToClientAPI + toFederator chan ToFederator + stop chan interface{} + log *logrus.Logger + config *config.Config + tc typeutils.TypeConverter + oauthServer oauth.Server + mediaHandler media.Handler + db db.DB +} + +// NewProcessor returns a new Processor that uses the given federator and logger +func NewProcessor(config *config.Config, tc typeutils.TypeConverter, oauthServer oauth.Server, mediaHandler media.Handler, db db.DB, log *logrus.Logger) Processor { + return &processor{ + toClientAPI: make(chan ToClientAPI, 100), + toFederator: make(chan ToFederator, 100), + stop: make(chan interface{}), + log: log, + config: config, + tc: tc, + oauthServer: oauthServer, + mediaHandler: mediaHandler, + db: db, + } +} + +func (d *processor) ToClientAPI() chan ToClientAPI { + return d.toClientAPI +} + +func (d *processor) ToFederator() chan ToFederator { + return d.toFederator +} + +// Start starts the Processor, reading from its channels and passing messages back and forth. +func (d *processor) Start() error { + go func() { + DistLoop: + for { + select { + case clientMsg := <-d.toClientAPI: + d.log.Infof("received message TO client API: %+v", clientMsg) + case federatorMsg := <-d.toFederator: + d.log.Infof("received message TO federator: %+v", federatorMsg) + case <-d.stop: + break DistLoop + } + } + }() + return nil +} + +// Stop stops the processor cleanly, finishing handling any remaining messages before closing down. +// TODO: empty message buffer properly before stopping otherwise we'll lose federating messages. +func (d *processor) Stop() error { + close(d.stop) + return nil +} + +// ToClientAPI wraps a message that travels from the processor into the client API +type ToClientAPI struct { + APObjectType gtsmodel.ActivityStreamsObject + APActivityType gtsmodel.ActivityStreamsActivity + Activity interface{} +} + +// ToFederator wraps a message that travels from the processor into the federator +type ToFederator struct { + APObjectType gtsmodel.ActivityStreamsObject + APActivityType gtsmodel.ActivityStreamsActivity + Activity interface{} +} diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go new file mode 100644 index 000000000..c2d3f995c --- /dev/null +++ b/internal/message/processorutil.go @@ -0,0 +1,305 @@ +package message + +import ( + "bytes" + "errors" + "fmt" + "io" + "mime/multipart" + + "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +func (p *processor) processVisibility(form *model.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 = gtsmodel.Visibility(*form.VisibilityAdvanced) + } else if form.Visibility != "" { + gtsBasicVis = p.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 (p *processor) processReplyToID(form *apimodel.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 := p.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 := p.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 := p.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 (p *processor) processMediaIDs(form *apimodel.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 := p.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 (p *processor) processLanguage(form *apimodel.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 (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { + menchies := []string{} + gtsMenchies, err := p.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 := p.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 (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { + tags := []string{} + gtsTags, err := p.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 := p.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 (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { + emojis := []string{} + gtsEmojis, err := p.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 +} + +/* + HELPER FUNCTIONS +*/ + +// TODO: try to combine the below two functions because this is a lot of code repetition. + +// updateAccountAvatar does the dirty work of checking the avatar part of an account update form, +// parsing and checking the image, and doing the necessary updates in the database for this to become +// the account's new avatar image. +func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { + var err error + if int(avatar.Size) > p.config.MediaConfig.MaxImageSize { + err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize) + return nil, err + } + f, err := avatar.Open() + if err != nil { + return nil, fmt.Errorf("could not read provided avatar: %s", err) + } + + // extract the bytes + buf := new(bytes.Buffer) + size, err := io.Copy(buf, f) + if err != nil { + return nil, fmt.Errorf("could not read provided avatar: %s", err) + } + if size == 0 { + return nil, errors.New("could not read provided avatar: size 0 bytes") + } + + // do the setting + avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.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 (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { + var err error + if int(header.Size) > p.config.MediaConfig.MaxImageSize { + err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize) + return nil, err + } + f, err := header.Open() + if err != nil { + return nil, fmt.Errorf("could not read provided header: %s", err) + } + + // extract the bytes + buf := new(bytes.Buffer) + size, err := io.Copy(buf, f) + if err != nil { + return nil, fmt.Errorf("could not read provided header: %s", err) + } + if size == 0 { + return nil, errors.New("could not read provided header: size 0 bytes") + } + + // do the setting + headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.MediaHeader) + if err != nil { + return nil, fmt.Errorf("error processing header: %s", err) + } + + return headerInfo, f.Close() +} diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go new file mode 100644 index 000000000..8c8e5f7c0 --- /dev/null +++ b/internal/message/statusprocess.go @@ -0,0 +1,354 @@ +package message + +import ( + "errors" + "fmt" + "time" + + "github.com/google/uuid" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) { + uris := util.GenerateURIsForAccount(auth.Account.Username, p.config.Protocol, p.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: auth.Account.ID, + ContentWarning: form.SpoilerText, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, + Sensitive: form.Sensitive, + Language: form.Language, + CreatedWithApplicationID: auth.Application.ID, + Text: form.Status, + } + + // check if replyToID is ok + if err := p.processReplyToID(form, auth.Account.ID, newStatus); err != nil { + return nil, err + } + + // check if mediaIDs are ok + if err := p.processMediaIDs(form, auth.Account.ID, newStatus); err != nil { + return nil, err + } + + // check if visibility settings are ok + if err := p.processVisibility(form, auth.Account.Privacy, newStatus); err != nil { + return nil, err + } + + // handle language settings + if err := p.processLanguage(form, auth.Account.Language, newStatus); err != nil { + return nil, err + } + + // handle mentions + if err := p.processMentions(form, auth.Account.ID, newStatus); err != nil { + return nil, err + } + + if err := p.processTags(form, auth.Account.ID, newStatus); err != nil { + return nil, err + } + + if err := p.processEmojis(form, auth.Account.ID, newStatus); err != nil { + return nil, err + } + + // put the new status in the database, generating an ID for it in the process + if err := p.db.Put(newStatus); err != nil { + return nil, err + } + + // 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 := p.db.UpdateByID(a.ID, a); err != nil { + return nil, err + } + } + + // return the frontend representation of the new status to the submitter + mastoStatus, err := p.tc.StatusToMasto(newStatus, auth.Account, auth.Account, nil, newStatus.GTSReplyToAccount, nil) + if err != nil { + return nil, err + } + return mastoStatus, nil +} + +func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { + l := p.log.WithField("func", "StatusDelete") + l.Tracef("going to search for target status %s", targetStatusID) + targetStatus := >smodel.Status{} + if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { + return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err) + } + + if targetStatus.AccountID != authed.Account.ID { + return nil, errors.New("status doesn't belong to requesting account") + } + + l.Trace("going to get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) + } + + var boostOfStatus *gtsmodel.Status + if targetStatus.BoostOfID != "" { + boostOfStatus = >smodel.Status{} + if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { + return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) + } + } + + mastoStatus, err := p.tc.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + if err != nil { + return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) + } + + if err := p.db.DeleteByID(targetStatus.ID, targetStatus); err != nil { + return nil, fmt.Errorf("error deleting status from the database: %s", err) + } + + return mastoStatus, nil +} + +func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { + l := p.log.WithField("func", "StatusFave") + l.Tracef("going to search for target status %s", targetStatusID) + targetStatus := >smodel.Status{} + if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { + return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err) + } + + l.Tracef("going to search for target account %s", targetStatus.AccountID) + targetAccount := >smodel.Account{} + if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { + return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) + } + + l.Trace("going to get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) + } + + l.Trace("going to see if status is visible") + visible, err := p.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 { + return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) + } + + if !visible { + return nil, errors.New("status is not visible") + } + + // is the status faveable? + if !targetStatus.VisibilityAdvanced.Likeable { + return nil, errors.New("status is not faveable") + } + + // it's visible! it's faveable! so let's fave the FUCK out of it + _, err = p.db.FaveStatus(targetStatus, authed.Account.ID) + if err != nil { + return nil, fmt.Errorf("error faveing status: %s", err) + } + + var boostOfStatus *gtsmodel.Status + if targetStatus.BoostOfID != "" { + boostOfStatus = >smodel.Status{} + if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { + return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) + } + } + + mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + if err != nil { + return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) + } + + return mastoStatus, nil +} + +func (p *processor) StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) { + l := p.log.WithField("func", "StatusFavedBy") + + l.Tracef("going to search for target status %s", targetStatusID) + targetStatus := >smodel.Status{} + if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { + return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err) + } + + l.Tracef("going to search for target account %s", targetStatus.AccountID) + targetAccount := >smodel.Account{} + if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { + return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) + } + + l.Trace("going to get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) + } + + l.Trace("going to see if status is visible") + visible, err := p.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 { + return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) + } + + if !visible { + return nil, errors.New("status is not visible") + } + + // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff + favingAccounts, err := p.db.WhoFavedStatus(targetStatus) + if err != nil { + return nil, fmt.Errorf("error seeing who faved status: %s", err) + } + + // 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 := p.db.Blocked(authed.Account.ID, acc.ID) + if err != nil { + return nil, fmt.Errorf("error checking blocks: %s", err) + } + 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 := []*apimodel.Account{} + for _, acc := range filteredAccounts { + mastoAccount, err := p.tc.AccountToMastoPublic(acc) + if err != nil { + return nil, fmt.Errorf("error converting account to api model: %s", err) + } + mastoAccounts = append(mastoAccounts, mastoAccount) + } + + return mastoAccounts, nil +} + +func (p *processor) StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { + l := p.log.WithField("func", "StatusGet") + + l.Tracef("going to search for target status %s", targetStatusID) + targetStatus := >smodel.Status{} + if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { + return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err) + } + + l.Tracef("going to search for target account %s", targetStatus.AccountID) + targetAccount := >smodel.Account{} + if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { + return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) + } + + l.Trace("going to get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) + } + + l.Trace("going to see if status is visible") + visible, err := p.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 { + return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) + } + + if !visible { + return nil, errors.New("status is not visible") + } + + var boostOfStatus *gtsmodel.Status + if targetStatus.BoostOfID != "" { + boostOfStatus = >smodel.Status{} + if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { + return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) + } + } + + mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + if err != nil { + return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) + } + + return mastoStatus, nil + +} + +func (p *processor) StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { + l := p.log.WithField("func", "StatusUnfave") + l.Tracef("going to search for target status %s", targetStatusID) + targetStatus := >smodel.Status{} + if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { + return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err) + } + + l.Tracef("going to search for target account %s", targetStatus.AccountID) + targetAccount := >smodel.Account{} + if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { + return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) + } + + l.Trace("going to get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) + } + + l.Trace("going to see if status is visible") + visible, err := p.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 { + return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) + } + + if !visible { + return nil, errors.New("status is not visible") + } + + // is the status faveable? + if !targetStatus.VisibilityAdvanced.Likeable { + return nil, errors.New("status is not faveable") + } + + // it's visible! it's faveable! so let's unfave the FUCK out of it + _, err = p.db.UnfaveStatus(targetStatus, authed.Account.ID) + if err != nil { + return nil, fmt.Errorf("error unfaveing status: %s", err) + } + + var boostOfStatus *gtsmodel.Status + if targetStatus.BoostOfID != "" { + boostOfStatus = >smodel.Status{} + if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { + return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) + } + } + + mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + if err != nil { + return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) + } + + return mastoStatus, nil +} diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index 39d64ccab..b77163e48 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -62,7 +62,7 @@ func (suite *PgClientStoreTestSuite) SetupTest() { Database: "postgres", ApplicationName: "gotosocial", } - db, err := db.New(context.Background(), c, log) + db, err := db.NewPostgresService(context.Background(), c, log) if err != nil { logrus.Panicf("error creating database connection: %s", err) } diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 7bd4f8e7c..ca749aece 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -26,7 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/errors" "github.com/superseriousbusiness/oauth2/v4/manage" @@ -66,18 +66,18 @@ type s struct { log *logrus.Logger } -// Authed wraps an authorized token, application, user, and account. +// Auth wraps an authorized token, application, user, and account. // It is used in the functions GetAuthed and MustAuth. // Because the user might *not* be authed, any of the fields in this struct // might be nil, so make sure to check that when you're using this struct anywhere. -type Authed struct { +type Auth struct { Token oauth2.TokenInfo Application *gtsmodel.Application User *gtsmodel.User Account *gtsmodel.Account } -// GetAuthed is a convenience function for returning an Authed struct from a gin context. +// Authed is a convenience function for returning an Authed struct from a gin context. // In essence, it tries to extract a token, application, user, and account from the context, // and then sets them on a struct for convenience. // @@ -86,9 +86,10 @@ type Authed struct { // If *ALL* are not present, then nil and an error will be returned. // // If something goes wrong during parsing, then nil and an error will be returned (consider this not authed). -func GetAuthed(c *gin.Context) (*Authed, error) { +// Authed is like GetAuthed, but will fail if one of the requirements is not met. +func Authed(c *gin.Context, requireToken bool, requireApp bool, requireUser bool, requireAccount bool) (*Auth, error) { ctx := c.Copy() - a := &Authed{} + a := &Auth{} var i interface{} var ok bool @@ -128,19 +129,6 @@ func GetAuthed(c *gin.Context) (*Authed, error) { a.Account = parsed } - if a.Token == nil && a.Application == nil && a.User == nil && a.Account == nil { - return nil, errors.New("not authorized") - } - - return a, nil -} - -// MustAuth is like GetAuthed, but will fail if one of the requirements is not met. -func MustAuth(c *gin.Context, requireToken bool, requireApp bool, requireUser bool, requireAccount bool) (*Authed, error) { - a, err := GetAuthed(c) - if err != nil { - return nil, err - } if requireToken && a.Token == nil { return nil, errors.New("token not supplied") } diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 6593de5b7..a691a21ad 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -1,77 +1,71 @@ package typeutils import ( - "errors" - "time" - "github.com/go-fed/activity/streams/vocab" - "github.com/google/uuid" - "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) { - + // acct := >smodel.Account{ + // URI: "", + // URL: "", + // ID: "", + // Username: "", + // Domain: "", + // AvatarMediaAttachmentID: "", + // AvatarRemoteURL: "", + // HeaderMediaAttachmentID: "", + // HeaderRemoteURL: "", + // DisplayName: "", + // Fields: nil, + // Note: "", + // Memorial: false, + // MovedToAccountID: "", + // CreatedAt: time.Time{}, + // UpdatedAt: time.Time{}, + // Bot: false, + // Reason: "", + // Locked: false, + // Discoverable: true, + // Privacy: "", + // Sensitive: false, + // Language: "", + // LastWebfingeredAt: time.Now(), + // InboxURI: "", + // OutboxURI: "", + // FollowingURI: "", + // FollowersURI: "", + // FeaturedCollectionURI: "", + // ActorType: gtsmodel.ActivityStreamsPerson, + // AlsoKnownAs: "", + // PrivateKey: nil, + // PublicKey: nil, + // PublicKeyURI: "", + // SensitizedAt: time.Time{}, + // SilencedAt: time.Time{}, + // SuspendedAt: time.Time{}, + // HideCollections: false, + // SuspensionOrigin: "", + // } - acct := >smodel.Account{ - URI: "", - URL: "", - ID: "", - Username: "", - Domain: "", - AvatarMediaAttachmentID: "", - AvatarRemoteURL: "", - HeaderMediaAttachmentID: "", - HeaderRemoteURL: "", - DisplayName: "", - Fields: nil, - Note: "", - Memorial: false, - MovedToAccountID: "", - CreatedAt: time.Time{}, - UpdatedAt: time.Time{}, - Bot: false, - Reason: "", - Locked: false, - Discoverable: true, - Privacy: "", - Sensitive: false, - Language: "", - LastWebfingeredAt: time.Now(), - InboxURI: "", - OutboxURI: "", - FollowingURI: "", - FollowersURI: "", - FeaturedCollectionURI: "", - ActorType: gtsmodel.ActivityStreamsPerson, - AlsoKnownAs: "", - PrivateKey: nil, - PublicKey: nil, - PublicKeyURI: "", - SensitizedAt: time.Time{}, - SilencedAt: time.Time{}, - SuspendedAt: time.Time{}, - HideCollections: false, - SuspensionOrigin: "", - } + // // ID + // // Generate a new uuid for our particular database. + // // This is distinct from the AP ID of the person. + // id := uuid.NewString() + // acct.ID = id - // ID - // Generate a new uuid for our particular database. - // This is distinct from the AP ID of the person. - id := uuid.NewString() - acct.ID = id + // // Username + // // We need this one so bail if it's not set. + // username := person.GetActivityStreamsPreferredUsername() + // if username == nil || username.GetXMLSchemaString() == "" { + // return nil, errors.New("preferredusername was empty") + // } + // acct.Username = username.GetXMLSchemaString() - // Username - // We need this one so bail if it's not set. - username := person.GetActivityStreamsPreferredUsername() - if username == nil || username.GetXMLSchemaString() == "" { - return nil, errors.New("preferredusername was empty") - } - acct.Username = username.GetXMLSchemaString() + // // Domain + // // We need this one as well + // acct.Domain = domain - // Domain - // We need this one as well - acct.Domain = domain - - return acct, nil + return nil, nil } diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index c44e0d71c..af941b83a 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -20,13 +20,13 @@ package typeutils import ( "github.com/go-fed/activity/streams/vocab" + "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" ) -// TypeConverter is an interface for the common action of converting between mastotypes (frontend, serializable) models, +// TypeConverter is an interface for the common action of converting between apimodule (frontend, serializable) models, // internal gts models used in the database, and activitypub models used in federation. // // It requires access to the database because many of the conversions require pulling out database entries and counting them etc. @@ -39,47 +39,47 @@ type TypeConverter interface { // AccountToMastoSensitive takes a db model account as a param, and returns a populated mastotype account, or an error // if something goes wrong. The returned account should be ready to serialize on an API level, and may have sensitive fields, // so serve it only to an authorized user who should have permission to see it. - AccountToMastoSensitive(account *gtsmodel.Account) (*mastotypes.Account, error) + AccountToMastoSensitive(account *gtsmodel.Account) (*model.Account, error) // AccountToMastoPublic takes a db model account as a param, and returns a populated mastotype account, or an error // if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. // In other words, this is the public record that the server has of an account. - AccountToMastoPublic(account *gtsmodel.Account) (*mastotypes.Account, error) + AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error) // AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error // if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields // (such as client id and client secret), so serve it only to an authorized user who should have permission to see it. - AppToMastoSensitive(application *gtsmodel.Application) (*mastotypes.Application, error) + AppToMastoSensitive(application *gtsmodel.Application) (*model.Application, error) // AppToMastoPublic takes a db model application as a param, and returns a populated mastotype application, or an error // if something goes wrong. The returned application should be ready to serialize on an API level, and has sensitive // fields sanitized so that it can be served to non-authorized accounts without revealing any private information. - AppToMastoPublic(application *gtsmodel.Application) (*mastotypes.Application, error) + AppToMastoPublic(application *gtsmodel.Application) (*model.Application, error) // AttachmentToMasto converts a gts model media attacahment into its mastodon representation for serialization on the API. - AttachmentToMasto(attachment *gtsmodel.MediaAttachment) (mastotypes.Attachment, error) + AttachmentToMasto(attachment *gtsmodel.MediaAttachment) (model.Attachment, error) // MentionToMasto converts a gts model mention into its mastodon (frontend) representation for serialization on the API. - MentionToMasto(m *gtsmodel.Mention) (mastotypes.Mention, error) + MentionToMasto(m *gtsmodel.Mention) (model.Mention, error) // EmojiToMasto converts a gts model emoji into its mastodon (frontend) representation for serialization on the API. - EmojiToMasto(e *gtsmodel.Emoji) (mastotypes.Emoji, error) + EmojiToMasto(e *gtsmodel.Emoji) (model.Emoji, error) // TagToMasto converts a gts model tag into its mastodon (frontend) representation for serialization on the API. - TagToMasto(t *gtsmodel.Tag) (mastotypes.Tag, error) + TagToMasto(t *gtsmodel.Tag) (model.Tag, error) // StatusToMasto converts a gts model status into its mastodon (frontend) representation for serialization on the API. - StatusToMasto(s *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, boostOfAccount *gtsmodel.Account, replyToAccount *gtsmodel.Account, reblogOfStatus *gtsmodel.Status) (*mastotypes.Status, error) + StatusToMasto(s *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, boostOfAccount *gtsmodel.Account, replyToAccount *gtsmodel.Account, reblogOfStatus *gtsmodel.Status) (*model.Status, error) // VisToMasto converts a gts visibility into its mastodon equivalent - VisToMasto(m gtsmodel.Visibility) mastotypes.Visibility + VisToMasto(m gtsmodel.Visibility) model.Visibility /* FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL */ // MastoVisToVis converts a mastodon visibility into its gts equivalent. - MastoVisToVis(m mastotypes.Visibility) gtsmodel.Visibility + MastoVisToVis(m model.Visibility) gtsmodel.Visibility /* ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL diff --git a/internal/typeutils/frontendtointernal.go b/internal/typeutils/frontendtointernal.go index 4ac6a74cf..6bb45d61b 100644 --- a/internal/typeutils/frontendtointernal.go +++ b/internal/typeutils/frontendtointernal.go @@ -19,20 +19,20 @@ package typeutils import ( - "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" ) // MastoVisToVis converts a mastodon visibility into its gts equivalent. -func (c *converter) MastoVisToVis(m mastotypes.Visibility) gtsmodel.Visibility { +func (c *converter) MastoVisToVis(m model.Visibility) gtsmodel.Visibility { switch m { - case mastotypes.VisibilityPublic: + case model.VisibilityPublic: return gtsmodel.VisibilityPublic - case mastotypes.VisibilityUnlisted: + case model.VisibilityUnlisted: return gtsmodel.VisibilityUnlocked - case mastotypes.VisibilityPrivate: + case model.VisibilityPrivate: return gtsmodel.VisibilityFollowersOnly - case mastotypes.VisibilityDirect: + case model.VisibilityDirect: return gtsmodel.VisibilityDirect } return "" diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index bb1531eb2..33697917c 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -25,7 +25,7 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) // Converts a gts model account into an Activity Streams person type, following diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index d606ad945..b6a7717d2 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -29,7 +29,7 @@ import ( "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/typeutils" "github.com/superseriousbusiness/gotosocial/testrig" ) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 60e7fa3fa..6ec2e0509 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -23,11 +23,11 @@ import ( "time" "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/api/model" ) -func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*mastotypes.Account, error) { +func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*model.Account, error) { // we can build this sensitive account easily by first getting the public account.... mastoAccount, err := c.AccountToMastoPublic(a) if err != nil { @@ -48,7 +48,7 @@ func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*mastotypes.Ac frc = len(fr) } - mastoAccount.Source = &mastotypes.Source{ + mastoAccount.Source = &model.Source{ Privacy: c.VisToMasto(a.Privacy), Sensitive: a.Sensitive, Language: a.Language, @@ -60,7 +60,7 @@ func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*mastotypes.Ac return mastoAccount, nil } -func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Account, error) { +func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, error) { // count followers followers := []gtsmodel.Follow{} if err := c.db.GetFollowersByAccountID(a.ID, &followers); err != nil { @@ -129,9 +129,9 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Accou headerURLStatic := header.Thumbnail.URL // get the fields set on this account - fields := []mastotypes.Field{} + fields := []model.Field{} for _, f := range a.Fields { - mField := mastotypes.Field{ + mField := model.Field{ Name: f.Name, Value: f.Value, } @@ -150,7 +150,7 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Accou acct = a.Username } - return &mastotypes.Account{ + return &model.Account{ ID: a.ID, Username: a.Username, Acct: acct, @@ -173,8 +173,8 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Accou }, nil } -func (c *converter) AppToMastoSensitive(a *gtsmodel.Application) (*mastotypes.Application, error) { - return &mastotypes.Application{ +func (c *converter) AppToMastoSensitive(a *gtsmodel.Application) (*model.Application, error) { + return &model.Application{ ID: a.ID, Name: a.Name, Website: a.Website, @@ -185,35 +185,35 @@ func (c *converter) AppToMastoSensitive(a *gtsmodel.Application) (*mastotypes.Ap }, nil } -func (c *converter) AppToMastoPublic(a *gtsmodel.Application) (*mastotypes.Application, error) { - return &mastotypes.Application{ +func (c *converter) AppToMastoPublic(a *gtsmodel.Application) (*model.Application, error) { + return &model.Application{ Name: a.Name, Website: a.Website, }, nil } -func (c *converter) AttachmentToMasto(a *gtsmodel.MediaAttachment) (mastotypes.Attachment, error) { - return mastotypes.Attachment{ +func (c *converter) AttachmentToMasto(a *gtsmodel.MediaAttachment) (model.Attachment, error) { + return model.Attachment{ ID: a.ID, Type: string(a.Type), URL: a.URL, PreviewURL: a.Thumbnail.URL, RemoteURL: a.RemoteURL, PreviewRemoteURL: a.Thumbnail.RemoteURL, - Meta: mastotypes.MediaMeta{ - Original: mastotypes.MediaDimensions{ + Meta: model.MediaMeta{ + Original: model.MediaDimensions{ Width: a.FileMeta.Original.Width, Height: a.FileMeta.Original.Height, Size: fmt.Sprintf("%dx%d", a.FileMeta.Original.Width, a.FileMeta.Original.Height), Aspect: float32(a.FileMeta.Original.Aspect), }, - Small: mastotypes.MediaDimensions{ + Small: model.MediaDimensions{ Width: a.FileMeta.Small.Width, Height: a.FileMeta.Small.Height, Size: fmt.Sprintf("%dx%d", a.FileMeta.Small.Width, a.FileMeta.Small.Height), Aspect: float32(a.FileMeta.Small.Aspect), }, - Focus: mastotypes.MediaFocus{ + Focus: model.MediaFocus{ X: a.FileMeta.Focus.X, Y: a.FileMeta.Focus.Y, }, @@ -223,10 +223,10 @@ func (c *converter) AttachmentToMasto(a *gtsmodel.MediaAttachment) (mastotypes.A }, nil } -func (c *converter) MentionToMasto(m *gtsmodel.Mention) (mastotypes.Mention, error) { +func (c *converter) MentionToMasto(m *gtsmodel.Mention) (model.Mention, error) { target := >smodel.Account{} if err := c.db.GetByID(m.TargetAccountID, target); err != nil { - return mastotypes.Mention{}, err + return model.Mention{}, err } var local bool @@ -241,7 +241,7 @@ func (c *converter) MentionToMasto(m *gtsmodel.Mention) (mastotypes.Mention, err acct = fmt.Sprintf("@%s@%s", target.Username, target.Domain) } - return mastotypes.Mention{ + return model.Mention{ ID: target.ID, Username: target.Username, URL: target.URL, @@ -249,8 +249,8 @@ func (c *converter) MentionToMasto(m *gtsmodel.Mention) (mastotypes.Mention, err }, nil } -func (c *converter) EmojiToMasto(e *gtsmodel.Emoji) (mastotypes.Emoji, error) { - return mastotypes.Emoji{ +func (c *converter) EmojiToMasto(e *gtsmodel.Emoji) (model.Emoji, error) { + return model.Emoji{ Shortcode: e.Shortcode, URL: e.ImageURL, StaticURL: e.ImageStaticURL, @@ -259,10 +259,10 @@ func (c *converter) EmojiToMasto(e *gtsmodel.Emoji) (mastotypes.Emoji, error) { }, nil } -func (c *converter) TagToMasto(t *gtsmodel.Tag) (mastotypes.Tag, error) { +func (c *converter) TagToMasto(t *gtsmodel.Tag) (model.Tag, error) { tagURL := fmt.Sprintf("%s://%s/tags/%s", c.config.Protocol, c.config.Host, t.Name) - return mastotypes.Tag{ + return model.Tag{ Name: t.Name, URL: tagURL, // we don't serve URLs with collections of tagged statuses (FOR NOW) so this is purely for mastodon compatibility ¯\_(ツ)_/¯ }, nil @@ -274,7 +274,7 @@ func (c *converter) StatusToMasto( requestingAccount *gtsmodel.Account, boostOfAccount *gtsmodel.Account, replyToAccount *gtsmodel.Account, - reblogOfStatus *gtsmodel.Status) (*mastotypes.Status, error) { + reblogOfStatus *gtsmodel.Status) (*model.Status, error) { repliesCount, err := c.db.GetReplyCountForStatus(s) if err != nil { @@ -326,9 +326,9 @@ func (c *converter) StatusToMasto( } } - var mastoRebloggedStatus *mastotypes.Status // TODO + var mastoRebloggedStatus *model.Status // TODO - var mastoApplication *mastotypes.Application + var mastoApplication *model.Application if s.CreatedWithApplicationID != "" { gtsApplication := >smodel.Application{} if err := c.db.GetByID(s.CreatedWithApplicationID, gtsApplication); err != nil { @@ -345,7 +345,7 @@ func (c *converter) StatusToMasto( return nil, fmt.Errorf("error parsing account of status author: %s", err) } - mastoAttachments := []mastotypes.Attachment{} + mastoAttachments := []model.Attachment{} // the status might already have some gts attachments on it if it's not been pulled directly from the database // if so, we can directly convert the gts attachments into masto ones if s.GTSMediaAttachments != nil { @@ -372,7 +372,7 @@ func (c *converter) StatusToMasto( } } - mastoMentions := []mastotypes.Mention{} + mastoMentions := []model.Mention{} // the status might already have some gts mentions on it if it's not been pulled directly from the database // if so, we can directly convert the gts mentions into masto ones if s.GTSMentions != nil { @@ -399,7 +399,7 @@ func (c *converter) StatusToMasto( } } - mastoTags := []mastotypes.Tag{} + mastoTags := []model.Tag{} // the status might already have some gts tags on it if it's not been pulled directly from the database // if so, we can directly convert the gts tags into masto ones if s.GTSTags != nil { @@ -426,7 +426,7 @@ func (c *converter) StatusToMasto( } } - mastoEmojis := []mastotypes.Emoji{} + mastoEmojis := []model.Emoji{} // the status might already have some gts emojis on it if it's not been pulled directly from the database // if so, we can directly convert the gts emojis into masto ones if s.GTSEmojis != nil { @@ -453,10 +453,10 @@ func (c *converter) StatusToMasto( } } - var mastoCard *mastotypes.Card - var mastoPoll *mastotypes.Poll + var mastoCard *model.Card + var mastoPoll *model.Poll - return &mastotypes.Status{ + return &model.Status{ ID: s.ID, CreatedAt: s.CreatedAt.Format(time.RFC3339), InReplyToID: s.InReplyToID, @@ -490,16 +490,16 @@ func (c *converter) StatusToMasto( } // VisToMasto converts a gts visibility into its mastodon equivalent -func (c *converter) VisToMasto(m gtsmodel.Visibility) mastotypes.Visibility { +func (c *converter) VisToMasto(m gtsmodel.Visibility) model.Visibility { switch m { case gtsmodel.VisibilityPublic: - return mastotypes.VisibilityPublic + return model.VisibilityPublic case gtsmodel.VisibilityUnlocked: - return mastotypes.VisibilityUnlisted + return model.VisibilityUnlisted case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: - return mastotypes.VisibilityPrivate + return model.VisibilityPrivate case gtsmodel.VisibilityDirect: - return mastotypes.VisibilityDirect + return model.VisibilityDirect } return "" } diff --git a/testrig/actions.go b/testrig/actions.go index 07ffe7184..87b3cae7d 100644 --- a/testrig/actions.go +++ b/testrig/actions.go @@ -30,16 +30,15 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/action" - "github.com/superseriousbusiness/gotosocial/internal/apimodule" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/account" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/admin" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/app" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/auth" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver" - mediaModule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/security" - "github.com/superseriousbusiness/gotosocial/internal/apimodule/status" - "github.com/superseriousbusiness/gotosocial/internal/cache" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" + "github.com/superseriousbusiness/gotosocial/internal/api/client/app" + "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" + "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" + mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" + "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/security" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gotosocial" @@ -51,11 +50,9 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr dbService := NewTestDB() router := NewTestRouter() storageBackend := NewTestStorage() - mediaHandler := NewTestMediaHandler(dbService, storageBackend) - oauthServer := NewTestOauthServer(dbService) - distributor := NewTestDistributor() - if err := distributor.Start(); err != nil { - return fmt.Errorf("error starting distributor: %s", err) + processor := NewTestProcessor(dbService, storageBackend) + if err := processor.Start(); err != nil { + return fmt.Errorf("error starting processor: %s", err) } typeConverter := NewTestTypeConverter(dbService) transportController := NewTestTransportController(NewMockHTTPClient(func(req *http.Request) (*http.Response, error) { @@ -65,22 +62,22 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr Body: r, }, nil })) - federator := federation.NewFederator(dbService, transportController, c, log, distributor, typeConverter) + federator := federation.NewFederator(dbService, transportController, c, log, processor, typeConverter) StandardDBSetup(dbService) StandardStorageSetup(storageBackend, "./testrig/media") // build client api modules - authModule := auth.New(oauthServer, dbService, log) - accountModule := account.New(c, dbService, oauthServer, mediaHandler, typeConverter, log) - appsModule := app.New(oauthServer, dbService, typeConverter, log) - mm := mediaModule.New(dbService, mediaHandler, typeConverter, c, log) - fileServerModule := fileserver.New(c, dbService, storageBackend, log) - adminModule := admin.New(c, dbService, mediaHandler, typeConverter, log) - statusModule := status.New(c, dbService, mediaHandler, typeConverter, distributor, log) + authModule := auth.New(c, processor, log) + accountModule := account.New(c, processor, log) + appsModule := app.New(c, processor, log) + mm := mediaModule.New(c, processor, log) + fileServerModule := fileserver.New(c, processor, log) + adminModule := admin.New(c, processor, log) + statusModule := status.New(c, processor, log) securityModule := security.New(c, log) - apiModules := []apimodule.ClientAPIModule{ + apis := []api.ClientModule{ // modules with middleware go first securityModule, authModule, @@ -94,20 +91,13 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr statusModule, } - for _, m := range apiModules { + for _, m := range apis { if err := m.Route(router); err != nil { return fmt.Errorf("routing error: %s", err) } - if err := m.CreateTables(dbService); err != nil { - return fmt.Errorf("table creation error: %s", err) - } } - // if err := dbService.CreateInstanceAccount(); err != nil { - // return fmt.Errorf("error creating instance account: %s", err) - // } - - gts, err := gotosocial.New(dbService, &cache.MockCache{}, router, federator, c) + gts, err := gotosocial.New(dbService, router, federator, c) if err != nil { return fmt.Errorf("error creating gotosocial service: %s", err) } diff --git a/testrig/db.go b/testrig/db.go index 5974eae69..4d22ab3c8 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -23,7 +23,7 @@ import ( "github.com/sirupsen/logrus" "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" ) @@ -54,7 +54,7 @@ func NewTestDB() db.DB { config := NewTestConfig() l := logrus.New() l.SetLevel(logrus.TraceLevel) - testDB, err := db.New(context.Background(), config, l) + testDB, err := db.NewPostgresService(context.Background(), config, l) if err != nil { panic(err) } diff --git a/testrig/media/test-jpeg.jpg b/testrig/media/test-jpeg.jpg new file mode 100644 index 000000000..a9ab154d4 Binary files /dev/null and b/testrig/media/test-jpeg.jpg differ diff --git a/testrig/distributor.go b/testrig/processor.go similarity index 61% rename from testrig/distributor.go rename to testrig/processor.go index a7206e5ea..eea66fd0c 100644 --- a/testrig/distributor.go +++ b/testrig/processor.go @@ -18,9 +18,13 @@ package testrig -import "github.com/superseriousbusiness/gotosocial/internal/distributor" +import ( + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/message" + "github.com/superseriousbusiness/gotosocial/internal/storage" +) -// NewTestDistributor returns a Distributor suitable for testing purposes -func NewTestDistributor() distributor.Distributor { - return distributor.New(NewTestLog()) +// NewTestProcessor returns a Processor suitable for testing purposes +func NewTestProcessor(db db.DB, storage storage.Storage) message.Processor { + return message.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), NewTestOauthServer(db), NewTestMediaHandler(db, storage), db, NewTestLog()) } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index ef629cdb3..1ce1b6a90 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -34,7 +34,7 @@ import ( "github.com/go-fed/activity/pub" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" )