linting + organizing

This commit is contained in:
tsmethurst 2021-04-20 18:14:23 +02:00
parent 32c5fd987a
commit dafc3b5b92
60 changed files with 746 additions and 390 deletions

View file

@ -37,25 +37,31 @@ import (
) )
const ( const (
idKey = "id" // IDKey is the key to use for retrieving account ID in requests
basePath = "/api/v1/accounts" IDKey = "id"
basePathWithID = basePath + "/:" + idKey // BasePath is the base API path for this module
verifyPath = basePath + "/verify_credentials" BasePath = "/api/v1/accounts"
updateCredentialsPath = basePath + "/update_credentials" // BasePathWithID is the base path for this module with the ID key
BasePathWithID = BasePath + "/:" + IDKey
// VerifyPath is for verifying account credentials
VerifyPath = BasePath + "/verify_credentials"
// UpdateCredentialsPath is for updating account credentials
UpdateCredentialsPath = BasePath + "/update_credentials"
) )
type accountModule struct { // Module implements the ClientAPIModule interface for account-related actions
type Module struct {
config *config.Config config *config.Config
db db.DB db db.DB
oauthServer oauth.Server oauthServer oauth.Server
mediaHandler media.MediaHandler mediaHandler media.Handler
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
log *logrus.Logger log *logrus.Logger
} }
// New returns a new account module // New returns a new account module
func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule { func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.Handler, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule {
return &accountModule{ return &Module{
config: config, config: config,
db: db, db: db,
oauthServer: oauthServer, oauthServer: oauthServer,
@ -66,14 +72,15 @@ func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler
} }
// Route attaches all routes from this module to the given router // Route attaches all routes from this module to the given router
func (m *accountModule) Route(r router.Router) error { func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodPost, basePath, m.accountCreatePOSTHandler) r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler)
r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler) r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
r.AttachHandler(http.MethodPatch, basePathWithID, m.muxHandler) r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
return nil return nil
} }
func (m *accountModule) CreateTables(db db.DB) error { // CreateTables creates the required tables for this module in the given database
func (m *Module) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.User{}, &gtsmodel.User{},
&gtsmodel.Account{}, &gtsmodel.Account{},
@ -93,18 +100,18 @@ func (m *accountModule) CreateTables(db db.DB) error {
return nil return nil
} }
func (m *accountModule) muxHandler(c *gin.Context) { func (m *Module) muxHandler(c *gin.Context) {
ru := c.Request.RequestURI ru := c.Request.RequestURI
switch c.Request.Method { switch c.Request.Method {
case http.MethodGet: case http.MethodGet:
if strings.HasPrefix(ru, verifyPath) { if strings.HasPrefix(ru, VerifyPath) {
m.accountVerifyGETHandler(c) m.AccountVerifyGETHandler(c)
} else { } else {
m.accountGETHandler(c) m.AccountGETHandler(c)
} }
case http.MethodPatch: case http.MethodPatch:
if strings.HasPrefix(ru, updateCredentialsPath) { if strings.HasPrefix(ru, UpdateCredentialsPath) {
m.accountUpdateCredentialsPATCHHandler(c) m.AccountUpdateCredentialsPATCHHandler(c)
} }
} }
} }

View file

@ -34,10 +34,10 @@ import (
"github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4"
) )
// accountCreatePOSTHandler handles create account requests, validates them, // AccountCreatePOSTHandler handles create account requests, validates them,
// and puts them in the database if they're valid. // and puts them in the database if they're valid.
// It should be served as a POST at /api/v1/accounts // It should be served as a POST at /api/v1/accounts
func (m *accountModule) accountCreatePOSTHandler(c *gin.Context) { func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "accountCreatePOSTHandler") l := m.log.WithField("func", "accountCreatePOSTHandler")
authed, err := oauth.MustAuth(c, true, true, false, false) authed, err := oauth.MustAuth(c, true, true, false, false)
if err != nil { if err != nil {
@ -83,7 +83,7 @@ func (m *accountModule) accountCreatePOSTHandler(c *gin.Context) {
// accountCreate does the dirty work of making an account and user in the database. // 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 // 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/ // spec here: https://docs.joinmastodon.org/methods/accounts/
func (m *accountModule) accountCreate(form *mastotypes.AccountCreateRequest, signUpIP net.IP, token oauth2.TokenInfo, app *gtsmodel.Application) (*mastotypes.Token, error) { 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") l := m.log.WithField("func", "accountCreate")
// don't store a reason if we don't require one // don't store a reason if we don't require one

View file

@ -26,12 +26,12 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
) )
// accountGetHandler serves the account information held by the server in response to a GET // AccountGETHandler serves the account information held by the server in response to a GET
// request. It should be served as a GET at /api/v1/accounts/:id. // request. It should be served as a GET at /api/v1/accounts/:id.
// //
// See: https://docs.joinmastodon.org/methods/accounts/ // See: https://docs.joinmastodon.org/methods/accounts/
func (m *accountModule) accountGETHandler(c *gin.Context) { func (m *Module) AccountGETHandler(c *gin.Context) {
targetAcctID := c.Param(idKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
return return

View file

@ -34,7 +34,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
// accountUpdateCredentialsPATCHHandler allows a user to modify their account/profile settings. // AccountUpdateCredentialsPATCHHandler allows a user to modify their account/profile settings.
// It should be served as a PATCH at /api/v1/accounts/update_credentials // 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 // TODO: this can be optimized massively by building up a picture of what we want the new account
@ -42,7 +42,7 @@ import (
// which is not gonna make the database very happy when lots of requests are going through. // 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. // 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. // Otherwise we risk doing a partial update and that's gonna cause probllleeemmmsss.
func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
l := m.log.WithField("func", "accountUpdateCredentialsPATCHHandler") l := m.log.WithField("func", "accountUpdateCredentialsPATCHHandler")
authed, err := oauth.MustAuth(c, true, false, false, true) authed, err := oauth.MustAuth(c, true, false, false, true)
if err != nil { if err != nil {
@ -196,7 +196,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) {
// UpdateAccountAvatar does the dirty work of checking the avatar part of an account update form, // 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 // parsing and checking the image, and doing the necessary updates in the database for this to become
// the account's new avatar image. // the account's new avatar image.
func (m *accountModule) UpdateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { func (m *Module) UpdateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
var err error var err error
if int(avatar.Size) > m.config.MediaConfig.MaxImageSize { 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) err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, m.config.MediaConfig.MaxImageSize)
@ -229,7 +229,7 @@ func (m *accountModule) UpdateAccountAvatar(avatar *multipart.FileHeader, accoun
// UpdateAccountHeader does the dirty work of checking the header part of an account update form, // 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 // parsing and checking the image, and doing the necessary updates in the database for this to become
// the account's new header image. // the account's new header image.
func (m *accountModule) UpdateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { func (m *Module) UpdateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
var err error var err error
if int(header.Size) > m.config.MediaConfig.MaxImageSize { 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) err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, m.config.MediaConfig.MaxImageSize)

View file

@ -25,10 +25,10 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// accountVerifyGETHandler serves a user's account details to them IF they reached this // AccountVerifyGETHandler serves a user's account details to them IF they reached this
// handler while in possession of a valid token, according to the oauth middleware. // handler while in possession of a valid token, according to the oauth middleware.
// It should be served as a GET at /api/v1/accounts/verify_credentials // It should be served as a GET at /api/v1/accounts/verify_credentials
func (m *accountModule) accountVerifyGETHandler(c *gin.Context) { func (m *Module) AccountVerifyGETHandler(c *gin.Context) {
l := m.log.WithField("func", "accountVerifyGETHandler") l := m.log.WithField("func", "accountVerifyGETHandler")
authed, err := oauth.MustAuth(c, true, false, false, true) authed, err := oauth.MustAuth(c, true, false, false, true)
if err != nil { if err != nil {

View file

@ -39,6 +39,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/account"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
@ -63,10 +64,10 @@ type AccountCreateTestSuite struct {
testToken oauth2.TokenInfo testToken oauth2.TokenInfo
mockOauthServer *oauth.MockServer mockOauthServer *oauth.MockServer
mockStorage *storage.MockStorage mockStorage *storage.MockStorage
mediaHandler media.MediaHandler mediaHandler media.Handler
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
db db.DB db db.DB
accountModule *accountModule accountModule *account.Module
newUserFormHappyPath url.Values newUserFormHappyPath url.Values
} }
@ -164,7 +165,7 @@ func (suite *AccountCreateTestSuite) SetupSuite() {
suite.mastoConverter = mastotypes.New(suite.config, suite.db) suite.mastoConverter = mastotypes.New(suite.config, suite.db)
// and finally here's the thing we're actually testing! // and finally here's the thing we're actually testing!
suite.accountModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*accountModule) suite.accountModule = account.New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*account.Module)
} }
func (suite *AccountCreateTestSuite) TearDownSuite() { func (suite *AccountCreateTestSuite) TearDownSuite() {
@ -250,9 +251,9 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() {
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting 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 ctx.Request.Form = suite.newUserFormHappyPath
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
@ -324,9 +325,9 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoAuth() {
// setup // setup
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting 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 ctx.Request.Form = suite.newUserFormHappyPath
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
@ -349,8 +350,8 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoForm() {
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusBadRequest, recorder.Code)
@ -371,11 +372,11 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeakPassword()
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting 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 ctx.Request.Form = suite.newUserFormHappyPath
// set a weak password // set a weak password
ctx.Request.Form.Set("password", "weak") ctx.Request.Form.Set("password", "weak")
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusBadRequest, recorder.Code)
@ -396,11 +397,11 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeirdLocale() {
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting 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 ctx.Request.Form = suite.newUserFormHappyPath
// set an invalid locale // set an invalid locale
ctx.Request.Form.Set("locale", "neverneverland") ctx.Request.Form.Set("locale", "neverneverland")
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusBadRequest, recorder.Code)
@ -421,12 +422,12 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerRegistrationsCl
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting 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 ctx.Request.Form = suite.newUserFormHappyPath
// close registrations // close registrations
suite.config.AccountsConfig.OpenRegistration = false suite.config.AccountsConfig.OpenRegistration = false
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusBadRequest, recorder.Code)
@ -447,13 +448,13 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerReasonNotProvid
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting 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 ctx.Request.Form = suite.newUserFormHappyPath
// remove reason // remove reason
ctx.Request.Form.Set("reason", "") ctx.Request.Form.Set("reason", "")
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusBadRequest, recorder.Code)
@ -474,13 +475,13 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerInsufficientRea
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting 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 ctx.Request.Form = suite.newUserFormHappyPath
// remove reason // remove reason
ctx.Request.Form.Set("reason", "just cuz") ctx.Request.Form.Set("reason", "just cuz")
suite.accountModule.accountCreatePOSTHandler(ctx) suite.accountModule.AccountCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusBadRequest, recorder.Code)
@ -526,9 +527,9 @@ func (suite *AccountCreateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", updateCredentialsPath), body) // the endpoint we're hitting 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()) ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
suite.accountModule.accountUpdateCredentialsPATCHHandler(ctx) suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx)
// check response // check response

View file

@ -37,6 +37,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/account"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
@ -58,10 +59,10 @@ type AccountUpdateTestSuite struct {
testToken oauth2.TokenInfo testToken oauth2.TokenInfo
mockOauthServer *oauth.MockServer mockOauthServer *oauth.MockServer
mockStorage *storage.MockStorage mockStorage *storage.MockStorage
mediaHandler media.MediaHandler mediaHandler media.Handler
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
db db.DB db db.DB
accountModule *accountModule accountModule *account.Module
newUserFormHappyPath url.Values newUserFormHappyPath url.Values
} }
@ -159,7 +160,7 @@ func (suite *AccountUpdateTestSuite) SetupSuite() {
suite.mastoConverter = mastotypes.New(suite.config, suite.db) suite.mastoConverter = mastotypes.New(suite.config, suite.db)
// and finally here's the thing we're actually testing! // and finally here's the thing we're actually testing!
suite.accountModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*accountModule) suite.accountModule = account.New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*account.Module)
} }
func (suite *AccountUpdateTestSuite) TearDownSuite() { func (suite *AccountUpdateTestSuite) TearDownSuite() {
@ -278,9 +279,9 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal)
ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", updateCredentialsPath), body) // the endpoint we're hitting 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()) ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
suite.accountModule.accountUpdateCredentialsPATCHHandler(ctx) suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx)
// check response // check response

View file

@ -33,21 +33,24 @@ import (
) )
const ( const (
basePath = "/api/v1/admin" // BasePath is the base API path for this module
emojiPath = basePath + "/custom_emojis" BasePath = "/api/v1/admin"
// EmojiPath is used for posting/deleting custom emojis
EmojiPath = BasePath + "/custom_emojis"
) )
type adminModule struct { // Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc)
type Module struct {
config *config.Config config *config.Config
db db.DB db db.DB
mediaHandler media.MediaHandler mediaHandler media.Handler
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
log *logrus.Logger log *logrus.Logger
} }
// New returns a new account module // New returns a new admin module
func New(config *config.Config, db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule { func New(config *config.Config, db db.DB, mediaHandler media.Handler, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule {
return &adminModule{ return &Module{
config: config, config: config,
db: db, db: db,
mediaHandler: mediaHandler, mediaHandler: mediaHandler,
@ -57,12 +60,13 @@ func New(config *config.Config, db db.DB, mediaHandler media.MediaHandler, masto
} }
// Route attaches all routes from this module to the given router // Route attaches all routes from this module to the given router
func (m *adminModule) Route(r router.Router) error { func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodPost, emojiPath, m.emojiCreatePOSTHandler) r.AttachHandler(http.MethodPost, EmojiPath, m.emojiCreatePOSTHandler)
return nil return nil
} }
func (m *adminModule) CreateTables(db db.DB) error { // CreateTables creates the necessary tables for this module in the given database
func (m *Module) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.User{}, &gtsmodel.User{},
&gtsmodel.Account{}, &gtsmodel.Account{},

View file

@ -33,7 +33,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
func (m *adminModule) emojiCreatePOSTHandler(c *gin.Context) { func (m *Module) emojiCreatePOSTHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "emojiCreatePOSTHandler", "func": "emojiCreatePOSTHandler",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,

View file

@ -31,9 +31,11 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/router"
) )
const appsPath = "/api/v1/apps" // BasePath is the base path for this api module
const BasePath = "/api/v1/apps"
type appModule struct { // Module implements the ClientAPIModule interface for requests relating to registering/removing applications
type Module struct {
server oauth.Server server oauth.Server
db db.DB db db.DB
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
@ -42,7 +44,7 @@ type appModule struct {
// New returns a new auth module // New returns a new auth module
func New(srv oauth.Server, db db.DB, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule { func New(srv oauth.Server, db db.DB, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule {
return &appModule{ return &Module{
server: srv, server: srv,
db: db, db: db,
mastoConverter: mastoConverter, mastoConverter: mastoConverter,
@ -51,12 +53,13 @@ func New(srv oauth.Server, db db.DB, mastoConverter mastotypes.Converter, log *l
} }
// Route satisfies the RESTAPIModule interface // Route satisfies the RESTAPIModule interface
func (m *appModule) Route(s router.Router) error { func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodPost, appsPath, m.appsPOSTHandler) s.AttachHandler(http.MethodPost, BasePath, m.AppsPOSTHandler)
return nil return nil
} }
func (m *appModule) CreateTables(db db.DB) error { // CreateTables creates the necessary tables for this module in the given database
func (m *Module) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&oauth.Client{}, &oauth.Client{},
&oauth.Token{}, &oauth.Token{},

View file

@ -29,9 +29,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// appsPOSTHandler should be served at https://example.org/api/v1/apps // AppsPOSTHandler should be served at https://example.org/api/v1/apps
// It is equivalent to: https://docs.joinmastodon.org/methods/apps/ // It is equivalent to: https://docs.joinmastodon.org/methods/apps/
func (m *appModule) appsPOSTHandler(c *gin.Context) { func (m *Module) AppsPOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "AppsPOSTHandler") l := m.log.WithField("func", "AppsPOSTHandler")
l.Trace("entering AppsPOSTHandler") l.Trace("entering AppsPOSTHandler")

View file

@ -1,5 +0,0 @@
# auth
This package provides uses the [GoToSocial oauth2](https://github.com/gotosocial/oauth2) module (forked from [go-oauth2](https://github.com/go-oauth2/oauth2)) to provide [oauth2](https://www.oauth.com/) functionality to the GoToSocial client API.
It also provides a handler/middleware for attaching to the Gin engine for validating authenticated users.

View file

@ -16,12 +16,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
// Package auth is a module that provides oauth functionality to a router.
// It adds the following paths:
// /auth/sign_in
// /oauth/token
// /oauth/authorize
// It also includes the oauthTokenMiddleware, which can be attached to a router to authenticate every request by Bearer token.
package auth package auth
import ( import (
@ -37,12 +31,16 @@ import (
) )
const ( const (
authSignInPath = "/auth/sign_in" // AuthSignInPath is the API path for users to sign in through
oauthTokenPath = "/oauth/token" AuthSignInPath = "/auth/sign_in"
oauthAuthorizePath = "/oauth/authorize" // OauthTokenPath is the API path to use for granting token requests to users with valid credentials
OauthTokenPath = "/oauth/token"
// OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user)
OauthAuthorizePath = "/oauth/authorize"
) )
type authModule struct { // Module implements the ClientAPIModule interface for
type Module struct {
server oauth.Server server oauth.Server
db db.DB db db.DB
log *logrus.Logger log *logrus.Logger
@ -50,7 +48,7 @@ type authModule struct {
// New returns a new auth module // New returns a new auth module
func New(srv oauth.Server, db db.DB, log *logrus.Logger) apimodule.ClientAPIModule { func New(srv oauth.Server, db db.DB, log *logrus.Logger) apimodule.ClientAPIModule {
return &authModule{ return &Module{
server: srv, server: srv,
db: db, db: db,
log: log, log: log,
@ -58,20 +56,21 @@ func New(srv oauth.Server, db db.DB, log *logrus.Logger) apimodule.ClientAPIModu
} }
// Route satisfies the RESTAPIModule interface // Route satisfies the RESTAPIModule interface
func (m *authModule) Route(s router.Router) error { func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, authSignInPath, m.signInGETHandler) s.AttachHandler(http.MethodGet, AuthSignInPath, m.SignInGETHandler)
s.AttachHandler(http.MethodPost, authSignInPath, m.signInPOSTHandler) s.AttachHandler(http.MethodPost, AuthSignInPath, m.SignInPOSTHandler)
s.AttachHandler(http.MethodPost, oauthTokenPath, m.tokenPOSTHandler) s.AttachHandler(http.MethodPost, OauthTokenPath, m.TokenPOSTHandler)
s.AttachHandler(http.MethodGet, oauthAuthorizePath, m.authorizeGETHandler) s.AttachHandler(http.MethodGet, OauthAuthorizePath, m.AuthorizeGETHandler)
s.AttachHandler(http.MethodPost, oauthAuthorizePath, m.authorizePOSTHandler) s.AttachHandler(http.MethodPost, OauthAuthorizePath, m.AuthorizePOSTHandler)
s.AttachMiddleware(m.oauthTokenMiddleware) s.AttachMiddleware(m.OauthTokenMiddleware)
return nil return nil
} }
func (m *authModule) CreateTables(db db.DB) error { // CreateTables creates the necessary tables for this module in the given database
func (m *Module) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&oauth.Client{}, &oauth.Client{},
&oauth.Token{}, &oauth.Token{},

View file

@ -31,10 +31,10 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
) )
// authorizeGETHandler should be served as GET at https://example.org/oauth/authorize // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
// The idea here is to present an oauth authorize page to the user, with a button // The idea here is to present an oauth authorize page to the user, with a button
// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user // that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
func (m *authModule) authorizeGETHandler(c *gin.Context) { func (m *Module) AuthorizeGETHandler(c *gin.Context) {
l := m.log.WithField("func", "AuthorizeGETHandler") l := m.log.WithField("func", "AuthorizeGETHandler")
s := sessions.Default(c) s := sessions.Default(c)
@ -46,7 +46,7 @@ func (m *authModule) authorizeGETHandler(c *gin.Context) {
if err := parseAuthForm(c, l); err != nil { if err := parseAuthForm(c, l); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else { } else {
c.Redirect(http.StatusFound, authSignInPath) c.Redirect(http.StatusFound, AuthSignInPath)
} }
return return
} }
@ -108,11 +108,11 @@ func (m *authModule) authorizeGETHandler(c *gin.Context) {
}) })
} }
// authorizePOSTHandler should be served as POST at https://example.org/oauth/authorize // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
// At this point we assume that the user has A) logged in and B) accepted that the app should act for them, // At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
// so we should proceed with the authentication flow and generate an oauth token for them if we can. // so we should proceed with the authentication flow and generate an oauth token for them if we can.
// See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user // See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
func (m *authModule) authorizePOSTHandler(c *gin.Context) { func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "AuthorizePOSTHandler") l := m.log.WithField("func", "AuthorizePOSTHandler")
s := sessions.Default(c) s := sessions.Default(c)

View file

@ -24,12 +24,12 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// oauthTokenMiddleware checks if the client has presented a valid oauth Bearer token. // OauthTokenMiddleware checks if the client has presented a valid oauth Bearer token.
// If so, it will check the User that the token belongs to, and set that in the context of // If so, it will check the User that the token belongs to, and set that in the context of
// the request. Then, it will look up the account for that user, and set that in the request too. // the request. Then, it will look up the account for that user, and set that in the request too.
// If user or account can't be found, then the handler won't *fail*, in case the server wants to allow // If user or account can't be found, then the handler won't *fail*, in case the server wants to allow
// public requests that don't have a Bearer token set (eg., for public instance information and so on). // public requests that don't have a Bearer token set (eg., for public instance information and so on).
func (m *authModule) oauthTokenMiddleware(c *gin.Context) { func (m *Module) OauthTokenMiddleware(c *gin.Context) {
l := m.log.WithField("func", "ValidatePassword") l := m.log.WithField("func", "ValidatePassword")
l.Trace("entering OauthTokenMiddleware") l.Trace("entering OauthTokenMiddleware")

View file

@ -28,23 +28,24 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// login just wraps a form-submitted username (we want an email) and password
type login struct { type login struct {
Email string `form:"username"` Email string `form:"username"`
Password string `form:"password"` Password string `form:"password"`
} }
// signInGETHandler should be served at https://example.org/auth/sign_in. // SignInGETHandler should be served at https://example.org/auth/sign_in.
// The idea is to present a sign in page to the user, where they can enter their username and password. // The idea is to present a sign in page to the user, where they can enter their username and password.
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler
func (m *authModule) signInGETHandler(c *gin.Context) { func (m *Module) SignInGETHandler(c *gin.Context) {
m.log.WithField("func", "SignInGETHandler").Trace("serving sign in html") m.log.WithField("func", "SignInGETHandler").Trace("serving sign in html")
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
} }
// signInPOSTHandler should be served at https://example.org/auth/sign_in. // SignInPOSTHandler should be served at https://example.org/auth/sign_in.
// The idea is to present a sign in page to the user, where they can enter their username and password. // The idea is to present a sign in page to the user, where they can enter their username and password.
// The handler will then redirect to the auth handler served at /auth // The handler will then redirect to the auth handler served at /auth
func (m *authModule) signInPOSTHandler(c *gin.Context) { func (m *Module) SignInPOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "SignInPOSTHandler") l := m.log.WithField("func", "SignInPOSTHandler")
s := sessions.Default(c) s := sessions.Default(c)
form := &login{} form := &login{}
@ -54,7 +55,7 @@ func (m *authModule) signInPOSTHandler(c *gin.Context) {
} }
l.Tracef("parsed form: %+v", form) l.Tracef("parsed form: %+v", form)
userid, err := m.validatePassword(form.Email, form.Password) userid, err := m.ValidatePassword(form.Email, form.Password)
if err != nil { if err != nil {
c.String(http.StatusForbidden, err.Error()) c.String(http.StatusForbidden, err.Error())
return return
@ -67,14 +68,14 @@ func (m *authModule) signInPOSTHandler(c *gin.Context) {
} }
l.Trace("redirecting to auth page") l.Trace("redirecting to auth page")
c.Redirect(http.StatusFound, oauthAuthorizePath) c.Redirect(http.StatusFound, OauthAuthorizePath)
} }
// validatePassword takes an email address and a password. // ValidatePassword takes an email address and a password.
// The goal is to authenticate the password against the one for that email // The goal is to authenticate the password against the one for that email
// address stored in the database. If OK, we return the userid (a uuid) for that user, // address stored in the database. If OK, we return the userid (a uuid) for that user,
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
func (m *authModule) validatePassword(email string, password string) (userid string, err error) { func (m *Module) ValidatePassword(email string, password string) (userid string, err error) {
l := m.log.WithField("func", "ValidatePassword") l := m.log.WithField("func", "ValidatePassword")
// make sure an email/password was provided and bail if not // make sure an email/password was provided and bail if not

View file

@ -24,10 +24,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// tokenPOSTHandler should be served as a POST at https://example.org/oauth/token // TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
// See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token
func (m *authModule) tokenPOSTHandler(c *gin.Context) { func (m *Module) TokenPOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "TokenPOSTHandler") l := m.log.WithField("func", "TokenPOSTHandler")
l.Trace("entered TokenPOSTHandler") l.Trace("entered TokenPOSTHandler")
if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil { if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil {

View file

@ -32,12 +32,14 @@ import (
) )
const ( const (
// AccountIDKey is the url key for account id (an account uuid)
AccountIDKey = "account_id" AccountIDKey = "account_id"
// MediaTypeKey is the url key for media type (usually something like attachment or header etc)
MediaTypeKey = "media_type" MediaTypeKey = "media_type"
// MediaSizeKey is the url key for the desired media size--original/small/static
MediaSizeKey = "media_size" MediaSizeKey = "media_size"
// FileNameKey is the actual filename being sought. Will usually be a UUID then something like .jpeg
FileNameKey = "file_name" FileNameKey = "file_name"
FilesPath = "files"
) )
// FileServer implements the RESTAPIModule interface. // FileServer implements the RESTAPIModule interface.
@ -67,6 +69,7 @@ func (m *FileServer) Route(s router.Router) error {
return nil return nil
} }
// CreateTables populates necessary tables in the given DB
func (m *FileServer) CreateTables(db db.DB) error { func (m *FileServer) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.MediaAttachment{}, &gtsmodel.MediaAttachment{},

View file

@ -49,7 +49,7 @@ type ServeFileTestSuite struct {
log *logrus.Logger log *logrus.Logger
storage storage.Storage storage storage.Storage
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
mediaHandler media.MediaHandler mediaHandler media.Handler
oauthServer oauth.Server oauthServer oauth.Server
// standard suite models // standard suite models

View file

@ -32,10 +32,12 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/router"
) )
// BasePath is the base API path for making media requests
const BasePath = "/api/v1/media" const BasePath = "/api/v1/media"
type MediaModule struct { // Module implements the ClientAPIModule interface for media
mediaHandler media.MediaHandler type Module struct {
mediaHandler media.Handler
config *config.Config config *config.Config
db db.DB db db.DB
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
@ -43,8 +45,8 @@ type MediaModule struct {
} }
// New returns a new auth module // New returns a new auth module
func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule { func New(db db.DB, mediaHandler media.Handler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
return &MediaModule{ return &Module{
mediaHandler: mediaHandler, mediaHandler: mediaHandler,
config: config, config: config,
db: db, db: db,
@ -54,12 +56,13 @@ func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Co
} }
// Route satisfies the RESTAPIModule interface // Route satisfies the RESTAPIModule interface
func (m *MediaModule) Route(s router.Router) error { func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler) s.AttachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler)
return nil return nil
} }
func (m *MediaModule) CreateTables(db db.DB) error { // CreateTables populates necessary tables in the given DB
func (m *Module) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.MediaAttachment{}, &gtsmodel.MediaAttachment{},
} }

View file

@ -33,7 +33,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *MediaModule) MediaCreatePOSTHandler(c *gin.Context) { // MediaCreatePOSTHandler handles requests to create/upload media attachments
func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "statusCreatePOSTHandler") 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* authed, err := oauth.MustAuth(c, true, true, true, true) // posting new media is serious business so we want *everything*
if err != nil { if err != nil {

View file

@ -52,7 +52,7 @@ type MediaCreateTestSuite struct {
log *logrus.Logger log *logrus.Logger
storage storage.Storage storage storage.Storage
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
mediaHandler media.MediaHandler mediaHandler media.Handler
oauthServer oauth.Server oauthServer oauth.Server
// standard suite models // standard suite models
@ -64,7 +64,7 @@ type MediaCreateTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
// item being tested // item being tested
mediaModule *mediamodule.MediaModule mediaModule *mediamodule.Module
} }
/* /*
@ -82,7 +82,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() {
suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.oauthServer = testrig.NewTestOauthServer(suite.db)
// setup module being tested // setup module being tested
suite.mediaModule = mediamodule.New(suite.db, suite.mediaHandler, suite.mastoConverter, suite.config, suite.log).(*mediamodule.MediaModule) suite.mediaModule = mediamodule.New(suite.db, suite.mediaHandler, suite.mastoConverter, suite.config, suite.log).(*mediamodule.Module)
} }
func (suite *MediaCreateTestSuite) TearDownSuite() { func (suite *MediaCreateTestSuite) TearDownSuite() {
@ -115,7 +115,7 @@ func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful()
// set up the context for the request // set up the context for the request
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])

View file

@ -20,8 +20,9 @@ package security
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
// flocBlock prevents google chrome cohort tracking by writing the Permissions-Policy header after all other parts of the request have been completed. // FlocBlock is a middleware that prevents google chrome cohort tracking by
// writing the Permissions-Policy header after all other parts of the request have been completed.
// See: https://plausible.io/blog/google-floc // See: https://plausible.io/blog/google-floc
func (m *module) flocBlock(c *gin.Context) { func (m *Module) FlocBlock(c *gin.Context) {
c.Header("Permissions-Policy", "interest-cohort=()") c.Header("Permissions-Policy", "interest-cohort=()")
} }

View file

@ -26,25 +26,27 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/router"
) )
// module implements the apiclient interface // Module implements the ClientAPIModule interface for security middleware
type module struct { type Module struct {
config *config.Config config *config.Config
log *logrus.Logger log *logrus.Logger
} }
// New returns a new security module // New returns a new security module
func New(config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule { func New(config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
return &module{ return &Module{
config: config, config: config,
log: log, log: log,
} }
} }
func (m *module) Route(s router.Router) error { // Route attaches security middleware to the given router
s.AttachMiddleware(m.flocBlock) func (m *Module) Route(s router.Router) error {
s.AttachMiddleware(m.FlocBlock)
return nil return nil
} }
func (m *module) CreateTables(db db.DB) error { // 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 return nil
} }

View file

@ -36,42 +36,60 @@ import (
) )
const ( const (
// IDKey is for status UUIDs
IDKey = "id" IDKey = "id"
// BasePath is the base path for serving the status API
BasePath = "/api/v1/statuses" BasePath = "/api/v1/statuses"
// BasePathWithID is just the base path with the ID key in it.
// Use this anywhere you need to know the ID of the status being queried.
BasePathWithID = BasePath + "/:" + IDKey BasePathWithID = BasePath + "/:" + IDKey
// ContextPath is used for fetching context of posts
ContextPath = BasePathWithID + "/context" ContextPath = BasePathWithID + "/context"
// FavouritedPath is for seeing who's faved a given status
FavouritedPath = BasePathWithID + "/favourited_by" FavouritedPath = BasePathWithID + "/favourited_by"
// FavouritePath is for posting a fave on a status
FavouritePath = BasePathWithID + "/favourite" FavouritePath = BasePathWithID + "/favourite"
// UnfavouritePath is for removing a fave from a status
UnfavouritePath = BasePathWithID + "/unfavourite" UnfavouritePath = BasePathWithID + "/unfavourite"
// RebloggedPath is for seeing who's boosted a given status
RebloggedPath = BasePathWithID + "/reblogged_by" RebloggedPath = BasePathWithID + "/reblogged_by"
// ReblogPath is for boosting/reblogging a given status
ReblogPath = BasePathWithID + "/reblog" ReblogPath = BasePathWithID + "/reblog"
// UnreblogPath is for undoing a boost/reblog of a given status
UnreblogPath = BasePathWithID + "/unreblog" UnreblogPath = BasePathWithID + "/unreblog"
// BookmarkPath is for creating a bookmark on a given status
BookmarkPath = BasePathWithID + "/bookmark" BookmarkPath = BasePathWithID + "/bookmark"
// UnbookmarkPath is for removing a bookmark from a given status
UnbookmarkPath = BasePathWithID + "/unbookmark" UnbookmarkPath = BasePathWithID + "/unbookmark"
// MutePath is for muting a given status so that notifications will no longer be received about it.
MutePath = BasePathWithID + "/mute" MutePath = BasePathWithID + "/mute"
// UnmutePath is for undoing an existing mute
UnmutePath = BasePathWithID + "/unmute" UnmutePath = BasePathWithID + "/unmute"
// PinPath is for pinning a status to an account profile so that it's the first thing people see
PinPath = BasePathWithID + "/pin" PinPath = BasePathWithID + "/pin"
// UnpinPath is for undoing a pin and returning a status to the ever-swirling drain of time and entropy
UnpinPath = BasePathWithID + "/unpin" UnpinPath = BasePathWithID + "/unpin"
) )
type StatusModule struct { // Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with statuses
type Module struct {
config *config.Config config *config.Config
db db.DB db db.DB
mediaHandler media.MediaHandler mediaHandler media.Handler
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
distributor distributor.Distributor distributor distributor.Distributor
log *logrus.Logger log *logrus.Logger
} }
// New returns a new account module // New returns a new account module
func New(config *config.Config, db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule { func New(config *config.Config, db db.DB, mediaHandler media.Handler, mastoConverter mastotypes.Converter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule {
return &StatusModule{ return &Module{
config: config, config: config,
db: db, db: db,
mediaHandler: mediaHandler, mediaHandler: mediaHandler,
@ -82,7 +100,7 @@ func New(config *config.Config, db db.DB, mediaHandler media.MediaHandler, masto
} }
// Route attaches all routes from this module to the given router // Route attaches all routes from this module to the given router
func (m *StatusModule) Route(r router.Router) error { func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler) r.AttachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler)
r.AttachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler) r.AttachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler)
@ -93,7 +111,8 @@ func (m *StatusModule) Route(r router.Router) error {
return nil return nil
} }
func (m *StatusModule) CreateTables(db db.DB) error { // CreateTables populates necessary tables in the given DB
func (m *Module) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.User{}, &gtsmodel.User{},
&gtsmodel.Account{}, &gtsmodel.Account{},
@ -121,7 +140,8 @@ func (m *StatusModule) CreateTables(db db.DB) error {
return nil return nil
} }
func (m *StatusModule) muxHandler(c *gin.Context) { // muxHandler is a little workaround to overcome the limitations of Gin
func (m *Module) muxHandler(c *gin.Context) {
m.log.Debug("entering mux handler") m.log.Debug("entering mux handler")
ru := c.Request.RequestURI ru := c.Request.RequestURI

View file

@ -53,7 +53,8 @@ type advancedVisibilityFlagsForm struct {
Likeable *bool `form:"likeable"` Likeable *bool `form:"likeable"`
} }
func (m *StatusModule) StatusCreatePOSTHandler(c *gin.Context) { // StatusCreatePOSTHandler deals with the creation of new statuses
func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "statusCreatePOSTHandler") 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* authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything*
if err != nil { if err != nil {
@ -318,7 +319,7 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis gtsmodel.
return nil return nil
} }
func (m *StatusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { func (m *Module) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
if form.InReplyToID == "" { if form.InReplyToID == "" {
return nil return nil
} }
@ -336,9 +337,8 @@ func (m *StatusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun
if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil { if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
if _, ok := err.(db.ErrNoEntries); ok { 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 because it doesn't exist", form.InReplyToID)
} else {
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
} }
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
} }
if !repliedStatus.VisibilityAdvanced.Replyable { if !repliedStatus.VisibilityAdvanced.Replyable {
@ -349,9 +349,8 @@ func (m *StatusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun
if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
if _, ok := err.(db.ErrNoEntries); ok { 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 because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
} else {
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
} }
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
} }
// check if a block exists // check if a block exists
if blocked, err := m.db.Blocked(thisAccountID, repliedAccount.ID); err != nil { if blocked, err := m.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
@ -367,7 +366,7 @@ func (m *StatusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun
return nil return nil
} }
func (m *StatusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { func (m *Module) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
if form.MediaIDs == nil { if form.MediaIDs == nil {
return nil return nil
} }
@ -408,7 +407,7 @@ func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string
return nil return nil
} }
func (m *StatusModule) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { func (m *Module) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
menchies := []string{} menchies := []string{}
gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID) gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID)
if err != nil { if err != nil {
@ -427,7 +426,7 @@ func (m *StatusModule) parseMentions(form *advancedStatusCreateForm, accountID s
return nil return nil
} }
func (m *StatusModule) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { func (m *Module) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
tags := []string{} tags := []string{}
gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID) gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID)
if err != nil { if err != nil {
@ -446,7 +445,7 @@ func (m *StatusModule) parseTags(form *advancedStatusCreateForm, accountID strin
return nil return nil
} }
func (m *StatusModule) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { func (m *Module) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
emojis := []string{} emojis := []string{}
gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID) gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID)
if err != nil { if err != nil {

View file

@ -29,7 +29,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *StatusModule) StatusDELETEHandler(c *gin.Context) { // StatusDELETEHandler verifies and handles deletion of a status
func (m *Module) StatusDELETEHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "StatusDELETEHandler", "func": "StatusDELETEHandler",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,

View file

@ -29,7 +29,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *StatusModule) StatusFavePOSTHandler(c *gin.Context) { // StatusFavePOSTHandler handles fave requests against a given status ID
func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "StatusFavePOSTHandler", "func": "StatusFavePOSTHandler",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,

View file

@ -29,7 +29,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *StatusModule) StatusFavedByGETHandler(c *gin.Context) { // 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{ l := m.log.WithFields(logrus.Fields{
"func": "statusGETHandler", "func": "statusGETHandler",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,

View file

@ -28,7 +28,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *StatusModule) StatusGETHandler(c *gin.Context) { // 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{ l := m.log.WithFields(logrus.Fields{
"func": "statusGETHandler", "func": "statusGETHandler",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,

View file

@ -29,7 +29,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *StatusModule) StatusUnfavePOSTHandler(c *gin.Context) { // 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{ l := m.log.WithFields(logrus.Fields{
"func": "StatusUnfavePOSTHandler", "func": "StatusUnfavePOSTHandler",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,

View file

@ -52,7 +52,7 @@ type StatusCreateTestSuite struct {
log *logrus.Logger log *logrus.Logger
storage storage.Storage storage storage.Storage
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
mediaHandler media.MediaHandler mediaHandler media.Handler
oauthServer oauth.Server oauthServer oauth.Server
distributor distributor.Distributor distributor distributor.Distributor
@ -65,7 +65,7 @@ type StatusCreateTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
// module being tested // module being tested
statusModule *status.StatusModule statusModule *status.Module
} }
/* /*
@ -85,7 +85,7 @@ func (suite *StatusCreateTestSuite) SetupSuite() {
suite.distributor = testrig.NewTestDistributor() suite.distributor = testrig.NewTestDistributor()
// setup module being tested // setup module being tested
suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
} }
func (suite *StatusCreateTestSuite) TearDownSuite() { func (suite *StatusCreateTestSuite) TearDownSuite() {
@ -121,7 +121,7 @@ func (suite *StatusCreateTestSuite) TearDownTest() {
func (suite *StatusCreateTestSuite) TestPostNewStatus() { func (suite *StatusCreateTestSuite) TestPostNewStatus() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
// setup // setup
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
@ -175,7 +175,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() { func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
// setup // setup
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
@ -216,7 +216,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
// Try to reply to a status that doesn't exist // Try to reply to a status that doesn't exist
func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() { func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
// setup // setup
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
@ -247,7 +247,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
// Post a reply to the status of a local user that allows replies. // Post a reply to the status of a local user that allows replies.
func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() { func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
// setup // setup
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
@ -287,7 +287,7 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
// Take a media file which is currently not associated with a status, and attach it to a new status. // Take a media file which is currently not associated with a status, and attach it to a new status.
func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() { func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
// setup // setup
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()

View file

@ -52,7 +52,7 @@ type StatusFaveTestSuite struct {
log *logrus.Logger log *logrus.Logger
storage storage.Storage storage storage.Storage
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
mediaHandler media.MediaHandler mediaHandler media.Handler
oauthServer oauth.Server oauthServer oauth.Server
distributor distributor.Distributor distributor distributor.Distributor
@ -66,7 +66,7 @@ type StatusFaveTestSuite struct {
testStatuses map[string]*gtsmodel.Status testStatuses map[string]*gtsmodel.Status
// module being tested // module being tested
statusModule *status.StatusModule statusModule *status.Module
} }
/* /*
@ -86,7 +86,7 @@ func (suite *StatusFaveTestSuite) SetupSuite() {
suite.distributor = testrig.NewTestDistributor() suite.distributor = testrig.NewTestDistributor()
// setup module being tested // setup module being tested
suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
} }
func (suite *StatusFaveTestSuite) TearDownSuite() { func (suite *StatusFaveTestSuite) TearDownSuite() {
@ -120,7 +120,7 @@ func (suite *StatusFaveTestSuite) TearDownTest() {
func (suite *StatusFaveTestSuite) TestPostFave() { func (suite *StatusFaveTestSuite) TestPostFave() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
targetStatus := suite.testStatuses["admin_account_status_2"] targetStatus := suite.testStatuses["admin_account_status_2"]
@ -168,7 +168,7 @@ func (suite *StatusFaveTestSuite) TestPostFave() {
func (suite *StatusFaveTestSuite) TestPostUnfaveable() { func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable

View file

@ -52,7 +52,7 @@ type StatusFavedByTestSuite struct {
log *logrus.Logger log *logrus.Logger
storage storage.Storage storage storage.Storage
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
mediaHandler media.MediaHandler mediaHandler media.Handler
oauthServer oauth.Server oauthServer oauth.Server
distributor distributor.Distributor distributor distributor.Distributor
@ -66,7 +66,7 @@ type StatusFavedByTestSuite struct {
testStatuses map[string]*gtsmodel.Status testStatuses map[string]*gtsmodel.Status
// module being tested // module being tested
statusModule *status.StatusModule statusModule *status.Module
} }
// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
@ -82,7 +82,7 @@ func (suite *StatusFavedByTestSuite) SetupSuite() {
suite.distributor = testrig.NewTestDistributor() suite.distributor = testrig.NewTestDistributor()
// setup module being tested // setup module being tested
suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
} }
func (suite *StatusFavedByTestSuite) TearDownSuite() { func (suite *StatusFavedByTestSuite) TearDownSuite() {
@ -114,7 +114,7 @@ func (suite *StatusFavedByTestSuite) TearDownTest() {
func (suite *StatusFavedByTestSuite) TestGetFavedBy() { func (suite *StatusFavedByTestSuite) TestGetFavedBy() {
t := suite.testTokens["local_account_2"] t := suite.testTokens["local_account_2"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
targetStatus := suite.testStatuses["admin_account_status_1"] // this status is faved by local_account_1 targetStatus := suite.testStatuses["admin_account_status_1"] // this status is faved by local_account_1

View file

@ -43,7 +43,7 @@ type StatusGetTestSuite struct {
log *logrus.Logger log *logrus.Logger
storage storage.Storage storage storage.Storage
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
mediaHandler media.MediaHandler mediaHandler media.Handler
oauthServer oauth.Server oauthServer oauth.Server
distributor distributor.Distributor distributor distributor.Distributor
@ -56,7 +56,7 @@ type StatusGetTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
// module being tested // module being tested
statusModule *status.StatusModule statusModule *status.Module
} }
/* /*
@ -76,7 +76,7 @@ func (suite *StatusGetTestSuite) SetupSuite() {
suite.distributor = testrig.NewTestDistributor() suite.distributor = testrig.NewTestDistributor()
// setup module being tested // setup module being tested
suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
} }
func (suite *StatusGetTestSuite) TearDownSuite() { func (suite *StatusGetTestSuite) TearDownSuite() {

View file

@ -52,7 +52,7 @@ type StatusUnfaveTestSuite struct {
log *logrus.Logger log *logrus.Logger
storage storage.Storage storage storage.Storage
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
mediaHandler media.MediaHandler mediaHandler media.Handler
oauthServer oauth.Server oauthServer oauth.Server
distributor distributor.Distributor distributor distributor.Distributor
@ -66,7 +66,7 @@ type StatusUnfaveTestSuite struct {
testStatuses map[string]*gtsmodel.Status testStatuses map[string]*gtsmodel.Status
// module being tested // module being tested
statusModule *status.StatusModule statusModule *status.Module
} }
/* /*
@ -86,7 +86,7 @@ func (suite *StatusUnfaveTestSuite) SetupSuite() {
suite.distributor = testrig.NewTestDistributor() suite.distributor = testrig.NewTestDistributor()
// setup module being tested // setup module being tested
suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
} }
func (suite *StatusUnfaveTestSuite) TearDownSuite() { func (suite *StatusUnfaveTestSuite) TearDownSuite() {
@ -120,7 +120,7 @@ func (suite *StatusUnfaveTestSuite) TearDownTest() {
func (suite *StatusUnfaveTestSuite) TestPostUnfave() { func (suite *StatusUnfaveTestSuite) TestPostUnfave() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
// this is the status we wanna unfave: in the testrig it's already faved by this account // this is the status we wanna unfave: in the testrig it's already faved by this account
targetStatus := suite.testStatuses["admin_account_status_1"] targetStatus := suite.testStatuses["admin_account_status_1"]
@ -169,7 +169,7 @@ func (suite *StatusUnfaveTestSuite) TestPostUnfave() {
func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() { func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.TokenToOauthToken(t)
// this is the status we wanna unfave: in the testrig it's not faved by this account // this is the status we wanna unfave: in the testrig it's not faved by this account
targetStatus := suite.testStatuses["admin_account_status_2"] targetStatus := suite.testStatuses["admin_account_status_2"]

View file

@ -251,6 +251,7 @@ type Flags struct {
StatusesMaxMediaFiles string StatusesMaxMediaFiles string
} }
// Defaults contains all the default values for a gotosocial config
type Defaults struct { type Defaults struct {
LogLevel string LogLevel string
ApplicationName string ApplicationName string

View file

@ -96,6 +96,8 @@ func Default() *Config {
} }
} }
// GetDefaults returns a populated Defaults struct with most of the values set to reasonable defaults.
// Note that if you use this function, you still need to set Host and, if desired, ConfigPath.
func GetDefaults() Defaults { func GetDefaults() Defaults {
return Defaults{ return Defaults{
LogLevel: "info", LogLevel: "info",
@ -136,6 +138,7 @@ func GetDefaults() Defaults {
} }
} }
// GetTestDefaults returns a Defaults struct with values set that are suitable for local testing.
func GetTestDefaults() Defaults { func GetTestDefaults() Defaults {
return Defaults{ return Defaults{
LogLevel: "trace", LogLevel: "trace",

View file

@ -25,7 +25,6 @@ import (
"sync" "sync"
"github.com/go-fed/activity/pub" "github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
) )
@ -49,7 +48,19 @@ func newFederatingDB(db DB, config *config.Config) pub.Database {
/* /*
GO-FED DB INTERFACE-IMPLEMENTING FUNCTIONS GO-FED DB INTERFACE-IMPLEMENTING FUNCTIONS
*/ */
func (f *federatingDB) Lock(ctx context.Context, id *url.URL) error {
// Lock takes a lock for the object at the specified id. If an error
// is returned, the lock must not have been taken.
//
// The lock must be able to succeed for an id that does not exist in
// the database. This means acquiring the lock does not guarantee the
// entry exists in the database.
//
// Locks are encouraged to be lightweight and in the Go layer, as some
// processes require tight loops acquiring and releasing locks.
//
// Used to ensure race conditions in multiple requests do not occur.
func (f *federatingDB) Lock(c context.Context, id *url.URL) error {
// Before any other Database methods are called, the relevant `id` // Before any other Database methods are called, the relevant `id`
// entries are locked to allow for fine-grained concurrency. // entries are locked to allow for fine-grained concurrency.
@ -65,7 +76,11 @@ func (f *federatingDB) Lock(ctx context.Context, id *url.URL) error {
return nil return nil
} }
func (f *federatingDB) Unlock(ctx context.Context, id *url.URL) error { // Unlock makes the lock for the object at the specified id available.
// If an error is returned, the lock must have still been freed.
//
// Used to ensure race conditions in multiple requests do not occur.
func (f *federatingDB) Unlock(c context.Context, id *url.URL) error {
// Once Go-Fed is done calling Database methods, the relevant `id` // Once Go-Fed is done calling Database methods, the relevant `id`
// entries are unlocked. // entries are unlocked.
@ -78,82 +93,168 @@ func (f *federatingDB) Unlock(ctx context.Context, id *url.URL) error {
return nil return nil
} }
func (f *federatingDB) InboxContains(ctx context.Context, inbox *url.URL, id *url.URL) (bool, error) { // InboxContains returns true if the OrderedCollection at 'inbox'
// contains the specified 'id'.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error) {
return false, nil return false, nil
} }
func (f *federatingDB) GetInbox(ctx context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { // GetInbox returns the first ordered collection page of the outbox at
// the specified IRI, for prepending new items.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) {
return nil, nil return nil, nil
} }
func (f *federatingDB) SetInbox(ctx context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error { // SetInbox saves the inbox value given from GetInbox, with new items
// prepended. Note that the new items must not be added as independent
// database entries. Separate calls to Create will do that.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error {
return nil return nil
} }
func (f *federatingDB) Owns(ctx context.Context, id *url.URL) (owns bool, err error) { // Owns returns true if the database has an entry for the IRI and it
return id.Host == f.config.Host, nil // exists in the database.
} //
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) ActorForOutbox(ctx context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) { func (f *federatingDB) Owns(c context.Context, id *url.URL) (owns bool, err error) {
return nil, nil
}
func (f *federatingDB) ActorForInbox(ctx context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) {
return nil, nil
}
func (f *federatingDB) OutboxForInbox(ctx context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) {
return nil, nil
}
func (f *federatingDB) Exists(ctx context.Context, id *url.URL) (exists bool, err error) {
return false, nil return false, nil
} }
func (f *federatingDB) Get(ctx context.Context, id *url.URL) (value vocab.Type, err error) { // ActorForOutbox fetches the actor's IRI for the given outbox IRI.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) {
return nil, nil return nil, nil
} }
func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { // ActorForInbox fetches the actor's IRI for the given outbox IRI.
t, err := streams.NewTypeResolver() //
if err != nil { // The library makes this call only after acquiring a lock first.
return err func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) {
} return nil, nil
if err := t.Resolve(ctx, asType); err != nil { }
return err
} // OutboxForInbox fetches the corresponding actor's outbox IRI for the
asType.GetTypeName() // actor's inbox IRI.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) {
return nil, nil
}
// Exists returns true if the database has an entry for the specified
// id. It may not be owned by this application instance.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err error) {
return false, nil
}
// Get returns the database entry for the specified id.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, err error) {
return nil, nil
}
// Create adds a new entry to the database which must be able to be
// keyed by its id.
//
// Note that Activity values received from federated peers may also be
// created in the database this way if the Federating Protocol is
// enabled. The client may freely decide to store only the id instead of
// the entire value.
//
// The library makes this call only after acquiring a lock first.
//
// Under certain conditions and network activities, Create may be called
// multiple times for the same ActivityStreams object.
func (f *federatingDB) Create(c context.Context, asType vocab.Type) error {
return nil return nil
} }
func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { // Update sets an existing entry to the database based on the value's
// id.
//
// Note that Activity values received from federated peers may also be
// updated in the database this way if the Federating Protocol is
// enabled. The client may freely decide to store only the id instead of
// the entire value.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Update(c context.Context, asType vocab.Type) error {
return nil return nil
} }
func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { // Delete removes the entry with the given id.
//
// Delete is only called for federated objects. Deletes from the Social
// Protocol instead call Update to create a Tombstone.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Delete(c context.Context, id *url.URL) error {
return nil return nil
} }
func (f *federatingDB) GetOutbox(ctx context.Context, outboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { // GetOutbox returns the first ordered collection page of the outbox
// at the specified IRI, for prepending new items.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) {
return nil, nil return nil, nil
} }
func (f *federatingDB) SetOutbox(ctx context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error { // SetOutbox saves the outbox value given from GetOutbox, with new items
// prepended. Note that the new items must not be added as independent
// database entries. Separate calls to Create will do that.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error {
return nil return nil
} }
func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (id *url.URL, err error) { // NewID creates a new IRI id for the provided activity or object. The
// implementation does not need to set the 'id' property and simply
// needs to determine the value.
//
// The go-fed library will handle setting the 'id' property on the
// activity or object provided with the value returned.
func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) {
return nil, nil return nil, nil
} }
func (f *federatingDB) Followers(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { // Followers obtains the Followers Collection for an actor with the
// given id.
//
// If modified, the library will then call Update.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) {
return nil, nil return nil, nil
} }
func (f *federatingDB) Following(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { // Following obtains the Following Collection for an actor with the
// given id.
//
// If modified, the library will then call Update.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) {
return nil, nil return nil, nil
} }
func (f *federatingDB) Liked(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { // Liked obtains the Liked Collection for an actor with the
// given id.
//
// If modified, the library will then call Update.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) {
return nil, nil return nil, nil
} }

View file

@ -22,29 +22,29 @@ package gtsmodel
type ActivityStreamsObject string type ActivityStreamsObject string
const ( const (
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article // ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article
ActivityStreamsArticle ActivityStreamsObject = "Article" ActivityStreamsArticle ActivityStreamsObject = "Article"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio // ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio
ActivityStreamsAudio ActivityStreamsObject = "Audio" ActivityStreamsAudio ActivityStreamsObject = "Audio"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document // ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document
ActivityStreamsDocument ActivityStreamsObject = "Event" ActivityStreamsDocument ActivityStreamsObject = "Event"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event // ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
ActivityStreamsEvent ActivityStreamsObject = "Event" ActivityStreamsEvent ActivityStreamsObject = "Event"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image // ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image
ActivityStreamsImage ActivityStreamsObject = "Image" ActivityStreamsImage ActivityStreamsObject = "Image"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note // ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
ActivityStreamsNote ActivityStreamsObject = "Note" ActivityStreamsNote ActivityStreamsObject = "Note"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page // ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page
ActivityStreamsPage ActivityStreamsObject = "Page" ActivityStreamsPage ActivityStreamsObject = "Page"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place // ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place
ActivityStreamsPlace ActivityStreamsObject = "Place" ActivityStreamsPlace ActivityStreamsObject = "Place"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile // ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile
ActivityStreamsProfile ActivityStreamsObject = "Profile" ActivityStreamsProfile ActivityStreamsObject = "Profile"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship // ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship
ActivityStreamsRelationship ActivityStreamsObject = "Relationship" ActivityStreamsRelationship ActivityStreamsObject = "Relationship"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone // ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
ActivityStreamsTombstone ActivityStreamsObject = "Tombstone" ActivityStreamsTombstone ActivityStreamsObject = "Tombstone"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video // ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video
ActivityStreamsVideo ActivityStreamsObject = "Video" ActivityStreamsVideo ActivityStreamsObject = "Video"
) )
@ -52,15 +52,15 @@ const (
type ActivityStreamsActor string type ActivityStreamsActor string
const ( const (
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application // ActivityStreamsApplication https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
ActivityStreamsApplication ActivityStreamsActor = "Application" ActivityStreamsApplication ActivityStreamsActor = "Application"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group // ActivityStreamsGroup https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
ActivityStreamsGroup ActivityStreamsActor = "Group" ActivityStreamsGroup ActivityStreamsActor = "Group"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization // ActivityStreamsOrganization https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
ActivityStreamsOrganization ActivityStreamsActor = "Organization" ActivityStreamsOrganization ActivityStreamsActor = "Organization"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person // ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
ActivityStreamsPerson ActivityStreamsActor = "Person" ActivityStreamsPerson ActivityStreamsActor = "Person"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service // ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
ActivityStreamsService ActivityStreamsActor = "Service" ActivityStreamsService ActivityStreamsActor = "Service"
) )
@ -68,60 +68,60 @@ const (
type ActivityStreamsActivity string type ActivityStreamsActivity string
const ( const (
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept // ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept
ActivityStreamsAccept ActivityStreamsActivity = "Accept" ActivityStreamsAccept ActivityStreamsActivity = "Accept"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add // ActivityStreamsAdd https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add
ActivityStreamsAdd ActivityStreamsActivity = "Add" ActivityStreamsAdd ActivityStreamsActivity = "Add"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce // ActivityStreamsAnnounce https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce
ActivityStreamsAnnounce ActivityStreamsActivity = "Announce" ActivityStreamsAnnounce ActivityStreamsActivity = "Announce"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive // ActivityStreamsArrive https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive
ActivityStreamsArrive ActivityStreamsActivity = "Arrive" ActivityStreamsArrive ActivityStreamsActivity = "Arrive"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block // ActivityStreamsBlock https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block
ActivityStreamsBlock ActivityStreamsActivity = "Block" ActivityStreamsBlock ActivityStreamsActivity = "Block"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create // ActivityStreamsCreate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create
ActivityStreamsCreate ActivityStreamsActivity = "Create" ActivityStreamsCreate ActivityStreamsActivity = "Create"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete // ActivityStreamsDelete https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete
ActivityStreamsDelete ActivityStreamsActivity = "Delete" ActivityStreamsDelete ActivityStreamsActivity = "Delete"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike // ActivityStreamsDislike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike
ActivityStreamsDislike ActivityStreamsActivity = "Dislike" ActivityStreamsDislike ActivityStreamsActivity = "Dislike"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag // ActivityStreamsFlag https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag
ActivityStreamsFlag ActivityStreamsActivity = "Flag" ActivityStreamsFlag ActivityStreamsActivity = "Flag"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow // ActivityStreamsFollow https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow
ActivityStreamsFollow ActivityStreamsActivity = "Follow" ActivityStreamsFollow ActivityStreamsActivity = "Follow"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore // ActivityStreamsIgnore https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore
ActivityStreamsIgnore ActivityStreamsActivity = "Ignore" ActivityStreamsIgnore ActivityStreamsActivity = "Ignore"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite // ActivityStreamsInvite https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite
ActivityStreamsInvite ActivityStreamsActivity = "Invite" ActivityStreamsInvite ActivityStreamsActivity = "Invite"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join // ActivityStreamsJoin https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join
ActivityStreamsJoin ActivityStreamsActivity = "Join" ActivityStreamsJoin ActivityStreamsActivity = "Join"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave // ActivityStreamsLeave https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave
ActivityStreamsLeave ActivityStreamsActivity = "Leave" ActivityStreamsLeave ActivityStreamsActivity = "Leave"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like // ActivityStreamsLike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like
ActivityStreamsLike ActivityStreamsActivity = "Like" ActivityStreamsLike ActivityStreamsActivity = "Like"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen // ActivityStreamsListen https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen
ActivityStreamsListen ActivityStreamsActivity = "Listen" ActivityStreamsListen ActivityStreamsActivity = "Listen"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move // ActivityStreamsMove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move
ActivityStreamsMove ActivityStreamsActivity = "Move" ActivityStreamsMove ActivityStreamsActivity = "Move"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer // ActivityStreamsOffer https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer
ActivityStreamsOffer ActivityStreamsActivity = "Offer" ActivityStreamsOffer ActivityStreamsActivity = "Offer"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question // ActivityStreamsQuestion https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question
ActivityStreamsQuestion ActivityStreamsActivity = "Question" ActivityStreamsQuestion ActivityStreamsActivity = "Question"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject // ActivityStreamsReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject
ActivityStreamsReject ActivityStreamsActivity = "Reject" ActivityStreamsReject ActivityStreamsActivity = "Reject"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read // ActivityStreamsRead https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read
ActivityStreamsRead ActivityStreamsActivity = "Read" ActivityStreamsRead ActivityStreamsActivity = "Read"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove // ActivityStreamsRemove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove
ActivityStreamsRemove ActivityStreamsActivity = "Remove" ActivityStreamsRemove ActivityStreamsActivity = "Remove"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject // ActivityStreamsTentativeReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject
ActivityStreamsTentativeReject ActivityStreamsActivity = "TentativeReject" ActivityStreamsTentativeReject ActivityStreamsActivity = "TentativeReject"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept // ActivityStreamsTentativeAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept
ActivityStreamsTentativeAccept ActivityStreamsActivity = "TentativeAccept" ActivityStreamsTentativeAccept ActivityStreamsActivity = "TentativeAccept"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel // ActivityStreamsTravel https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel
ActivityStreamsTravel ActivityStreamsActivity = "Travel" ActivityStreamsTravel ActivityStreamsActivity = "Travel"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo // ActivityStreamsUndo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo
ActivityStreamsUndo ActivityStreamsActivity = "Undo" ActivityStreamsUndo ActivityStreamsActivity = "Undo"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update // ActivityStreamsUpdate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update
ActivityStreamsUpdate ActivityStreamsActivity = "Update" ActivityStreamsUpdate ActivityStreamsActivity = "Update"
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view // ActivityStreamsView https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view
ActivityStreamsView ActivityStreamsActivity = "View" ActivityStreamsView ActivityStreamsActivity = "View"
) )

View file

@ -20,6 +20,7 @@ package gtsmodel
import "time" import "time"
// Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens.
type Emoji struct { type Emoji struct {
// database ID of this emoji // database ID of this emoji
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`

View file

@ -93,13 +93,13 @@ type Thumbnail struct {
type ProcessingStatus int type ProcessingStatus int
const ( const (
// ProcessingStatusReceived: the attachment has been received and is awaiting processing. No thumbnail available yet. // ProcessingStatusReceived indicates the attachment has been received and is awaiting processing. No thumbnail available yet.
ProcessingStatusReceived ProcessingStatus = 0 ProcessingStatusReceived ProcessingStatus = 0
// ProcessingStatusProcessing: the attachment is currently being processed. Thumbnail is available but full media is not. // ProcessingStatusProcessing indicates the attachment is currently being processed. Thumbnail is available but full media is not.
ProcessingStatusProcessing ProcessingStatus = 1 ProcessingStatusProcessing ProcessingStatus = 1
// ProcessingStatusProcessed: the attachment has been fully processed and is ready to be served. // ProcessingStatusProcessed indicates the attachment has been fully processed and is ready to be served.
ProcessingStatusProcessed ProcessingStatus = 2 ProcessingStatusProcessed ProcessingStatus = 2
// ProcessingStatusError: something went wrong processing the attachment and it won't be tried again--these can be deleted. // ProcessingStatusError indicates something went wrong processing the attachment and it won't be tried again--these can be deleted.
ProcessingStatusError ProcessingStatus = 666 ProcessingStatusError ProcessingStatus = 666
) )
@ -142,6 +142,8 @@ type Original struct {
Aspect float64 Aspect float64
} }
// Focus describes the 'center' of the image for display purposes.
// X and Y should each be between -1 and 1
type Focus struct { type Focus struct {
X float32 X float32
Y float32 Y float32

View file

@ -95,17 +95,17 @@ type Status struct {
type Visibility string type Visibility string
const ( const (
// This status will be visible to everyone on all timelines. // VisibilityPublic means this status will be visible to everyone on all timelines.
VisibilityPublic Visibility = "public" VisibilityPublic Visibility = "public"
// This status will be visible to everyone, but will only show on home timeline to followers, and in lists. // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
VisibilityUnlocked Visibility = "unlocked" VisibilityUnlocked Visibility = "unlocked"
// This status is viewable to followers only. // VisibilityFollowersOnly means this status is viewable to followers only.
VisibilityFollowersOnly Visibility = "followers_only" VisibilityFollowersOnly Visibility = "followers_only"
// This status is visible to mutual followers only. // VisibilityMutualsOnly means this status is visible to mutual followers only.
VisibilityMutualsOnly Visibility = "mutuals_only" VisibilityMutualsOnly Visibility = "mutuals_only"
// This status is visible only to mentioned recipients // VisibilityDirect means this status is visible only to mentioned recipients
VisibilityDirect Visibility = "direct" VisibilityDirect Visibility = "direct"
// Default visibility to use when no other setting can be found // VisibilityDefault is used when no other setting can be found
VisibilityDefault Visibility = "public" VisibilityDefault Visibility = "public"
) )

View file

@ -574,9 +574,8 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro
if err == pg.ErrNoRows { if err == pg.ErrNoRows {
blocked = false blocked = false
return blocked, nil return blocked, nil
} else {
return blocked, err
} }
return blocked, err
} }
blocked = true blocked = true
return blocked, nil return blocked, nil
@ -597,9 +596,8 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
l.Debug("target user could not be selected") l.Debug("target user could not be selected")
if err == pg.ErrNoRows { if err == pg.ErrNoRows {
return false, ErrNoEntries{} return false, ErrNoEntries{}
} else {
return false, err
} }
return false, err
} }
// if target user is disabled, not yet approved, or not confirmed then don't show the status // if target user is disabled, not yet approved, or not confirmed then don't show the status
@ -635,10 +633,9 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
if err == pg.ErrNoRows { if err == pg.ErrNoRows {
l.Debug("requesting account is local but there's no corresponding user") l.Debug("requesting account is local but there's no corresponding user")
return false, nil return false, nil
} else {
l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err)
return false, err
} }
l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err)
return false, err
} }
// okay, user exists, so make sure it has full privileges/is confirmed/approved // okay, user exists, so make sure it has full privileges/is confirmed/approved
if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
@ -751,9 +748,8 @@ func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmode
if err != nil { if err != nil {
if err == pg.ErrNoRows { if err == pg.ErrNoRows {
return false, nil return false, nil
} else {
return false, err
} }
return false, err
} }
// make sure account 2 follows account 1 // make sure account 2 follows account 1
@ -761,9 +757,8 @@ func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmode
if err != nil { if err != nil {
if err == pg.ErrNoRows { if err == pg.ErrNoRows {
return false, nil return false, nil
} else {
return false, err
} }
return false, err
} }
return f1 && f2, nil return f1 && f2, nil

View file

@ -95,12 +95,14 @@ func (d *distributor) Stop() error {
return nil return nil
} }
// FromClientAPI wraps a message that travels from the client API into the distributor
type FromClientAPI struct { type FromClientAPI struct {
APObjectType gtsmodel.ActivityStreamsObject APObjectType gtsmodel.ActivityStreamsObject
APActivityType gtsmodel.ActivityStreamsActivity APActivityType gtsmodel.ActivityStreamsActivity
Activity interface{} Activity interface{}
} }
// ToClientAPI wraps a message that travels from the distributor into the client API
type ToClientAPI struct { type ToClientAPI struct {
APObjectType gtsmodel.ActivityStreamsObject APObjectType gtsmodel.ActivityStreamsObject
APActivityType gtsmodel.ActivityStreamsActivity APActivityType gtsmodel.ActivityStreamsActivity

View file

@ -44,76 +44,260 @@ type Federator struct {
db db.DB db db.DB
} }
// AuthenticateGetInbox determines whether the request is for a GET call to the Actor's Inbox. /*
GO FED FEDERATING PROTOCOL INTERFACE
FederatingProtocol contains behaviors an application needs to satisfy for the
full ActivityPub S2S implementation to be supported by this library.
It is only required if the client application wants to support the server-to-
server, or federating, protocol.
It is passed to the library as a dependency injection from the client
application.
*/
// PostInboxRequestBodyHook callback after parsing the request body for a federated request
// to the Actor's inbox.
//
// Can be used to set contextual information based on the Activity
// received.
//
// Only called if the Federated Protocol is enabled.
//
// Warning: Neither authentication nor authorization has taken place at
// this time. Doing anything beyond setting contextual information is
// strongly discouraged.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the DelegateActor implementation must not
// write a response to the ResponseWriter as is expected that the caller
// to PostInbox will do so when handling the error.
func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
// TODO
return nil, nil
}
// AuthenticatePostInbox delegates the authentication of a POST to an
// inbox.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
func (f *Federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
// TODO
return nil, false, nil
}
// Blocked should determine whether to permit a set of actors given by
// their ids are able to interact with this particular end user due to
// being blocked or other application-specific logic.
//
// If an error is returned, it is passed back to the caller of
// PostInbox.
//
// If no error is returned, but authentication or authorization fails,
// then blocked must be true and error nil. An http.StatusForbidden
// will be written in the wresponse.
//
// Finally, if the authentication and authorization succeeds, then
// blocked must be false and error nil. The request will continue
// to be processed.
func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
// TODO
return false, nil
}
// FederatingCallbacks returns the application logic that handles
// ActivityStreams received from federating peers.
//
// Note that certain types of callbacks will be 'wrapped' with default
// behaviors supported natively by the library. Other callbacks
// compatible with streams.TypeResolver can be specified by 'other'.
//
// For example, setting the 'Create' field in the
// FederatingWrappedCallbacks lets an application dependency inject
// additional behaviors they want to take place, including the default
// behavior supplied by this library. This is guaranteed to be compliant
// with the ActivityPub Social protocol.
//
// To override the default behavior, instead supply the function in
// 'other', which does not guarantee the application will be compliant
// with the ActivityPub Social Protocol.
//
// Applications are not expected to handle every single ActivityStreams
// type and extension. The unhandled ones are passed to DefaultCallback.
func (f *Federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) {
// TODO
return pub.FederatingWrappedCallbacks{}, nil, nil
}
// DefaultCallback is called for types that go-fed can deserialize but
// are not handled by the application's callbacks returned in the
// Callbacks method.
//
// Applications are not expected to handle every single ActivityStreams
// type and extension, so the unhandled ones are passed to
// DefaultCallback.
func (f *Federator) DefaultCallback(ctx context.Context, activity pub.Activity) error {
// TODO
return nil
}
// MaxInboxForwardingRecursionDepth determines how deep to search within
// an activity to determine if inbox forwarding needs to occur.
//
// Zero or negative numbers indicate infinite recursion.
func (f *Federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
// TODO
return 0
}
// MaxDeliveryRecursionDepth determines how deep to search within
// collections owned by peers when they are targeted to receive a
// delivery.
//
// Zero or negative numbers indicate infinite recursion.
func (f *Federator) MaxDeliveryRecursionDepth(ctx context.Context) int {
// TODO
return 0
}
// FilterForwarding allows the implementation to apply business logic
// such as blocks, spam filtering, and so on to a list of potential
// Collections and OrderedCollections of recipients when inbox
// forwarding has been triggered.
//
// The activity is provided as a reference for more intelligent
// logic to be used, but the implementation must not modify it.
func (f *Federator) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) {
// TODO
return nil, nil
}
// GetInbox returns the OrderedCollection inbox of the actor for this
// context. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
//
// AuthenticateGetInbox will be called prior to this.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
func (f *Federator) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
// TODO
return nil, nil
}
/*
GOFED COMMON BEHAVIOR INTERFACE
Contains functions required for both the Social API and Federating Protocol.
It is passed to the library as a dependency injection from the client
application.
*/
// AuthenticateGetInbox delegates the authentication of a GET to an
// inbox.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
//
// If an error is returned, it is passed back to the caller of
// GetInbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
func (f *Federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { func (f *Federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
// TODO // TODO
// use context.WithValue() and context.Value() to set and get values through here // use context.WithValue() and context.Value() to set and get values through here
return nil, false, nil return nil, false, nil
} }
// AuthenticateGetOutbox determines whether the request is for a GET call to the Actor's Outbox. // AuthenticateGetOutbox delegates the authentication of a GET to an
// outbox.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
//
// If an error is returned, it is passed back to the caller of
// GetOutbox. In this case, the implementation must not write a
// response to the ResponseWriter as is expected that the client will
// do so when handling the error. The 'authenticated' is ignored.
//
// If no error is returned, but authentication or authorization fails,
// then authenticated must be false and error nil. It is expected that
// the implementation handles writing to the ResponseWriter in this
// case.
//
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
func (f *Federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { func (f *Federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
// TODO // TODO
return nil, false, nil return nil, false, nil
} }
// GetOutbox returns a proper paginated view of the Outbox for serving in a response. // GetOutbox returns the OrderedCollection inbox of the actor for this
// context. It is up to the implementation to provide the correct
// collection for the kind of authorization given in the request.
//
// AuthenticateGetOutbox will be called prior to this.
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
func (f *Federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { func (f *Federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
// TODO // TODO
return nil, nil return nil, nil
} }
// NewTransport returns a new pub.Transport for federating with peer software. // NewTransport returns a new Transport on behalf of a specific actor.
//
// The actorBoxIRI will be either the inbox or outbox of an actor who is
// attempting to do the dereferencing or delivery. Any authentication
// scheme applied on the request must be based on this actor. The
// request must contain some sort of credential of the user, such as a
// HTTP Signature.
//
// The gofedAgent passed in should be used by the Transport
// implementation in the User-Agent, as well as the application-specific
// user agent string. The gofedAgent will indicate this library's use as
// well as the library's version number.
//
// Any server-wide rate-limiting that needs to occur should happen in a
// Transport implementation. This factory function allows this to be
// created, so peer servers are not DOS'd.
//
// Any retry logic should also be handled by the Transport
// implementation.
//
// Note that the library will not maintain a long-lived pointer to the
// returned Transport so that any private credentials are able to be
// garbage collected.
func (f *Federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) { func (f *Federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) {
// TODO // TODO
return nil, nil return nil, nil
} }
func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) { /*
// TODO GOFED CLOCK INTERFACE
return nil, nil Determines the time.
} */
func (f *Federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
// TODO
return nil, false, nil
}
func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
// TODO
return false, nil
}
func (f *Federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) {
// TODO
return pub.FederatingWrappedCallbacks{}, nil, nil
}
func (f *Federator) DefaultCallback(ctx context.Context, activity pub.Activity) error {
// TODO
return nil
}
func (f *Federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
// TODO
return 0
}
func (f *Federator) MaxDeliveryRecursionDepth(ctx context.Context) int {
// TODO
return 0
}
func (f *Federator) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) {
// TODO
return nil, nil
}
func (f *Federator) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
// TODO
return nil, nil
}
// Now returns the current time.
func (f *Federator) Now() time.Time { func (f *Federator) Now() time.Time {
return time.Now() return time.Now()
} }

View file

@ -105,15 +105,16 @@ type StatusCreateRequest struct {
Language string `form:"language"` Language string `form:"language"`
} }
// Visibility denotes the visibility of this status to other users
type Visibility string type Visibility string
const ( const (
// visible to everyone // VisibilityPublic means visible to everyone
VisibilityPublic Visibility = "public" VisibilityPublic Visibility = "public"
// visible to everyone but only on home timelines or in lists // VisibilityUnlisted means visible to everyone but only on home timelines or in lists
VisibilityUnlisted Visibility = "unlisted" VisibilityUnlisted Visibility = "unlisted"
// visible to followers only // VisibilityPrivate means visible to followers only
VisibilityPrivate Visibility = "private" VisibilityPrivate Visibility = "private"
// visible only to tagged recipients // VisibilityDirect means visible only to tagged recipients
VisibilityDirect Visibility = "direct" VisibilityDirect Visibility = "direct"
) )

View file

@ -33,27 +33,27 @@ import (
) )
const ( const (
// Key for small/thumbnail versions of media // MediaSmall is the key for small/thumbnail versions of media
MediaSmall = "small" MediaSmall = "small"
// Key for original/fullsize versions of media and emoji // MediaOriginal is the key for original/fullsize versions of media and emoji
MediaOriginal = "original" MediaOriginal = "original"
// Key for static (non-animated) versions of emoji // MediaStatic is the key for static (non-animated) versions of emoji
MediaStatic = "static" MediaStatic = "static"
// Key for media attachments // MediaAttachment is the key for media attachments
MediaAttachment = "attachment" MediaAttachment = "attachment"
// Key for profile header // MediaHeader is the key for profile header requests
MediaHeader = "header" MediaHeader = "header"
// Key for profile avatar // MediaAvatar is the key for profile avatar requests
MediaAvatar = "avatar" MediaAvatar = "avatar"
// Key for emoji type // MediaEmoji is the key for emoji type requests
MediaEmoji = "emoji" MediaEmoji = "emoji"
// Maximum permitted bytes of an emoji upload (50kb) // EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb)
EmojiMaxBytes = 51200 EmojiMaxBytes = 51200
) )
// MediaHandler provides an interface for parsing, storing, and retrieving media objects like photos, videos, and gifs. // Handler provides an interface for parsing, storing, and retrieving media objects like photos, videos, and gifs.
type MediaHandler interface { type Handler interface {
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, // ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it,
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
// and then returns information to the caller about the new header. // and then returns information to the caller about the new header.
@ -77,7 +77,8 @@ type mediaHandler struct {
log *logrus.Logger log *logrus.Logger
} }
func New(config *config.Config, database db.DB, storage storage.Storage, log *logrus.Logger) MediaHandler { // New returns a new handler with the given config, db, storage, and logger
func New(config *config.Config, database db.DB, storage storage.Storage, log *logrus.Logger) Handler {
return &mediaHandler{ return &mediaHandler{
config: config, config: config,
db: database, db: database,
@ -90,6 +91,9 @@ func New(config *config.Config, database db.DB, storage storage.Storage, log *lo
INTERFACE FUNCTIONS INTERFACE FUNCTIONS
*/ */
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it,
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
// and then returns information to the caller about the new header.
func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) { func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) {
l := mh.log.WithField("func", "SetHeaderForAccountID") l := mh.log.WithField("func", "SetHeaderForAccountID")
@ -125,6 +129,9 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin
return ma, nil return ma, nil
} }
// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
// and then returns information to the caller about the attachment.
func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error) { func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error) {
contentType, err := parseContentType(attachment) contentType, err := parseContentType(attachment)
if err != nil { if err != nil {
@ -160,6 +167,9 @@ func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID stri
return nil, fmt.Errorf("content type %s not (yet) supported", contentType) return nil, fmt.Errorf("content type %s not (yet) supported", contentType)
} }
// ProcessLocalEmoji takes a new emoji and a shortcode, cleans it up, puts it in storage, and creates a new
// *gts.Emoji for it, then returns it to the caller. It's the caller's responsibility to put the returned struct
// in the database.
func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (*gtsmodel.Emoji, error) { func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (*gtsmodel.Emoji, error) {
var clean []byte var clean []byte
var err error var err error

View file

@ -64,6 +64,7 @@ func (cs *clientStore) Delete(ctx context.Context, id string) error {
return cs.db.DeleteByID(id, poc) return cs.db.DeleteByID(id, poc)
} }
// Client is a handy little wrapper for typical oauth client details
type Client struct { type Client struct {
ID string ID string
Secret string Secret string

View file

@ -46,7 +46,7 @@ const (
// of a User who has successfully passed Bearer token authorization. // of a User who has successfully passed Bearer token authorization.
// The interface returned from grabbing this key should be parsed as a *gtsmodel.Account // The interface returned from grabbing this key should be parsed as a *gtsmodel.Account
SessionAuthorizedAccount = "authorized_account" SessionAuthorizedAccount = "authorized_account"
// SessionAuthorizedAccount is the key set in the gin context for the Application // SessionAuthorizedApplication is the key set in the gin context for the Application
// of a Client who has successfully passed Bearer token authorization. // of a Client who has successfully passed Bearer token authorization.
// The interface returned from grabbing this key should be parsed as a *gtsmodel.Application // The interface returned from grabbing this key should be parsed as a *gtsmodel.Application
SessionAuthorizedApplication = "authorized_app" SessionAuthorizedApplication = "authorized_app"
@ -66,6 +66,10 @@ type s struct {
log *logrus.Logger log *logrus.Logger
} }
// Authed 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 Authed struct {
Token oauth2.TokenInfo Token oauth2.TokenInfo
Application *gtsmodel.Application Application *gtsmodel.Application
@ -208,6 +212,7 @@ func (s *s) GenerateUserAccessToken(ti oauth2.TokenInfo, clientSecret string, us
return accessToken, nil return accessToken, nil
} }
// New returns a new oauth server that implements the Server interface
func New(database db.DB, log *logrus.Logger) Server { func New(database db.DB, log *logrus.Logger) Server {
ts := newTokenStore(context.Background(), database, log) ts := newTokenStore(context.Background(), database, log)
cs := newClientStore(database) cs := newClientStore(database)

View file

@ -98,7 +98,7 @@ func (pts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error
if !ok { if !ok {
return errors.New("info param was not a models.Token") return errors.New("info param was not a models.Token")
} }
if err := pts.db.Put(OAuthTokenToPGToken(t)); err != nil { if err := pts.db.Put(TokenToPGToken(t)); err != nil {
return fmt.Errorf("error in tokenstore create: %s", err) return fmt.Errorf("error in tokenstore create: %s", err)
} }
return nil return nil
@ -130,7 +130,7 @@ func (pts *tokenStore) GetByCode(ctx context.Context, code string) (oauth2.Token
if err := pts.db.GetWhere("code", code, pgt); err != nil { if err := pts.db.GetWhere("code", code, pgt); err != nil {
return nil, err return nil, err
} }
return PGTokenToOauthToken(pgt), nil return TokenToOauthToken(pgt), nil
} }
// GetByAccess selects a token from the DB based on the Access field // GetByAccess selects a token from the DB based on the Access field
@ -144,7 +144,7 @@ func (pts *tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.T
if err := pts.db.GetWhere("access", access, pgt); err != nil { if err := pts.db.GetWhere("access", access, pgt); err != nil {
return nil, err return nil, err
} }
return PGTokenToOauthToken(pgt), nil return TokenToOauthToken(pgt), nil
} }
// GetByRefresh selects a token from the DB based on the Refresh field // GetByRefresh selects a token from the DB based on the Refresh field
@ -158,7 +158,7 @@ func (pts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2
if err := pts.db.GetWhere("refresh", refresh, pgt); err != nil { if err := pts.db.GetWhere("refresh", refresh, pgt); err != nil {
return nil, err return nil, err
} }
return PGTokenToOauthToken(pgt), nil return TokenToOauthToken(pgt), nil
} }
/* /*
@ -194,8 +194,8 @@ type Token struct {
RefreshExpiresAt time.Time `pg:"type:timestamp"` RefreshExpiresAt time.Time `pg:"type:timestamp"`
} }
// OAuthTokenToPGToken is a lil util function that takes a gotosocial token and gives back a token for inserting into postgres // TokenToPGToken is a lil util function that takes a gotosocial token and gives back a token for inserting into postgres
func OAuthTokenToPGToken(tkn *models.Token) *Token { func TokenToPGToken(tkn *models.Token) *Token {
now := time.Now() now := time.Now()
// For the following, we want to make sure we're not adding a time.Now() to an *empty* ExpiresIn, otherwise that's // For the following, we want to make sure we're not adding a time.Now() to an *empty* ExpiresIn, otherwise that's
@ -236,8 +236,8 @@ func OAuthTokenToPGToken(tkn *models.Token) *Token {
} }
} }
// PGTokenToOauthToken is a lil util function that takes a postgres token and gives back a gotosocial token // TokenToOauthToken is a lil util function that takes a postgres token and gives back a gotosocial token
func PGTokenToOauthToken(pgt *Token) *models.Token { func TokenToOauthToken(pgt *Token) *models.Token {
now := time.Now() now := time.Now()
return &models.Token{ return &models.Token{

View file

@ -128,16 +128,19 @@ func ValidateSignUpReason(reason string, reasonRequired bool) error {
return nil return nil
} }
// ValidateDisplayName checks that a requested display name is valid
func ValidateDisplayName(displayName string) error { func ValidateDisplayName(displayName string) error {
// TODO: add some validation logic here -- length, characters, etc // TODO: add some validation logic here -- length, characters, etc
return nil return nil
} }
// ValidateNote checks that a given profile/account note/bio is valid
func ValidateNote(note string) error { func ValidateNote(note string) error {
// TODO: add some validation logic here -- length, characters, etc // TODO: add some validation logic here -- length, characters, etc
return nil return nil
} }
// ValidatePrivacy checks that the desired privacy setting is valid
func ValidatePrivacy(privacy string) error { func ValidatePrivacy(privacy string) error {
// TODO: add some validation logic here -- length, characters, etc // TODO: add some validation logic here -- length, characters, etc
return nil return nil

View file

@ -20,6 +20,7 @@ package testrig
import "github.com/superseriousbusiness/gotosocial/internal/distributor" import "github.com/superseriousbusiness/gotosocial/internal/distributor"
// NewTestDistributor returns a Distributor suitable for testing purposes
func NewTestDistributor() distributor.Distributor { func NewTestDistributor() distributor.Distributor {
return distributor.New(NewTestLog()) return distributor.New(NewTestLog())
} }

View file

@ -26,6 +26,6 @@ import (
// NewTestMediaHandler returns a media handler with the default test config, the default test logger, // NewTestMediaHandler returns a media handler with the default test config, the default test logger,
// and the given db and storage. // and the given db and storage.
func NewTestMediaHandler(db db.DB, storage storage.Storage) media.MediaHandler { func NewTestMediaHandler(db db.DB, storage storage.Storage) media.Handler {
return media.New(NewTestConfig(), db, storage, NewTestLog()) return media.New(NewTestConfig(), db, storage, NewTestLog())
} }

View file

@ -20,6 +20,7 @@ package testrig
import "github.com/superseriousbusiness/gotosocial/internal/router" import "github.com/superseriousbusiness/gotosocial/internal/router"
// NewTestRouter returns a Router suitable for testing
func NewTestRouter() router.Router { func NewTestRouter() router.Router {
r, err := router.New(NewTestConfig(), NewTestLog()) r, err := router.New(NewTestConfig(), NewTestLog())
if err != nil { if err != nil {

View file

@ -36,15 +36,15 @@ func NewTestStorage() storage.Storage {
// StandardStorageSetup populates the storage with standard test entries from the given directory. // StandardStorageSetup populates the storage with standard test entries from the given directory.
func StandardStorageSetup(s storage.Storage, relativePath string) { func StandardStorageSetup(s storage.Storage, relativePath string) {
storedA := NewTestStoredAttachments() storedA := newTestStoredAttachments()
a := NewTestAttachments() a := NewTestAttachments()
for k, paths := range storedA { for k, paths := range storedA {
attachmentInfo, ok := a[k] attachmentInfo, ok := a[k]
if !ok { if !ok {
panic(fmt.Errorf("key %s not found in test attachments", k)) panic(fmt.Errorf("key %s not found in test attachments", k))
} }
filenameOriginal := paths.original filenameOriginal := paths.Original
filenameSmall := paths.small filenameSmall := paths.Small
pathOriginal := attachmentInfo.File.Path pathOriginal := attachmentInfo.File.Path
pathSmall := attachmentInfo.Thumbnail.Path pathSmall := attachmentInfo.Thumbnail.Path
bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal)) bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal))
@ -63,15 +63,15 @@ func StandardStorageSetup(s storage.Storage, relativePath string) {
} }
} }
storedE := NewTestStoredEmoji() storedE := newTestStoredEmoji()
e := NewTestEmojis() e := NewTestEmojis()
for k, paths := range storedE { for k, paths := range storedE {
emojiInfo, ok := e[k] emojiInfo, ok := e[k]
if !ok { if !ok {
panic(fmt.Errorf("key %s not found in test emojis", k)) panic(fmt.Errorf("key %s not found in test emojis", k))
} }
filenameOriginal := paths.original filenameOriginal := paths.Original
filenameStatic := paths.static filenameStatic := paths.Static
pathOriginal := emojiInfo.ImagePath pathOriginal := emojiInfo.ImagePath
pathStatic := emojiInfo.ImageStaticPath pathStatic := emojiInfo.ImageStaticPath
bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal)) bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal))

View file

@ -700,39 +700,39 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
} }
type filenames struct { type filenames struct {
original string Original string
small string Small string
static string Static string
} }
// NewTestStoredAttachments returns a map of filenames, keyed according to which attachment they pertain to. // newTestStoredAttachments returns a map of filenames, keyed according to which attachment they pertain to.
func NewTestStoredAttachments() map[string]filenames { func newTestStoredAttachments() map[string]filenames {
return map[string]filenames{ return map[string]filenames{
"admin_account_status_1_attachment_1": { "admin_account_status_1_attachment_1": {
original: "welcome-original.jpeg", Original: "welcome-original.jpeg",
small: "welcome-small.jpeg", Small: "welcome-small.jpeg",
}, },
"local_account_1_status_4_attachment_1": { "local_account_1_status_4_attachment_1": {
original: "trent-original.gif", Original: "trent-original.gif",
small: "trent-small.jpeg", Small: "trent-small.jpeg",
}, },
"local_account_1_unattached_1": { "local_account_1_unattached_1": {
original: "ohyou-original.jpeg", Original: "ohyou-original.jpeg",
small: "ohyou-small.jpeg", Small: "ohyou-small.jpeg",
}, },
"local_account_1_avatar": { "local_account_1_avatar": {
original: "zork-original.jpeg", Original: "zork-original.jpeg",
small: "zork-small.jpeg", Small: "zork-small.jpeg",
}, },
} }
} }
// NewtestStoredEmoji returns a map of filenames, keyed according to which emoji they pertain to // newTestStoredEmoji returns a map of filenames, keyed according to which emoji they pertain to
func NewTestStoredEmoji() map[string]filenames { func newTestStoredEmoji() map[string]filenames {
return map[string]filenames{ return map[string]filenames{
"rainbow": { "rainbow": {
original: "rainbow-original.png", Original: "rainbow-original.png",
static: "rainbow-static.png", Static: "rainbow-static.png",
}, },
} }
} }