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"
)