diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index 091678b29..7f185d8d9 100644 --- a/cmd/gotosocial/main.go +++ b/cmd/gotosocial/main.go @@ -153,8 +153,8 @@ func main() { }, &cli.StringFlag{ Name: flagNames.StorageBasePath, - Usage: "Full path to an already-created directory where gts should store/retrieve media files", - Value: "/opt/gotosocial", + Usage: "Full path to an already-created directory where gts should store/retrieve media files. Subfolders will be created within this dir.", + Value: "/gotosocial/storage/media", EnvVars: []string{envNames.StorageBasePath}, }, &cli.StringFlag{ diff --git a/internal/apimodule/account/account.go b/internal/apimodule/account/account.go index 2d9ddbb72..a94169eb2 100644 --- a/internal/apimodule/account/account.go +++ b/internal/apimodule/account/account.go @@ -28,7 +28,9 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/apimodule" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" + "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" @@ -43,21 +45,23 @@ const ( ) type accountModule struct { - config *config.Config - db db.DB - oauthServer oauth.Server - mediaHandler media.MediaHandler - log *logrus.Logger + config *config.Config + db db.DB + oauthServer oauth.Server + mediaHandler media.MediaHandler + mastoConverter mastotypes.Converter + log *logrus.Logger } // New returns a new account module -func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.MediaHandler, log *logrus.Logger) apimodule.ClientAPIModule { +func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule { return &accountModule{ - config: config, - db: db, - oauthServer: oauthServer, - mediaHandler: mediaHandler, - log: log, + config: config, + db: db, + oauthServer: oauthServer, + mediaHandler: mediaHandler, + mastoConverter: mastoConverter, + log: log, } } @@ -70,14 +74,14 @@ func (m *accountModule) Route(r router.Router) error { func (m *accountModule) CreateTables(db db.DB) error { models := []interface{}{ - &model.User{}, - &model.Account{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.Status{}, - &model.Application{}, - &model.EmailDomainBlock{}, - &model.MediaAttachment{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.Status{}, + >smodel.Application{}, + >smodel.EmailDomainBlock{}, + >smodel.MediaAttachment{}, } for _, m := range models { diff --git a/internal/apimodule/account/accountcreate.go b/internal/apimodule/account/accountcreate.go index 58b98c0e4..266d820af 100644 --- a/internal/apimodule/account/accountcreate.go +++ b/internal/apimodule/account/accountcreate.go @@ -27,10 +27,10 @@ import ( "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" "github.com/superseriousbusiness/oauth2/v4" ) @@ -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. // It then returns a token to the caller, for use with the new account, as per the // spec here: https://docs.joinmastodon.org/methods/accounts/ -func (m *accountModule) accountCreate(form *mastotypes.AccountCreateRequest, signUpIP net.IP, token oauth2.TokenInfo, app *model.Application) (*mastotypes.Token, error) { +func (m *accountModule) accountCreate(form *mastotypes.AccountCreateRequest, signUpIP net.IP, token oauth2.TokenInfo, app *gtsmodel.Application) (*mastotypes.Token, error) { l := m.log.WithField("func", "accountCreate") // don't store a reason if we don't require one diff --git a/internal/apimodule/account/accountcreate_test.go b/internal/apimodule/account/accountcreate_test.go index d5470b919..8677e3573 100644 --- a/internal/apimodule/account/accountcreate_test.go +++ b/internal/apimodule/account/accountcreate_test.go @@ -41,11 +41,13 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" + mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" + "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/storage" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/models" oauthmodels "github.com/superseriousbusiness/oauth2/v4/models" @@ -56,12 +58,13 @@ type AccountCreateTestSuite struct { suite.Suite config *config.Config log *logrus.Logger - testAccountLocal *model.Account - testApplication *model.Application + testAccountLocal *gtsmodel.Account + testApplication *gtsmodel.Application testToken oauth2.TokenInfo mockOauthServer *oauth.MockServer mockStorage *storage.MockStorage mediaHandler media.MediaHandler + mastoConverter mastotypes.Converter db db.DB accountModule *accountModule newUserFormHappyPath url.Values @@ -78,13 +81,13 @@ func (suite *AccountCreateTestSuite) SetupSuite() { log.SetLevel(logrus.TraceLevel) suite.log = log - suite.testAccountLocal = &model.Account{ + suite.testAccountLocal = >smodel.Account{ ID: uuid.NewString(), Username: "test_user", } // can use this test application throughout - suite.testApplication = &model.Application{ + suite.testApplication = >smodel.Application{ ID: "weeweeeeeeeeeeeeee", Name: "a test application", Website: "https://some-application-website.com", @@ -158,8 +161,10 @@ func (suite *AccountCreateTestSuite) SetupSuite() { // set a media handler because some handlers (eg update credentials) need to upload media (new header/avatar) suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log) + suite.mastoConverter = mastotypes.New(suite.config, suite.db) + // and finally here's the thing we're actually testing! - suite.accountModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.log).(*accountModule) + suite.accountModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*accountModule) } func (suite *AccountCreateTestSuite) TearDownSuite() { @@ -172,14 +177,14 @@ func (suite *AccountCreateTestSuite) TearDownSuite() { func (suite *AccountCreateTestSuite) SetupTest() { // create all the tables we might need in thie suite models := []interface{}{ - &model.User{}, - &model.Account{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.Status{}, - &model.Application{}, - &model.EmailDomainBlock{}, - &model.MediaAttachment{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.Status{}, + >smodel.Application{}, + >smodel.EmailDomainBlock{}, + >smodel.MediaAttachment{}, } for _, m := range models { if err := suite.db.CreateTable(m); err != nil { @@ -210,14 +215,14 @@ func (suite *AccountCreateTestSuite) TearDownTest() { // remove all the tables we might have used so it's clear for the next test models := []interface{}{ - &model.User{}, - &model.Account{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.Status{}, - &model.Application{}, - &model.EmailDomainBlock{}, - &model.MediaAttachment{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.Status{}, + >smodel.Application{}, + >smodel.EmailDomainBlock{}, + >smodel.MediaAttachment{}, } for _, m := range models { if err := suite.db.DropTable(m); err != nil { @@ -259,7 +264,7 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() { defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - t := &mastotypes.Token{} + t := &mastomodel.Token{} err = json.Unmarshal(b, t) assert.NoError(suite.T(), err) assert.Equal(suite.T(), "we're authorized now!", t.AccessToken) @@ -267,7 +272,7 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() { // check new account // 1. we should be able to get the new account from the db - acct := &model.Account{} + acct := >smodel.Account{} err = suite.db.GetWhere("username", "test_user", acct) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), acct) @@ -288,7 +293,7 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() { // check new user // 1. we should be able to get the new user from the db - usr := &model.User{} + usr := >smodel.User{} err = suite.db.GetWhere("unconfirmed_email", suite.newUserFormHappyPath.Get("email"), usr) assert.Nil(suite.T(), err) assert.NotNil(suite.T(), usr) diff --git a/internal/apimodule/account/accountget.go b/internal/apimodule/account/accountget.go index 5ee93386d..cd4aed22e 100644 --- a/internal/apimodule/account/accountget.go +++ b/internal/apimodule/account/accountget.go @@ -23,7 +23,7 @@ import ( "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" ) // accountGetHandler serves the account information held by the server in response to a GET @@ -37,7 +37,7 @@ func (m *accountModule) accountGETHandler(c *gin.Context) { return } - targetAccount := &model.Account{} + targetAccount := >smodel.Account{} if err := m.db.GetByID(targetAcctID, targetAccount); err != nil { if _, ok := err.(db.ErrNoEntries); ok { c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"}) @@ -47,7 +47,7 @@ func (m *accountModule) accountGETHandler(c *gin.Context) { return } - acctInfo, err := m.db.AccountToMastoPublic(targetAccount) + acctInfo, err := m.mastoConverter.AccountToMastoPublic(targetAccount) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/internal/apimodule/account/accountupdate.go b/internal/apimodule/account/accountupdate.go index 6686d3a50..ba245b929 100644 --- a/internal/apimodule/account/accountupdate.go +++ b/internal/apimodule/account/accountupdate.go @@ -27,10 +27,10 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" ) // accountUpdateCredentialsPATCHHandler allows a user to modify their account/profile settings. @@ -67,7 +67,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { } if form.Discoverable != nil { - if err := m.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil { l.Debugf("error updating discoverable: %s", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -75,7 +75,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { } if form.Bot != nil { - if err := m.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil { l.Debugf("error updating bot: %s", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -87,7 +87,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := m.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -98,7 +98,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := m.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, >smodel.Account{}); err != nil { l.Debugf("error updating note: %s", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -126,7 +126,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { } if form.Locked != nil { - if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -138,14 +138,14 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := m.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } if form.Source.Sensitive != nil { - if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -156,7 +156,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := m.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, &model.Account{}); err != nil { + if err := m.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -168,14 +168,14 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { // } // fetch the account with all updated values set - updatedAccount := &model.Account{} + updatedAccount := >smodel.Account{} if err := m.db.GetByID(authed.Account.ID, updatedAccount); err != nil { l.Debugf("could not fetch updated account %s: %s", authed.Account.ID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - acctSensitive, err := m.db.AccountToMastoSensitive(updatedAccount) + acctSensitive, err := m.mastoConverter.AccountToMastoSensitive(updatedAccount) if err != nil { l.Tracef("could not convert account into mastosensitive account: %s", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -195,7 +195,7 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) { // UpdateAccountAvatar does the dirty work of checking the avatar part of an account update form, // parsing and checking the image, and doing the necessary updates in the database for this to become // the account's new avatar image. -func (m *accountModule) UpdateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*model.MediaAttachment, error) { +func (m *accountModule) UpdateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { var err error if int(avatar.Size) > m.config.MediaConfig.MaxImageSize { err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, m.config.MediaConfig.MaxImageSize) @@ -228,7 +228,7 @@ func (m *accountModule) UpdateAccountAvatar(avatar *multipart.FileHeader, accoun // UpdateAccountHeader does the dirty work of checking the header part of an account update form, // parsing and checking the image, and doing the necessary updates in the database for this to become // the account's new header image. -func (m *accountModule) UpdateAccountHeader(header *multipart.FileHeader, accountID string) (*model.MediaAttachment, error) { +func (m *accountModule) UpdateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) { var err error if int(header.Size) > m.config.MediaConfig.MaxImageSize { err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, m.config.MediaConfig.MaxImageSize) diff --git a/internal/apimodule/account/accountupdate_test.go b/internal/apimodule/account/accountupdate_test.go index 651b4d29d..7ca2190d8 100644 --- a/internal/apimodule/account/accountupdate_test.go +++ b/internal/apimodule/account/accountupdate_test.go @@ -39,7 +39,8 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/storage" @@ -52,12 +53,13 @@ type AccountUpdateTestSuite struct { suite.Suite config *config.Config log *logrus.Logger - testAccountLocal *model.Account - testApplication *model.Application + testAccountLocal *gtsmodel.Account + testApplication *gtsmodel.Application testToken oauth2.TokenInfo mockOauthServer *oauth.MockServer mockStorage *storage.MockStorage mediaHandler media.MediaHandler + mastoConverter mastotypes.Converter db db.DB accountModule *accountModule newUserFormHappyPath url.Values @@ -74,13 +76,13 @@ func (suite *AccountUpdateTestSuite) SetupSuite() { log.SetLevel(logrus.TraceLevel) suite.log = log - suite.testAccountLocal = &model.Account{ + suite.testAccountLocal = >smodel.Account{ ID: uuid.NewString(), Username: "test_user", } // can use this test application throughout - suite.testApplication = &model.Application{ + suite.testApplication = >smodel.Application{ ID: "weeweeeeeeeeeeeeee", Name: "a test application", Website: "https://some-application-website.com", @@ -154,8 +156,10 @@ func (suite *AccountUpdateTestSuite) SetupSuite() { // set a media handler because some handlers (eg update credentials) need to upload media (new header/avatar) suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log) + suite.mastoConverter = mastotypes.New(suite.config, suite.db) + // and finally here's the thing we're actually testing! - suite.accountModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.log).(*accountModule) + suite.accountModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*accountModule) } func (suite *AccountUpdateTestSuite) TearDownSuite() { @@ -168,14 +172,14 @@ func (suite *AccountUpdateTestSuite) TearDownSuite() { func (suite *AccountUpdateTestSuite) SetupTest() { // create all the tables we might need in thie suite models := []interface{}{ - &model.User{}, - &model.Account{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.Status{}, - &model.Application{}, - &model.EmailDomainBlock{}, - &model.MediaAttachment{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.Status{}, + >smodel.Application{}, + >smodel.EmailDomainBlock{}, + >smodel.MediaAttachment{}, } for _, m := range models { if err := suite.db.CreateTable(m); err != nil { @@ -206,14 +210,14 @@ func (suite *AccountUpdateTestSuite) TearDownTest() { // remove all the tables we might have used so it's clear for the next test models := []interface{}{ - &model.User{}, - &model.Account{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.Status{}, - &model.Application{}, - &model.EmailDomainBlock{}, - &model.MediaAttachment{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.Status{}, + >smodel.Application{}, + >smodel.EmailDomainBlock{}, + >smodel.MediaAttachment{}, } for _, m := range models { if err := suite.db.DropTable(m); err != nil { diff --git a/internal/apimodule/account/accountverify.go b/internal/apimodule/account/accountverify.go index fe8d24b22..584ab6122 100644 --- a/internal/apimodule/account/accountverify.go +++ b/internal/apimodule/account/accountverify.go @@ -38,7 +38,7 @@ func (m *accountModule) accountVerifyGETHandler(c *gin.Context) { } l.Tracef("retrieved account %+v, converting to mastosensitive...", authed.Account.ID) - acctSensitive, err := m.db.AccountToMastoSensitive(authed.Account) + acctSensitive, err := m.mastoConverter.AccountToMastoSensitive(authed.Account) if err != nil { l.Tracef("could not convert account into mastosensitive account: %s", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) diff --git a/internal/apimodule/app/app.go b/internal/apimodule/app/app.go index 534f4cd3e..08292acd1 100644 --- a/internal/apimodule/app/app.go +++ b/internal/apimodule/app/app.go @@ -25,7 +25,8 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/apimodule" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" ) @@ -33,17 +34,19 @@ import ( const appsPath = "/api/v1/apps" type appModule struct { - server oauth.Server - db db.DB - log *logrus.Logger + server oauth.Server + db db.DB + mastoConverter mastotypes.Converter + log *logrus.Logger } // 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, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule { return &appModule{ - server: srv, - db: db, - log: log, + server: srv, + db: db, + mastoConverter: mastoConverter, + log: log, } } @@ -57,9 +60,9 @@ func (m *appModule) CreateTables(db db.DB) error { models := []interface{}{ &oauth.Client{}, &oauth.Token{}, - &model.User{}, - &model.Account{}, - &model.Application{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Application{}, } for _, m := range models { diff --git a/internal/apimodule/app/appcreate.go b/internal/apimodule/app/appcreate.go index 1adcef573..ec52a9d37 100644 --- a/internal/apimodule/app/appcreate.go +++ b/internal/apimodule/app/appcreate.go @@ -24,9 +24,9 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" ) // appsPOSTHandler should be served at https://example.org/api/v1/apps @@ -78,7 +78,7 @@ func (m *appModule) appsPOSTHandler(c *gin.Context) { vapidKey := uuid.NewString() // generate the application to put in the database - app := &model.Application{ + app := >smodel.Application{ Name: form.ClientName, Website: form.Website, RedirectURI: form.RedirectURIs, @@ -108,6 +108,12 @@ func (m *appModule) appsPOSTHandler(c *gin.Context) { return } + mastoApp, err := m.mastoConverter.AppToMastoSensitive(app) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + // done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/ - c.JSON(http.StatusOK, app.ToMastoSensitive()) + c.JSON(http.StatusOK, mastoApp) } diff --git a/internal/apimodule/auth/auth.go b/internal/apimodule/auth/auth.go index 3a85a4364..b70adeb43 100644 --- a/internal/apimodule/auth/auth.go +++ b/internal/apimodule/auth/auth.go @@ -31,7 +31,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/apimodule" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" ) @@ -75,9 +75,9 @@ func (m *authModule) CreateTables(db db.DB) error { models := []interface{}{ &oauth.Client{}, &oauth.Token{}, - &model.User{}, - &model.Account{}, - &model.Application{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Application{}, } for _, m := range models { diff --git a/internal/apimodule/auth/auth_test.go b/internal/apimodule/auth/auth_test.go index 0ec9b4a41..351c086e4 100644 --- a/internal/apimodule/auth/auth_test.go +++ b/internal/apimodule/auth/auth_test.go @@ -29,7 +29,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" "golang.org/x/crypto/bcrypt" @@ -39,9 +39,9 @@ type AuthTestSuite struct { suite.Suite oauthServer oauth.Server db db.DB - testAccount *model.Account - testApplication *model.Application - testUser *model.User + testAccount *gtsmodel.Account + testApplication *gtsmodel.Application + testUser *gtsmodel.User testClient *oauth.Client config *config.Config } @@ -75,11 +75,11 @@ func (suite *AuthTestSuite) SetupSuite() { acctID := uuid.NewString() - suite.testAccount = &model.Account{ + suite.testAccount = >smodel.Account{ ID: acctID, Username: "test_user", } - suite.testUser = &model.User{ + suite.testUser = >smodel.User{ EncryptedPassword: string(encryptedPassword), Email: "user@example.org", AccountID: acctID, @@ -89,7 +89,7 @@ func (suite *AuthTestSuite) SetupSuite() { Secret: "some-secret", Domain: fmt.Sprintf("%s://%s", c.Protocol, c.Host), } - suite.testApplication = &model.Application{ + suite.testApplication = >smodel.Application{ Name: "a test application", Website: "https://some-application-website.com", RedirectURI: "http://localhost:8080", @@ -115,9 +115,9 @@ func (suite *AuthTestSuite) SetupTest() { models := []interface{}{ &oauth.Client{}, &oauth.Token{}, - &model.User{}, - &model.Account{}, - &model.Application{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Application{}, } for _, m := range models { @@ -148,9 +148,9 @@ func (suite *AuthTestSuite) TearDownTest() { models := []interface{}{ &oauth.Client{}, &oauth.Token{}, - &model.User{}, - &model.Account{}, - &model.Application{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Application{}, } for _, m := range models { if err := suite.db.DropTable(m); err != nil { diff --git a/internal/apimodule/auth/authorize.go b/internal/apimodule/auth/authorize.go index 4a27cc20e..bf525e09e 100644 --- a/internal/apimodule/auth/authorize.go +++ b/internal/apimodule/auth/authorize.go @@ -27,8 +27,8 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/db/model" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" ) // authorizeGETHandler should be served as GET at https://example.org/oauth/authorize @@ -57,7 +57,7 @@ func (m *authModule) authorizeGETHandler(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session"}) return } - app := &model.Application{ + app := >smodel.Application{ ClientID: clientID, } if err := m.db.GetWhere("client_id", app.ClientID, app); err != nil { @@ -66,7 +66,7 @@ func (m *authModule) authorizeGETHandler(c *gin.Context) { } // we can also use the userid of the user to fetch their username from the db to greet them nicely <3 - user := &model.User{ + user := >smodel.User{ ID: userID, } if err := m.db.GetByID(user.ID, user); err != nil { @@ -74,7 +74,7 @@ func (m *authModule) authorizeGETHandler(c *gin.Context) { return } - acct := &model.Account{ + acct := >smodel.Account{ ID: user.AccountID, } diff --git a/internal/apimodule/auth/middleware.go b/internal/apimodule/auth/middleware.go index 32fc24d52..4ca1f47a2 100644 --- a/internal/apimodule/auth/middleware.go +++ b/internal/apimodule/auth/middleware.go @@ -20,7 +20,7 @@ package auth import ( "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -46,7 +46,7 @@ func (m *authModule) oauthTokenMiddleware(c *gin.Context) { l.Tracef("authenticated user %s with bearer token, scope is %s", uid, ti.GetScope()) // fetch user's and account for this user id - user := &model.User{} + user := >smodel.User{} if err := m.db.GetByID(uid, user); err != nil || user == nil { l.Warnf("no user found for validated uid %s", uid) return @@ -54,7 +54,7 @@ func (m *authModule) oauthTokenMiddleware(c *gin.Context) { c.Set(oauth.SessionAuthorizedUser, user) l.Tracef("set gin context %s to %+v", oauth.SessionAuthorizedUser, user) - acct := &model.Account{} + acct := >smodel.Account{} if err := m.db.GetByID(user.AccountID, acct); err != nil || acct == nil { l.Warnf("no account found for validated user %s", uid) return @@ -66,7 +66,7 @@ func (m *authModule) oauthTokenMiddleware(c *gin.Context) { // check for application token if cid := ti.GetClientID(); cid != "" { l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope()) - app := &model.Application{} + app := >smodel.Application{} if err := m.db.GetWhere("client_id", cid, app); err != nil { l.Tracef("no app found for client %s", cid) } diff --git a/internal/apimodule/auth/signin.go b/internal/apimodule/auth/signin.go index 34146cbfc..a6994c90e 100644 --- a/internal/apimodule/auth/signin.go +++ b/internal/apimodule/auth/signin.go @@ -24,7 +24,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "golang.org/x/crypto/bcrypt" ) @@ -84,7 +84,7 @@ func (m *authModule) validatePassword(email string, password string) (userid str } // first we select the user from the database based on email address, bail if no user found for that email - gtsUser := &model.User{} + gtsUser := >smodel.User{} if err := m.db.GetWhere("email", email, gtsUser); err != nil { l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) diff --git a/internal/apimodule/fileserver/fileserver.go b/internal/apimodule/fileserver/fileserver.go index bbafff76f..c82c9bbf1 100644 --- a/internal/apimodule/fileserver/fileserver.go +++ b/internal/apimodule/fileserver/fileserver.go @@ -7,7 +7,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/apimodule" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/storage" ) @@ -44,14 +44,14 @@ func (m *fileServer) Route(s router.Router) error { func (m *fileServer) CreateTables(db db.DB) error { models := []interface{}{ - &model.User{}, - &model.Account{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.Status{}, - &model.Application{}, - &model.EmailDomainBlock{}, - &model.MediaAttachment{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.Status{}, + >smodel.Application{}, + >smodel.EmailDomainBlock{}, + >smodel.MediaAttachment{}, } for _, m := range models { diff --git a/internal/apimodule/status/status.go b/internal/apimodule/status/status.go index a6b97fe21..02ae77f7c 100644 --- a/internal/apimodule/status/status.go +++ b/internal/apimodule/status/status.go @@ -25,8 +25,9 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/apimodule" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/distributor" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" @@ -51,22 +52,24 @@ const ( ) type statusModule struct { - config *config.Config - db db.DB - oauthServer oauth.Server - mediaHandler media.MediaHandler - distributor distributor.Distributor - log *logrus.Logger + config *config.Config + db db.DB + oauthServer oauth.Server + mediaHandler media.MediaHandler + mastoConverter mastotypes.Converter + distributor distributor.Distributor + log *logrus.Logger } // New returns a new account module -func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.MediaHandler, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule { +func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule { return &statusModule{ - config: config, - db: db, - mediaHandler: mediaHandler, - distributor: distributor, - log: log, + config: config, + db: db, + mediaHandler: mediaHandler, + mastoConverter: mastoConverter, + distributor: distributor, + log: log, } } @@ -79,17 +82,17 @@ func (m *statusModule) Route(r router.Router) error { func (m *statusModule) CreateTables(db db.DB) error { models := []interface{}{ - &model.User{}, - &model.Account{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.Status{}, - &model.Application{}, - &model.EmailDomainBlock{}, - &model.MediaAttachment{}, - &model.Emoji{}, - &model.Tag{}, - &model.Mention{}, + >smodel.User{}, + >smodel.Account{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.Status{}, + >smodel.Application{}, + >smodel.EmailDomainBlock{}, + >smodel.MediaAttachment{}, + >smodel.Emoji{}, + >smodel.Tag{}, + >smodel.Mention{}, } for _, m := range models { diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go index 0981caaf6..5687bacbf 100644 --- a/internal/apimodule/status/statuscreate.go +++ b/internal/apimodule/status/statuscreate.go @@ -28,11 +28,11 @@ import ( "github.com/google/uuid" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/distributor" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" ) type advancedStatusCreateForm struct { @@ -42,7 +42,7 @@ type advancedStatusCreateForm struct { type advancedVisibilityFlagsForm struct { // The gotosocial visibility model - VisibilityAdvanced *model.Visibility `form:"visibility_advanced"` + VisibilityAdvanced *gtsmodel.Visibility `form:"visibility_advanced"` // This status will be federated beyond the local timeline(s) Federated *bool `form:"federated"` // This status can be boosted/reblogged @@ -96,7 +96,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { thisStatusID := uuid.NewString() thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID) thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID) - newStatus := &model.Status{ + newStatus := >smodel.Status{ ID: thisStatusID, URI: thisStatusURI, URL: thisStatusURL, @@ -106,7 +106,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { Local: true, AccountID: authed.Account.ID, ContentWarning: form.SpoilerText, - ActivityStreamsType: model.ActivityStreamsNote, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, Sensitive: form.Sensitive, Language: form.Language, } @@ -135,16 +135,23 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { return } - // convert mentions to *model.Mention + // convert mentions to *gtsmodel.Mention menchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), authed.Account.ID, thisStatusID) if err != nil { l.Debugf("error generating mentions from status: %s", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating mentions from status"}) return } + for _, menchie := range menchies { + if err := m.db.Put(menchie); err != nil { + l.Debugf("error putting mentions in db: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error while generating mentions from status"}) + return + } + } newStatus.Mentions = menchies - // convert tags to *model.Tag + // convert tags to *gtsmodel.Tag tags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), authed.Account.ID, thisStatusID) if err != nil { l.Debugf("error generating hashtags from status: %s", err) @@ -153,7 +160,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { } newStatus.Tags = tags - // convert emojis to *model.Emoji + // convert emojis to *gtsmodel.Emoji emojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), authed.Account.ID, thisStatusID) if err != nil { l.Debugf("error generating emojis from status: %s", err) @@ -170,33 +177,64 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { // pass to the distributor to take care of side effects -- federation, mentions, updating metadata, etc, etc m.distributor.FromClientAPI() <- distributor.FromClientAPI{ - APObjectType: model.ActivityStreamsNote, - APActivityType: model.ActivityStreamsCreate, + APObjectType: gtsmodel.ActivityStreamsNote, + APActivityType: gtsmodel.ActivityStreamsCreate, Activity: newStatus, } - // return populated status to submitter - mastoAccount, err := m.db.AccountToMastoPublic(authed.Account) + // now we need to build up the mastodon-style status object to return to the submitter + + mastoVis := util.ParseMastoVisFromGTSVis(newStatus.Visibility) + + mastoAccount, err := m.mastoConverter.AccountToMastoPublic(authed.Account) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + + mastoAttachments := []mastotypes.Attachment{} + for _, a := range newStatus.Attachments { + ma, err := m.mastoConverter.AttachmentToMasto(a) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + mastoAttachments = append(mastoAttachments, ma) + } + + mastoMentions := []mastotypes.Mention{} + for _, gtsm := range newStatus.Mentions { + mm, err := m.mastoConverter.MentionToMasto(gtsm) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + mastoMentions = append(mastoMentions, mm) + } + + mastoApplication, err := m.mastoConverter.AppToMastoPublic(authed.Application) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + mastoStatus := &mastotypes.Status{ - ID: newStatus.ID, - CreatedAt: newStatus.CreatedAt.Format(time.RFC3339), - InReplyToID: newStatus.InReplyToID, - // InReplyToAccountID: newStatus.ReplyToAccount.ID, - Sensitive: newStatus.Sensitive, - SpoilerText: newStatus.ContentWarning, - Visibility: util.ParseMastoVisFromGTSVis(newStatus.Visibility), - Language: newStatus.Language, - URI: newStatus.URI, - URL: newStatus.URL, - Content: newStatus.Content, - Application: authed.Application.ToMastoPublic(), - Account: mastoAccount, - // MediaAttachments: , - Text: form.Status, + ID: newStatus.ID, + CreatedAt: newStatus.CreatedAt.Format(time.RFC3339), + InReplyToID: newStatus.InReplyToID, + InReplyToAccountID: newStatus.InReplyToAccountID, + Sensitive: newStatus.Sensitive, + SpoilerText: newStatus.ContentWarning, + Visibility: mastoVis, + Language: newStatus.Language, + URI: newStatus.URI, + URL: newStatus.URL, + Content: newStatus.Content, + Application: mastoApplication, + Account: mastoAccount, + MediaAttachments: mastoAttachments, + Mentions: mastoMentions, + Text: form.Status, } c.JSON(http.StatusOK, mastoStatus) } @@ -255,16 +293,16 @@ func validateCreateStatus(form *advancedStatusCreateForm, config *config.Statuse return nil } -func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis model.Visibility, status *model.Status) error { +func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error { // by default all flags are set to true - gtsAdvancedVis := &model.VisibilityAdvanced{ + gtsAdvancedVis := >smodel.VisibilityAdvanced{ Federated: true, Boostable: true, Replyable: true, Likeable: true, } - var gtsBasicVis model.Visibility + var gtsBasicVis gtsmodel.Visibility // Advanced takes priority if it's set. // If it's not set, take whatever masto visibility is set. // If *that's* not set either, then just take the account default. @@ -277,10 +315,10 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis model.Vis } switch gtsBasicVis { - case model.VisibilityPublic: + case gtsmodel.VisibilityPublic: // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out break - case model.VisibilityUnlocked: + case gtsmodel.VisibilityUnlocked: // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them if form.Federated != nil { gtsAdvancedVis.Federated = *form.Federated @@ -298,7 +336,7 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis model.Vis gtsAdvancedVis.Likeable = *form.Likeable } - case model.VisibilityFollowersOnly, model.VisibilityMutualsOnly: + case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them gtsAdvancedVis.Boostable = false @@ -314,7 +352,7 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis model.Vis gtsAdvancedVis.Likeable = *form.Likeable } - case model.VisibilityDirect: + case gtsmodel.VisibilityDirect: // direct is pretty easy: there's only one possible setting so return it gtsAdvancedVis.Federated = true gtsAdvancedVis.Boostable = false @@ -327,7 +365,7 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis model.Vis return nil } -func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *model.Status) error { +func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { if form.InReplyToID == "" { return nil } @@ -339,8 +377,8 @@ func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun // 3. Does a block exist between either the current account or the account that posted the status it's replying to? // // If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing. - repliedStatus := &model.Status{} - repliedAccount := &model.Account{} + repliedStatus := >smodel.Status{} + repliedAccount := >smodel.Account{} // check replied status exists + is replyable if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil { if _, ok := err.(db.ErrNoEntries); ok { @@ -356,26 +394,35 @@ func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun // check replied account is known to us if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + 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) + } else { + return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + } } // check if a block exists - if blocked, err := m.db.Blocked(thisAccountID, repliedAccount.ID); err != nil || blocked { - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + if blocked, err := m.db.Blocked(thisAccountID, repliedAccount.ID); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + } + } else if blocked { + return fmt.Errorf("status with id %s not replyable", form.InReplyToID) } status.InReplyToID = repliedStatus.ID + status.InReplyToAccountID = repliedAccount.ID return nil } -func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *model.Status) error { +func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { if form.MediaIDs == nil { return nil } - attachments := []*model.MediaAttachment{} + attachments := []*gtsmodel.MediaAttachment{} for _, mediaID := range form.MediaIDs { // check these attachments exist - a := &model.MediaAttachment{} + a := >smodel.MediaAttachment{} if err := m.db.GetByID(mediaID, a); err != nil { return fmt.Errorf("invalid media type or media not found for media id %s", mediaID) } @@ -389,7 +436,7 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount return nil } -func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string, status *model.Status) error { +func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error { if form.Language != "" { status.Language = form.Language } else { diff --git a/internal/apimodule/status/statuscreate_test.go b/internal/apimodule/status/statuscreate_test.go index 6a6aa9eee..446eab93b 100644 --- a/internal/apimodule/status/statuscreate_test.go +++ b/internal/apimodule/status/statuscreate_test.go @@ -34,12 +34,13 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/distributor" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" + mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/storage" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -49,12 +50,13 @@ type StatusCreateTestSuite struct { mockOauthServer *oauth.MockServer mockStorage *storage.MockStorage mediaHandler media.MediaHandler + mastoConverter mastotypes.Converter distributor *distributor.MockDistributor testTokens map[string]*oauth.Token testClients map[string]*oauth.Client - testApplications map[string]*model.Application - testUsers map[string]*model.User - testAccounts map[string]*model.Account + testApplications map[string]*gtsmodel.Application + testUsers map[string]*gtsmodel.User + testAccounts map[string]*gtsmodel.Account log *logrus.Logger db db.DB statusModule *statusModule @@ -113,10 +115,11 @@ func (suite *StatusCreateTestSuite) SetupSuite() { suite.mockOauthServer = &oauth.MockServer{} suite.mockStorage = &storage.MockStorage{} suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log) + suite.mastoConverter = mastotypes.New(suite.config, suite.db) suite.distributor = &distributor.MockDistributor{} suite.distributor.On("FromClientAPI").Return(make(chan distributor.FromClientAPI, 100)) - suite.statusModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.distributor, suite.log).(*statusModule) + suite.statusModule = New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*statusModule) } func (suite *StatusCreateTestSuite) TearDownSuite() { @@ -184,16 +187,15 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() { defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - fmt.Println(string(b)) - statusReply := &mastotypes.Status{} + statusReply := &mastomodel.Status{} err = json.Unmarshal(b, statusReply) assert.NoError(suite.T(), err) assert.Equal(suite.T(), "hello hello", statusReply.SpoilerText) assert.Equal(suite.T(), "this is a brand new status!", statusReply.Content) assert.True(suite.T(), statusReply.Sensitive) - assert.Equal(suite.T(), mastotypes.VisibilityPrivate, statusReply.Visibility) + assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility) } func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToFail() { @@ -209,31 +211,60 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToFail() { ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request.Form = url.Values{ - "status": {"this is a reply to a status that doesn't exist"}, - "spoiler_text": {"don't open cuz it won't work"}, - "in_reply_to_id": {"3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50"}, + "status": {"this is a reply to a status that doesn't exist"}, + "spoiler_text": {"don't open cuz it won't work"}, + "in_reply_to_id": {"3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50"}, } suite.statusModule.statusCreatePOSTHandler(ctx) // check response - // 1. we should have OK from our call to the function + suite.EqualValues(http.StatusBadRequest, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), `{"error":"status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b)) +} + +func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToLocalSuccess() { + t := suite.testTokens["local_account_1"] + oauthToken := oauth.PGTokenToOauthToken(t) + + // setup + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + ctx.Set(oauth.SessionAuthorizedToken, oauthToken) + ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) + ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting + ctx.Request.Form = url.Values{ + "status": {fmt.Sprintf("hello @%s this reply should work!", testrig.TestAccounts()["local_account_2"].Username)}, + "in_reply_to_id": {testrig.TestStatuses()["local_account_2_status_1"].ID}, + } + suite.statusModule.statusCreatePOSTHandler(ctx) + + // check response suite.EqualValues(http.StatusOK, recorder.Code) result := recorder.Result() defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - fmt.Println(string(b)) - statusReply := &mastotypes.Status{} + statusReply := &mastomodel.Status{} err = json.Unmarshal(b, statusReply) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), "hello hello", statusReply.SpoilerText) - assert.Equal(suite.T(), "this is a brand new status!", statusReply.Content) - assert.True(suite.T(), statusReply.Sensitive) - assert.Equal(suite.T(), mastotypes.VisibilityPrivate, statusReply.Visibility) + assert.Equal(suite.T(), "", statusReply.SpoilerText) + assert.Equal(suite.T(), fmt.Sprintf("hello @%s this reply should work!", testrig.TestAccounts()["local_account_2"].Username), statusReply.Content) + assert.False(suite.T(), statusReply.Sensitive) + assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility) + assert.Equal(suite.T(), testrig.TestStatuses()["local_account_2_status_1"].ID, statusReply.InReplyToID) + assert.Equal(suite.T(), testrig.TestAccounts()["local_account_2"].ID, statusReply.InReplyToAccountID) + assert.Len(suite.T(), statusReply.Mentions, 1) } func TestStatusCreateTestSuite(t *testing.T) { diff --git a/internal/db/db.go b/internal/db/db.go index bf516dfc7..140bca88c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -27,8 +27,7 @@ import ( "github.com/go-fed/activity/pub" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db/model" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" ) const dbTypePostgres string = "POSTGRES" @@ -115,38 +114,38 @@ type DB interface { // GetAccountByUserID is a shortcut for the common action of fetching an account corresponding to a user ID. // The given account pointer will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetAccountByUserID(userID string, account *model.Account) error + GetAccountByUserID(userID string, account *gtsmodel.Account) error // GetFollowRequestsForAccountID is a shortcut for the common action of fetching a list of follow requests targeting the given account ID. // The given slice 'followRequests' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetFollowRequestsForAccountID(accountID string, followRequests *[]model.FollowRequest) error + GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error // GetFollowingByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is following. // The given slice 'following' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetFollowingByAccountID(accountID string, following *[]model.Follow) error + GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error // GetFollowersByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is followed by. // The given slice 'followers' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetFollowersByAccountID(accountID string, followers *[]model.Follow) error + GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error // GetStatusesByAccountID is a shortcut for the common action of fetching a list of statuses produced by accountID. // The given slice 'statuses' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetStatusesByAccountID(accountID string, statuses *[]model.Status) error + GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error // GetStatusesByTimeDescending is a shortcut for getting the most recent statuses. accountID is optional, if not provided // then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can // be very memory intensive so you probably shouldn't do this! // In case of no entries, a 'no entries' error will be returned - GetStatusesByTimeDescending(accountID string, statuses *[]model.Status, limit int) error + GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error // GetLastStatusForAccountID simply gets the most recent status by the given account. // The given slice 'status' pointer will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetLastStatusForAccountID(accountID string, status *model.Status) error + GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error // IsUsernameAvailable checks whether a given username is available on our domain. // Returns an error if the username is already taken, or something went wrong in the db. @@ -161,18 +160,18 @@ type DB interface { // NewSignup creates a new user in the database with the given parameters, with an *unconfirmed* email address. // By the time this function is called, it should be assumed that all the parameters have passed validation! - NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*model.User, error) + NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) // SetHeaderOrAvatarForAccountID sets the header or avatar for the given accountID to the given media attachment. - SetHeaderOrAvatarForAccountID(mediaAttachment *model.MediaAttachment, accountID string) error + SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error // GetHeaderAvatarForAccountID gets the current avatar for the given account ID. // The passed mediaAttachment pointer will be populated with the value of the avatar, if it exists. - GetAvatarForAccountID(avatar *model.MediaAttachment, accountID string) error + GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error // GetHeaderForAccountID gets the current header for the given account ID. // The passed mediaAttachment pointer will be populated with the value of the header, if it exists. - GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error + GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error // Blocked checks whether a block exists in eiher direction between two accounts. // That is, it returns true if account1 blocks account2, OR if account2 blocks account1. @@ -182,39 +181,30 @@ type DB interface { USEFUL CONVERSION FUNCTIONS */ - // AccountToMastoSensitive takes a db model account as a param, and returns a populated mastotype account, or an error - // if something goes wrong. The returned account should be ready to serialize on an API level, and may have sensitive fields, - // so serve it only to an authorized user who should have permission to see it. - AccountToMastoSensitive(account *model.Account) (*mastotypes.Account, error) - - // AccountToMastoPublic takes a db model account as a param, and returns a populated mastotype account, or an error - // if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. - // In other words, this is the public record that the server has of an account. - AccountToMastoPublic(account *model.Account) (*mastotypes.Account, error) - - // MentionStringsToMentions takes a slice of deduplicated, lowercase account names in the form "@test@whatever.example.org", which have been - // mentioned in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then + // MentionStringsToMentions takes a slice of deduplicated, lowercase account names in the form "@test@whatever.example.org" for a remote account, + // or @test for a local account, which have been mentioned in a status. + // It takes the id of the account that wrote the status, and the id of the status itself, and then // checks in the database for the mentioned accounts, and returns a slice of mentions generated based on the given parameters. // - // Note: this func doesn't/shouldn't do any manipulation of the accounts in the DB, it's just for checking if they exist - // and conveniently returning them. - MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*model.Mention, error) + // Note: this func doesn't/shouldn't do any manipulation of the accounts in the DB, it's just for checking + // if they exist in the db and conveniently returning them. + MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) // TagStringsToTags takes a slice of deduplicated, lowercase tags in the form "somehashtag", which have been // used in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then // returns a slice of *model.Tag corresponding to the given tags. // - // Note: this func doesn't/shouldn't do any manipulation of the tags in the DB, it's just for checking if they exist - // and conveniently returning them. - TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*model.Tag, error) + // Note: this func doesn't/shouldn't do any manipulation of the tags in the DB, it's just for checking + // if they exist in the db and conveniently returning them. + TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) // EmojiStringsToEmojis takes a slice of deduplicated, lowercase emojis in the form ":emojiname:", which have been // used in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then // returns a slice of *model.Emoji corresponding to the given emojis. // - // Note: this func doesn't/shouldn't do any manipulation of the emoji in the DB, it's just for checking if they exist - // and conveniently returning them. - EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*model.Emoji, error) + // Note: this func doesn't/shouldn't do any manipulation of the emoji in the DB, it's just for checking + // if they exist in the db and conveniently returning them. + EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) } // New returns a new database service that satisfies the DB interface and, by extension, diff --git a/internal/db/model/README.md b/internal/db/gtsmodel/README.md similarity index 100% rename from internal/db/model/README.md rename to internal/db/gtsmodel/README.md diff --git a/internal/db/model/account.go b/internal/db/gtsmodel/account.go similarity index 96% rename from internal/db/model/account.go rename to internal/db/gtsmodel/account.go index 3601f53b8..82f5b5c6f 100644 --- a/internal/db/model/account.go +++ b/internal/db/gtsmodel/account.go @@ -16,11 +16,11 @@ along with this program. If not, see . */ -// Package model contains types used *internally* by GoToSocial and added/removed/selected from the database. +// Package gtsmodel contains types used *internally* by GoToSocial and added/removed/selected from the database. // These types should never be serialized and/or sent out via public APIs, as they contain sensitive information. // The annotation used on these structs is for handling them via the go-pg ORM (hence why they're in this db subdir). // See here for more info on go-pg model annotations: https://pg.uptrace.dev/models/ -package model +package gtsmodel import ( "crypto/rsa" @@ -38,7 +38,7 @@ type Account struct { // Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org`` Username string `pg:",notnull,unique:userdomain"` // username and domain should be unique *with* each other // Domain of the account, will be null if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username. - Domain string `pg:"default:null,unique:userdomain"` // username and domain should be unique *with* each other + Domain string `pg:",unique:userdomain"` // username and domain should be unique *with* each other /* ACCOUNT METADATA diff --git a/internal/db/model/activitystreams.go b/internal/db/gtsmodel/activitystreams.go similarity index 99% rename from internal/db/model/activitystreams.go rename to internal/db/gtsmodel/activitystreams.go index b6c9df662..059588a57 100644 --- a/internal/db/model/activitystreams.go +++ b/internal/db/gtsmodel/activitystreams.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel // ActivityStreamsObject refers to https://www.w3.org/TR/activitystreams-vocabulary/#object-types type ActivityStreamsObject string diff --git a/internal/db/model/application.go b/internal/db/gtsmodel/application.go similarity index 72% rename from internal/db/model/application.go rename to internal/db/gtsmodel/application.go index 439155264..8e1398beb 100644 --- a/internal/db/model/application.go +++ b/internal/db/gtsmodel/application.go @@ -16,9 +16,7 @@ along with this program. If not, see . */ -package model - -import "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" +package gtsmodel // Application represents an application that can perform actions on behalf of a user. // It is used to authorize tokens etc, and is associated with an oauth client id in the database. @@ -40,23 +38,3 @@ type Application struct { // a vapid key generated for this app when it was created VapidKey string } - -// ToMastoSensitive returns this application as a mastodon api type, ready for serialization -func (a *Application) ToMastoSensitive() *mastotypes.Application { - return &mastotypes.Application{ - ID: a.ID, - Name: a.Name, - Website: a.Website, - RedirectURI: a.RedirectURI, - ClientID: a.ClientID, - ClientSecret: a.ClientSecret, - VapidKey: a.VapidKey, - } -} - -func (a *Application) ToMastoPublic() *mastotypes.Application { - return &mastotypes.Application{ - Name: a.Name, - Website: a.Website, - } -} diff --git a/internal/db/model/block.go b/internal/db/gtsmodel/block.go similarity index 97% rename from internal/db/model/block.go rename to internal/db/gtsmodel/block.go index d106e25e2..fae43fbef 100644 --- a/internal/db/model/block.go +++ b/internal/db/gtsmodel/block.go @@ -1,4 +1,4 @@ -package model +package gtsmodel import "time" diff --git a/internal/db/model/domainblock.go b/internal/db/gtsmodel/domainblock.go similarity index 99% rename from internal/db/model/domainblock.go rename to internal/db/gtsmodel/domainblock.go index e6e89bc20..dcfb2acee 100644 --- a/internal/db/model/domainblock.go +++ b/internal/db/gtsmodel/domainblock.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" diff --git a/internal/db/model/emaildomainblock.go b/internal/db/gtsmodel/emaildomainblock.go similarity index 98% rename from internal/db/model/emaildomainblock.go rename to internal/db/gtsmodel/emaildomainblock.go index 6610a2075..4cda68b02 100644 --- a/internal/db/model/emaildomainblock.go +++ b/internal/db/gtsmodel/emaildomainblock.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" diff --git a/internal/db/model/emoji.go b/internal/db/gtsmodel/emoji.go similarity index 98% rename from internal/db/model/emoji.go rename to internal/db/gtsmodel/emoji.go index 0aaa1d724..d704ef5b4 100644 --- a/internal/db/model/emoji.go +++ b/internal/db/gtsmodel/emoji.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" diff --git a/internal/db/model/follow.go b/internal/db/gtsmodel/follow.go similarity index 98% rename from internal/db/model/follow.go rename to internal/db/gtsmodel/follow.go index 36e19e72e..90080da6e 100644 --- a/internal/db/model/follow.go +++ b/internal/db/gtsmodel/follow.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" diff --git a/internal/db/model/followrequest.go b/internal/db/gtsmodel/followrequest.go similarity index 99% rename from internal/db/model/followrequest.go rename to internal/db/gtsmodel/followrequest.go index 50d8a5f03..1401a26f1 100644 --- a/internal/db/model/followrequest.go +++ b/internal/db/gtsmodel/followrequest.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" diff --git a/internal/db/model/mediaattachment.go b/internal/db/gtsmodel/mediaattachment.go similarity index 88% rename from internal/db/model/mediaattachment.go rename to internal/db/gtsmodel/mediaattachment.go index 3aff18d80..a906f8350 100644 --- a/internal/db/model/mediaattachment.go +++ b/internal/db/gtsmodel/mediaattachment.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import ( "time" @@ -29,7 +29,9 @@ type MediaAttachment struct { ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` // ID of the status to which this is attached StatusID string - // Where can the attachment be retrieved on a remote server + // Where can the attachment be retrieved on *this* server + URL string + // Where can the attachment be retrieved on a remote server (empty for local media) RemoteURL string // When was the attachment created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` @@ -81,7 +83,9 @@ type Thumbnail struct { FileSize int // When was the file last updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` - // What is the remote URL of the thumbnail + // What is the URL of the thumbnail on the local server + URL string + // What is the remote URL of the thumbnail (empty for local media) RemoteURL string } @@ -106,11 +110,13 @@ const ( // FileTypeImage is for jpegs and pngs FileTypeImage FileType = "image" // FileTypeGif is for native gifs and soundless videos that have been converted to gifs - FileTypeGif FileType = "gif" + FileTypeGif FileType = "gifv" // FileTypeAudio is for audio-only files (no video) FileTypeAudio FileType = "audio" // FileTypeVideo is for files with audio + visual FileTypeVideo FileType = "video" + // FileTypeUnknown is for unknown file types (surprise surprise!) + FileTypeUnknown FileType = "unknown" ) // FileMeta describes metadata about the actual contents of the file. @@ -119,7 +125,7 @@ type FileMeta struct { Small Small } -// Small implements SmallMeta and can be used for a thumbnail of any media type +// Small can be used for a thumbnail of any media type type Small struct { Width int Height int @@ -127,7 +133,7 @@ type Small struct { Aspect float64 } -// ImageOriginal implements OriginalMeta for still images +// Original can be used for original metadata for any media type type Original struct { Width int Height int diff --git a/internal/db/model/mention.go b/internal/db/gtsmodel/mention.go similarity index 98% rename from internal/db/model/mention.go rename to internal/db/gtsmodel/mention.go index 74dd0011c..6c1993740 100644 --- a/internal/db/model/mention.go +++ b/internal/db/gtsmodel/mention.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" diff --git a/internal/db/gtsmodel/poll.go b/internal/db/gtsmodel/poll.go new file mode 100644 index 000000000..bc0fefaa7 --- /dev/null +++ b/internal/db/gtsmodel/poll.go @@ -0,0 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package gtsmodel + + diff --git a/internal/db/model/status.go b/internal/db/gtsmodel/status.go similarity index 97% rename from internal/db/model/status.go rename to internal/db/gtsmodel/status.go index b0e9ab084..ea198ae75 100644 --- a/internal/db/model/status.go +++ b/internal/db/gtsmodel/status.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" @@ -40,6 +40,8 @@ type Status struct { AccountID string // id of the status this status is a reply to InReplyToID string + // id of the account that this status replies to + InReplyToAccountID string // id of the status this status is a boost of BoostOfID string // cw string for this status diff --git a/internal/db/model/tag.go b/internal/db/gtsmodel/tag.go similarity index 98% rename from internal/db/model/tag.go rename to internal/db/gtsmodel/tag.go index f57a063bc..acc0549de 100644 --- a/internal/db/model/tag.go +++ b/internal/db/gtsmodel/tag.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import "time" diff --git a/internal/db/model/user.go b/internal/db/gtsmodel/user.go similarity index 99% rename from internal/db/model/user.go rename to internal/db/gtsmodel/user.go index 61e9954d5..a72569945 100644 --- a/internal/db/model/user.go +++ b/internal/db/gtsmodel/user.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package model +package gtsmodel import ( "net" diff --git a/internal/db/mock_DB.go b/internal/db/mock_DB.go index 8d0932c1f..df2e41907 100644 --- a/internal/db/mock_DB.go +++ b/internal/db/mock_DB.go @@ -6,9 +6,7 @@ import ( context "context" mock "github.com/stretchr/testify/mock" - mastotypes "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" - - model "github.com/superseriousbusiness/gotosocial/internal/db/model" + gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" net "net" @@ -20,52 +18,6 @@ type MockDB struct { mock.Mock } -// AccountToMastoPublic provides a mock function with given fields: account -func (_m *MockDB) AccountToMastoPublic(account *model.Account) (*mastotypes.Account, error) { - ret := _m.Called(account) - - var r0 *mastotypes.Account - if rf, ok := ret.Get(0).(func(*model.Account) *mastotypes.Account); ok { - r0 = rf(account) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*mastotypes.Account) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.Account) error); ok { - r1 = rf(account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// AccountToMastoSensitive provides a mock function with given fields: account -func (_m *MockDB) AccountToMastoSensitive(account *model.Account) (*mastotypes.Account, error) { - ret := _m.Called(account) - - var r0 *mastotypes.Account - if rf, ok := ret.Get(0).(func(*model.Account) *mastotypes.Account); ok { - r0 = rf(account) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*mastotypes.Account) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.Account) error); ok { - r1 = rf(account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Blocked provides a mock function with given fields: account1, account2 func (_m *MockDB) Blocked(account1 string, account2 string) (bool, error) { ret := _m.Called(account1, account2) @@ -144,15 +96,15 @@ func (_m *MockDB) DropTable(i interface{}) error { } // EmojiStringsToEmojis provides a mock function with given fields: emojis, originAccountID, statusID -func (_m *MockDB) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*model.Emoji, error) { +func (_m *MockDB) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) { ret := _m.Called(emojis, originAccountID, statusID) - var r0 []*model.Emoji - if rf, ok := ret.Get(0).(func([]string, string, string) []*model.Emoji); ok { + var r0 []*gtsmodel.Emoji + if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Emoji); ok { r0 = rf(emojis, originAccountID, statusID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Emoji) + r0 = ret.Get(0).([]*gtsmodel.Emoji) } } @@ -183,11 +135,11 @@ func (_m *MockDB) Federation() pub.Database { } // GetAccountByUserID provides a mock function with given fields: userID, account -func (_m *MockDB) GetAccountByUserID(userID string, account *model.Account) error { +func (_m *MockDB) GetAccountByUserID(userID string, account *gtsmodel.Account) error { ret := _m.Called(userID, account) var r0 error - if rf, ok := ret.Get(0).(func(string, *model.Account) error); ok { + if rf, ok := ret.Get(0).(func(string, *gtsmodel.Account) error); ok { r0 = rf(userID, account) } else { r0 = ret.Error(0) @@ -211,11 +163,11 @@ func (_m *MockDB) GetAll(i interface{}) error { } // GetAvatarForAccountID provides a mock function with given fields: avatar, accountID -func (_m *MockDB) GetAvatarForAccountID(avatar *model.MediaAttachment, accountID string) error { +func (_m *MockDB) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { ret := _m.Called(avatar, accountID) var r0 error - if rf, ok := ret.Get(0).(func(*model.MediaAttachment, string) error); ok { + if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { r0 = rf(avatar, accountID) } else { r0 = ret.Error(0) @@ -239,11 +191,11 @@ func (_m *MockDB) GetByID(id string, i interface{}) error { } // GetFollowRequestsForAccountID provides a mock function with given fields: accountID, followRequests -func (_m *MockDB) GetFollowRequestsForAccountID(accountID string, followRequests *[]model.FollowRequest) error { +func (_m *MockDB) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { ret := _m.Called(accountID, followRequests) var r0 error - if rf, ok := ret.Get(0).(func(string, *[]model.FollowRequest) error); ok { + if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.FollowRequest) error); ok { r0 = rf(accountID, followRequests) } else { r0 = ret.Error(0) @@ -253,11 +205,11 @@ func (_m *MockDB) GetFollowRequestsForAccountID(accountID string, followRequests } // GetFollowersByAccountID provides a mock function with given fields: accountID, followers -func (_m *MockDB) GetFollowersByAccountID(accountID string, followers *[]model.Follow) error { +func (_m *MockDB) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error { ret := _m.Called(accountID, followers) var r0 error - if rf, ok := ret.Get(0).(func(string, *[]model.Follow) error); ok { + if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Follow) error); ok { r0 = rf(accountID, followers) } else { r0 = ret.Error(0) @@ -267,11 +219,11 @@ func (_m *MockDB) GetFollowersByAccountID(accountID string, followers *[]model.F } // GetFollowingByAccountID provides a mock function with given fields: accountID, following -func (_m *MockDB) GetFollowingByAccountID(accountID string, following *[]model.Follow) error { +func (_m *MockDB) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { ret := _m.Called(accountID, following) var r0 error - if rf, ok := ret.Get(0).(func(string, *[]model.Follow) error); ok { + if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Follow) error); ok { r0 = rf(accountID, following) } else { r0 = ret.Error(0) @@ -281,11 +233,11 @@ func (_m *MockDB) GetFollowingByAccountID(accountID string, following *[]model.F } // GetHeaderForAccountID provides a mock function with given fields: header, accountID -func (_m *MockDB) GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error { +func (_m *MockDB) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { ret := _m.Called(header, accountID) var r0 error - if rf, ok := ret.Get(0).(func(*model.MediaAttachment, string) error); ok { + if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { r0 = rf(header, accountID) } else { r0 = ret.Error(0) @@ -295,11 +247,11 @@ func (_m *MockDB) GetHeaderForAccountID(header *model.MediaAttachment, accountID } // GetLastStatusForAccountID provides a mock function with given fields: accountID, status -func (_m *MockDB) GetLastStatusForAccountID(accountID string, status *model.Status) error { +func (_m *MockDB) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { ret := _m.Called(accountID, status) var r0 error - if rf, ok := ret.Get(0).(func(string, *model.Status) error); ok { + if rf, ok := ret.Get(0).(func(string, *gtsmodel.Status) error); ok { r0 = rf(accountID, status) } else { r0 = ret.Error(0) @@ -309,11 +261,11 @@ func (_m *MockDB) GetLastStatusForAccountID(accountID string, status *model.Stat } // GetStatusesByAccountID provides a mock function with given fields: accountID, statuses -func (_m *MockDB) GetStatusesByAccountID(accountID string, statuses *[]model.Status) error { +func (_m *MockDB) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error { ret := _m.Called(accountID, statuses) var r0 error - if rf, ok := ret.Get(0).(func(string, *[]model.Status) error); ok { + if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Status) error); ok { r0 = rf(accountID, statuses) } else { r0 = ret.Error(0) @@ -323,11 +275,11 @@ func (_m *MockDB) GetStatusesByAccountID(accountID string, statuses *[]model.Sta } // GetStatusesByTimeDescending provides a mock function with given fields: accountID, statuses, limit -func (_m *MockDB) GetStatusesByTimeDescending(accountID string, statuses *[]model.Status, limit int) error { +func (_m *MockDB) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error { ret := _m.Called(accountID, statuses, limit) var r0 error - if rf, ok := ret.Get(0).(func(string, *[]model.Status, int) error); ok { + if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Status, int) error); ok { r0 = rf(accountID, statuses, limit) } else { r0 = ret.Error(0) @@ -393,15 +345,15 @@ func (_m *MockDB) IsUsernameAvailable(username string) error { } // MentionStringsToMentions provides a mock function with given fields: targetAccounts, originAccountID, statusID -func (_m *MockDB) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*model.Mention, error) { +func (_m *MockDB) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { ret := _m.Called(targetAccounts, originAccountID, statusID) - var r0 []*model.Mention - if rf, ok := ret.Get(0).(func([]string, string, string) []*model.Mention); ok { + var r0 []*gtsmodel.Mention + if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Mention); ok { r0 = rf(targetAccounts, originAccountID, statusID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Mention) + r0 = ret.Get(0).([]*gtsmodel.Mention) } } @@ -416,15 +368,15 @@ func (_m *MockDB) MentionStringsToMentions(targetAccounts []string, originAccoun } // NewSignup provides a mock function with given fields: username, reason, requireApproval, email, password, signUpIP, locale, appID -func (_m *MockDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*model.User, error) { +func (_m *MockDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) { ret := _m.Called(username, reason, requireApproval, email, password, signUpIP, locale, appID) - var r0 *model.User - if rf, ok := ret.Get(0).(func(string, string, bool, string, string, net.IP, string, string) *model.User); ok { + var r0 *gtsmodel.User + if rf, ok := ret.Get(0).(func(string, string, bool, string, string, net.IP, string, string) *gtsmodel.User); ok { r0 = rf(username, reason, requireApproval, email, password, signUpIP, locale, appID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) + r0 = ret.Get(0).(*gtsmodel.User) } } @@ -453,11 +405,11 @@ func (_m *MockDB) Put(i interface{}) error { } // SetHeaderOrAvatarForAccountID provides a mock function with given fields: mediaAttachment, accountID -func (_m *MockDB) SetHeaderOrAvatarForAccountID(mediaAttachment *model.MediaAttachment, accountID string) error { +func (_m *MockDB) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { ret := _m.Called(mediaAttachment, accountID) var r0 error - if rf, ok := ret.Get(0).(func(*model.MediaAttachment, string) error); ok { + if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { r0 = rf(mediaAttachment, accountID) } else { r0 = ret.Error(0) @@ -481,15 +433,15 @@ func (_m *MockDB) Stop(ctx context.Context) error { } // TagStringsToTags provides a mock function with given fields: tags, originAccountID, statusID -func (_m *MockDB) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*model.Tag, error) { +func (_m *MockDB) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) { ret := _m.Called(tags, originAccountID, statusID) - var r0 []*model.Tag - if rf, ok := ret.Get(0).(func([]string, string, string) []*model.Tag); ok { + var r0 []*gtsmodel.Tag + if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Tag); ok { r0 = rf(tags, originAccountID, statusID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Tag) + r0 = ret.Get(0).([]*gtsmodel.Tag) } } diff --git a/internal/db/pg.go b/internal/db/pg.go index 623fc5f04..3e25b8fcd 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -36,9 +36,9 @@ import ( "github.com/go-pg/pg/v10/orm" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" "golang.org/x/crypto/bcrypt" ) @@ -214,9 +214,9 @@ func (ps *postgresService) IsHealthy(ctx context.Context) error { func (ps *postgresService) CreateSchema(ctx context.Context) error { models := []interface{}{ - (*model.Account)(nil), - (*model.Status)(nil), - (*model.User)(nil), + (*gtsmodel.Account)(nil), + (*gtsmodel.Status)(nil), + (*gtsmodel.User)(nil), } ps.log.Info("creating db schema") @@ -312,8 +312,8 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac HANDY SHORTCUTS */ -func (ps *postgresService) GetAccountByUserID(userID string, account *model.Account) error { - user := &model.User{ +func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { + user := >smodel.User{ ID: userID, } if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { @@ -331,7 +331,7 @@ func (ps *postgresService) GetAccountByUserID(userID string, account *model.Acco return nil } -func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]model.FollowRequest) error { +func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} @@ -341,7 +341,7 @@ func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, follo return nil } -func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]model.Follow) error { +func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} @@ -351,7 +351,7 @@ func (ps *postgresService) GetFollowingByAccountID(accountID string, following * return nil } -func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]model.Follow) error { +func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error { if err := ps.conn.Model(followers).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} @@ -361,7 +361,7 @@ func (ps *postgresService) GetFollowersByAccountID(accountID string, followers * return nil } -func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]model.Status) error { +func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error { if err := ps.conn.Model(statuses).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} @@ -371,7 +371,7 @@ func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[] return nil } -func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]model.Status, limit int) error { +func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error { q := ps.conn.Model(statuses).Order("created_at DESC") if limit != 0 { q = q.Limit(limit) @@ -388,7 +388,7 @@ func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuse return nil } -func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *model.Status) error { +func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { if err := ps.conn.Model(status).Order("created_at DESC").Limit(1).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} @@ -403,7 +403,7 @@ func (ps *postgresService) IsUsernameAvailable(username string) error { // if no error we fail because it means we found something // if error but it's not pg.ErrNoRows then we fail // if err is pg.ErrNoRows we're good, we found nothing so continue - if err := ps.conn.Model(&model.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { + if err := ps.conn.Model(>smodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { return fmt.Errorf("username %s already in use", username) } else if err != pg.ErrNoRows { return fmt.Errorf("db error: %s", err) @@ -420,7 +420,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error { domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ // check if the email domain is blocked - if err := ps.conn.Model(&model.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { + if err := ps.conn.Model(>smodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { // fail because we found something return fmt.Errorf("email domain %s is blocked", domain) } else if err != pg.ErrNoRows { @@ -429,7 +429,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error { } // check if this email is associated with a user already - if err := ps.conn.Model(&model.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { + if err := ps.conn.Model(>smodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { // fail because we found something return fmt.Errorf("email %s already in use", email) } else if err != pg.ErrNoRows { @@ -439,7 +439,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error { return nil } -func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*model.User, error) { +func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { ps.log.Errorf("error creating new rsa key: %s", err) @@ -448,14 +448,14 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr uris := util.GenerateURIs(username, ps.config.Protocol, ps.config.Host) - a := &model.Account{ + a := >smodel.Account{ Username: username, DisplayName: username, Reason: reason, URL: uris.UserURL, PrivateKey: key, PublicKey: &key.PublicKey, - ActorType: model.ActivityStreamsPerson, + ActorType: gtsmodel.ActivityStreamsPerson, URI: uris.UserURI, InboxURL: uris.InboxURI, OutboxURL: uris.OutboxURI, @@ -470,7 +470,7 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr if err != nil { return nil, fmt.Errorf("error hashing password: %s", err) } - u := &model.User{ + u := >smodel.User{ AccountID: a.ID, EncryptedPassword: string(pw), SignUpIP: signUpIP, @@ -486,12 +486,12 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr return u, nil } -func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *model.MediaAttachment, accountID string) error { +func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { _, err := ps.conn.Model(mediaAttachment).Insert() return err } -func (ps *postgresService) GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error { +func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { if err := ps.conn.Model(header).Where("account_id = ?", accountID).Where("header = ?", true).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} @@ -501,7 +501,7 @@ func (ps *postgresService) GetHeaderForAccountID(header *model.MediaAttachment, return nil } -func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment, accountID string) error { +func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { if err := ps.conn.Model(avatar).Where("account_id = ?", accountID).Where("avatar = ?", true).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} @@ -513,12 +513,13 @@ func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment, func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { var blocked bool - if err := ps.conn.Model(&model.Block{}). + if err := ps.conn.Model(>smodel.Block{}). Where("account_id = ?", account1).Where("target_account_id = ?", account2). WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). Select(); err != nil { if err == pg.ErrNoRows { blocked = false + return blocked, nil } else { return blocked, err } @@ -535,7 +536,7 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro // The resulting account fits the specifications for the path /api/v1/accounts/verify_credentials, as described here: // https://docs.joinmastodon.org/methods/accounts/. Note that it's *sensitive* because it's only meant to be exposed to the user // that the account actually belongs to. -func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotypes.Account, error) { +func (ps *postgresService) AccountToMastoSensitive(a *gtsmodel.Account) (*mastotypes.Account, error) { // we can build this sensitive account easily by first getting the public account.... mastoAccount, err := ps.AccountToMastoPublic(a) if err != nil { @@ -545,7 +546,7 @@ func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotype // then adding the Source object to it... // check pending follow requests aimed at this account - fr := []model.FollowRequest{} + fr := []gtsmodel.FollowRequest{} if err := ps.GetFollowRequestsForAccountID(a.ID, &fr); err != nil { if _, ok := err.(ErrNoEntries); !ok { return nil, fmt.Errorf("error getting follow requests: %s", err) @@ -568,9 +569,9 @@ func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotype return mastoAccount, nil } -func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.Account, error) { +func (ps *postgresService) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Account, error) { // count followers - followers := []model.Follow{} + followers := []gtsmodel.Follow{} if err := ps.GetFollowersByAccountID(a.ID, &followers); err != nil { if _, ok := err.(ErrNoEntries); !ok { return nil, fmt.Errorf("error getting followers: %s", err) @@ -582,7 +583,7 @@ func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.A } // count following - following := []model.Follow{} + following := []gtsmodel.Follow{} if err := ps.GetFollowingByAccountID(a.ID, &following); err != nil { if _, ok := err.(ErrNoEntries); !ok { return nil, fmt.Errorf("error getting following: %s", err) @@ -594,7 +595,7 @@ func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.A } // count statuses - statuses := []model.Status{} + statuses := []gtsmodel.Status{} if err := ps.GetStatusesByAccountID(a.ID, &statuses); err != nil { if _, ok := err.(ErrNoEntries); !ok { return nil, fmt.Errorf("error getting last statuses: %s", err) @@ -606,7 +607,7 @@ func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.A } // check when the last status was - lastStatus := &model.Status{} + lastStatus := >smodel.Status{} if err := ps.GetLastStatusForAccountID(a.ID, lastStatus); err != nil { if _, ok := err.(ErrNoEntries); !ok { return nil, fmt.Errorf("error getting last status: %s", err) @@ -618,7 +619,7 @@ func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.A } // build the avatar and header URLs - avi := &model.MediaAttachment{} + avi := >smodel.MediaAttachment{} if err := ps.GetAvatarForAccountID(avi, a.ID); err != nil { if _, ok := err.(ErrNoEntries); !ok { return nil, fmt.Errorf("error getting avatar: %s", err) @@ -627,7 +628,7 @@ func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.A aviURL := avi.File.Path aviURLStatic := avi.Thumbnail.Path - header := &model.MediaAttachment{} + header := >smodel.MediaAttachment{} if err := ps.GetHeaderForAccountID(avi, a.ID); err != nil { if _, ok := err.(ErrNoEntries); !ok { return nil, fmt.Errorf("error getting header: %s", err) @@ -681,11 +682,12 @@ func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.A }, nil } -func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*model.Mention, error) { - menchies := []*model.Mention{} +func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { + menchies := []*gtsmodel.Mention{} for _, a := range targetAccounts { - // A mentioned account looks like "@test@example.org" -- we can guarantee this from the regex that targetAccounts should have been derived from. - // But we still need to do a bit of fiddling to get what we need here -- the username and domain. + // A mentioned account looks like "@test@example.org" or just "@test" for a local account + // -- we can guarantee this from the regex that targetAccounts should have been derived from. + // But we still need to do a bit of fiddling to get what we need here -- the username and domain (if given). // 1. trim off the first @ t := strings.TrimPrefix(a, "@") @@ -693,41 +695,51 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori // 2. split the username and domain s := strings.Split(t, "@") - // 3. it should *always* be length 2 so if it's not then something is seriously wrong - if len(s) != 2 { - return nil, fmt.Errorf("mentioned account format %s was not valid", a) + // 3. if it's length 1 it's a local account, length 2 means remote, anything else means something is wrong + var local bool + switch len(s) { + case 1: + local = true + case 2: + local = false + default: + return nil, fmt.Errorf("mentioned account format '%s' was not valid", a) + } + + var username, domain string + username = s[0] + if !local { + domain = s[1] } - username := s[0] - domain := s[1] // 4. check we now have a proper username and domain - if username == "" || domain == "" { - return nil, fmt.Errorf("username or domain for %s was nil", a) + if username == "" || (!local && domain == "") { + return nil, fmt.Errorf("username or domain for '%s' was nil", a) } // okay we're good now, we can start pulling accounts out of the database - mentionedAccount := &model.Account{} + mentionedAccount := >smodel.Account{} var err error - if domain == ps.config.Host { + if local { // local user -- should have a null domain - err = ps.conn.Model(mentionedAccount).Where("id = ?", username).Where("domain = null").Select() + err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() } else { // remote user -- should have domain defined - err = ps.conn.Model(mentionedAccount).Where("id = ?", username).Where("domain = ?", domain).Select() + err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? = ?", pg.Ident("domain"), domain).Select() } if err != nil { if err == pg.ErrNoRows { // no result found for this username/domain so just don't include it as a mencho and carry on about our business - ps.log.Debugf("no account found with username %s and domain %s, skipping it", username, domain) + ps.log.Debugf("no account found with username '%s' and domain '%s', skipping it", username, domain) continue } // a serious error has happened so bail - return nil, fmt.Errorf("error getting account with username %s and domain %s: %s", username, domain, err) + return nil, fmt.Errorf("error getting account with username '%s' and domain '%s': %s", username, domain, err) } // id, createdAt and updatedAt will be populated by the db, so we have everything we need! - menchies = append(menchies, &model.Mention{ + menchies = append(menchies, >smodel.Mention{ StatusID: statusID, OriginAccountID: originAccountID, TargetAccountID: mentionedAccount.ID, @@ -737,26 +749,26 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori } // for now this function doesn't really use the database, but it's here because: -// A) it might later and +// A) it probably will later and // B) it's v. similar to MentionStringsToMentions -func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*model.Tag, error) { - newTags := []*model.Tag{} +func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) { + newTags := []*gtsmodel.Tag{} for _, t := range tags { - newTags = append(newTags, &model.Tag{ + newTags = append(newTags, >smodel.Tag{ Name: t, }) } return newTags, nil } -func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*model.Emoji, error) { - newEmojis := []*model.Emoji{} +func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) { + newEmojis := []*gtsmodel.Emoji{} for _, e := range emojis { - emoji := &model.Emoji{} + emoji := >smodel.Emoji{} err := ps.conn.Model(emoji).Where("shortcode = ?", e).Where("visible_in_picker = true").Where("disabled = false").Select() if err != nil { if err == pg.ErrNoRows { - // no result found for this username/domain so just don't include it as a mencho and carry on about our business + // no result found for this username/domain so just don't include it as an emoji and carry on about our business ps.log.Debugf("no emoji found with shortcode %s, skipping it", e) continue } diff --git a/internal/distributor/distributor.go b/internal/distributor/distributor.go index 1717da517..027b32279 100644 --- a/internal/distributor/distributor.go +++ b/internal/distributor/distributor.go @@ -21,7 +21,7 @@ package distributor import ( "github.com/go-fed/activity/pub" "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" ) // Distributor should be passed to api modules (see internal/apimodule/...). It is used for @@ -97,13 +97,13 @@ func (d *distributor) Stop() error { } type FromClientAPI struct { - APObjectType model.ActivityStreamsObject - APActivityType model.ActivityStreamsActivity + APObjectType gtsmodel.ActivityStreamsObject + APActivityType gtsmodel.ActivityStreamsActivity Activity interface{} } type ToClientAPI struct { - APObjectType model.ActivityStreamsObject - APActivityType model.ActivityStreamsActivity + APObjectType gtsmodel.ActivityStreamsObject + APActivityType gtsmodel.ActivityStreamsActivity Activity interface{} } diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 03d90217e..1b3dbf69b 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -35,6 +35,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" @@ -62,10 +63,13 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr mediaHandler := media.New(c, dbService, storageBackend, log) oauthServer := oauth.New(dbService, log) + // build converters and util + mastoConverter := mastotypes.New(c, dbService) + // build client api modules authModule := auth.New(oauthServer, dbService, log) - accountModule := account.New(c, dbService, oauthServer, mediaHandler, log) - appsModule := app.New(oauthServer, dbService, log) + accountModule := account.New(c, dbService, oauthServer, mediaHandler, mastoConverter, log) + appsModule := app.New(oauthServer, dbService, mastoConverter, log) apiModules := []apimodule.ClientAPIModule{ authModule, // this one has to go first so the other modules use its middleware diff --git a/internal/mastotypes/converter.go b/internal/mastotypes/converter.go new file mode 100644 index 000000000..b227f0c22 --- /dev/null +++ b/internal/mastotypes/converter.go @@ -0,0 +1,288 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package mastotypes + +import ( + "fmt" + "time" + + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// Converter is an interface for the common action of converting between mastotypes (frontend, serializable) models and internal gts models used in the database. +// It requires access to the database because many of the conversions require pulling out database entries and counting them etc. +type Converter interface { + // AccountToMastoSensitive takes a db model account as a param, and returns a populated mastotype account, or an error + // if something goes wrong. The returned account should be ready to serialize on an API level, and may have sensitive fields, + // so serve it only to an authorized user who should have permission to see it. + AccountToMastoSensitive(account *gtsmodel.Account) (*mastotypes.Account, error) + + // AccountToMastoPublic takes a db model account as a param, and returns a populated mastotype account, or an error + // if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. + // In other words, this is the public record that the server has of an account. + AccountToMastoPublic(account *gtsmodel.Account) (*mastotypes.Account, error) + + // AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error + // if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields + // (such as client id and client secret), so serve it only to an authorized user who should have permission to see it. + AppToMastoSensitive(application *gtsmodel.Application) (*mastotypes.Application, error) + + // AppToMastoPublic takes a db model application as a param, and returns a populated mastotype application, or an error + // if something goes wrong. The returned application should be ready to serialize on an API level, and has sensitive + // fields sanitized so that it can be served to non-authorized accounts without revealing any private information. + AppToMastoPublic(application *gtsmodel.Application) (*mastotypes.Application, error) + + AttachmentToMasto(attachment *gtsmodel.MediaAttachment) (mastotypes.Attachment, error) + + MentionToMasto(m *gtsmodel.Mention) (mastotypes.Mention, error) +} + +type converter struct { + config *config.Config + db db.DB +} + +func New(config *config.Config, db db.DB) Converter { + return &converter{ + config: config, + db: db, + } +} + +func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*mastotypes.Account, error) { + // we can build this sensitive account easily by first getting the public account.... + mastoAccount, err := c.AccountToMastoPublic(a) + if err != nil { + return nil, err + } + + // then adding the Source object to it... + + // check pending follow requests aimed at this account + fr := []gtsmodel.FollowRequest{} + if err := c.db.GetFollowRequestsForAccountID(a.ID, &fr); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, fmt.Errorf("error getting follow requests: %s", err) + } + } + var frc int + if fr != nil { + frc = len(fr) + } + + mastoAccount.Source = &mastotypes.Source{ + Privacy: util.ParseMastoVisFromGTSVis(a.Privacy), + Sensitive: a.Sensitive, + Language: a.Language, + Note: a.Note, + Fields: mastoAccount.Fields, + FollowRequestsCount: frc, + } + + return mastoAccount, nil +} + +func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Account, error) { + // count followers + followers := []gtsmodel.Follow{} + if err := c.db.GetFollowersByAccountID(a.ID, &followers); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, fmt.Errorf("error getting followers: %s", err) + } + } + var followersCount int + if followers != nil { + followersCount = len(followers) + } + + // count following + following := []gtsmodel.Follow{} + if err := c.db.GetFollowingByAccountID(a.ID, &following); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, fmt.Errorf("error getting following: %s", err) + } + } + var followingCount int + if following != nil { + followingCount = len(following) + } + + // count statuses + statuses := []gtsmodel.Status{} + if err := c.db.GetStatusesByAccountID(a.ID, &statuses); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, fmt.Errorf("error getting last statuses: %s", err) + } + } + var statusesCount int + if statuses != nil { + statusesCount = len(statuses) + } + + // check when the last status was + lastStatus := >smodel.Status{} + if err := c.db.GetLastStatusForAccountID(a.ID, lastStatus); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, fmt.Errorf("error getting last status: %s", err) + } + } + var lastStatusAt string + if lastStatus != nil { + lastStatusAt = lastStatus.CreatedAt.Format(time.RFC3339) + } + + // build the avatar and header URLs + avi := >smodel.MediaAttachment{} + if err := c.db.GetAvatarForAccountID(avi, a.ID); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, fmt.Errorf("error getting avatar: %s", err) + } + } + aviURL := avi.File.Path + aviURLStatic := avi.Thumbnail.Path + + header := >smodel.MediaAttachment{} + if err := c.db.GetHeaderForAccountID(avi, a.ID); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, fmt.Errorf("error getting header: %s", err) + } + } + headerURL := header.File.Path + headerURLStatic := header.Thumbnail.Path + + // get the fields set on this account + fields := []mastotypes.Field{} + for _, f := range a.Fields { + mField := mastotypes.Field{ + Name: f.Name, + Value: f.Value, + } + if !f.VerifiedAt.IsZero() { + mField.VerifiedAt = f.VerifiedAt.Format(time.RFC3339) + } + fields = append(fields, mField) + } + + var acct string + if a.Domain != "" { + // this is a remote user + acct = fmt.Sprintf("%s@%s", a.Username, a.Domain) + } else { + // this is a local user + acct = a.Username + } + + return &mastotypes.Account{ + ID: a.ID, + Username: a.Username, + Acct: acct, + DisplayName: a.DisplayName, + Locked: a.Locked, + Bot: a.Bot, + CreatedAt: a.CreatedAt.Format(time.RFC3339), + Note: a.Note, + URL: a.URL, + Avatar: aviURL, + AvatarStatic: aviURLStatic, + Header: headerURL, + HeaderStatic: headerURLStatic, + FollowersCount: followersCount, + FollowingCount: followingCount, + StatusesCount: statusesCount, + LastStatusAt: lastStatusAt, + Emojis: nil, // TODO: implement this + Fields: fields, + }, nil +} + +func (c *converter) AppToMastoSensitive(a *gtsmodel.Application) (*mastotypes.Application, error) { + return &mastotypes.Application{ + ID: a.ID, + Name: a.Name, + Website: a.Website, + RedirectURI: a.RedirectURI, + ClientID: a.ClientID, + ClientSecret: a.ClientSecret, + VapidKey: a.VapidKey, + }, nil +} + +func (c *converter) AppToMastoPublic(a *gtsmodel.Application) (*mastotypes.Application, error) { + return &mastotypes.Application{ + Name: a.Name, + Website: a.Website, + }, nil +} + +func (c *converter) AttachmentToMasto(a *gtsmodel.MediaAttachment) (mastotypes.Attachment, error) { + return mastotypes.Attachment{ + ID: a.ID, + Type: string(a.Type), + URL: a.URL, + PreviewURL: a.Thumbnail.URL, + RemoteURL: a.RemoteURL, + PreviewRemoteURL: a.Thumbnail.RemoteURL, + Meta: mastotypes.MediaMeta{ + Original: mastotypes.MediaDimensions{ + Width: a.FileMeta.Original.Width, + Height: a.FileMeta.Original.Height, + Size: fmt.Sprintf("%dx%d", a.FileMeta.Original.Width, a.FileMeta.Original.Height), + Aspect: float32(a.FileMeta.Original.Aspect), + }, + Small: mastotypes.MediaDimensions{ + Width: a.FileMeta.Small.Width, + Height: a.FileMeta.Small.Height, + Size: fmt.Sprintf("%dx%d", a.FileMeta.Small.Width, a.FileMeta.Small.Height), + Aspect: float32(a.FileMeta.Small.Aspect), + }, + }, + Description: a.Description, + Blurhash: a.Blurhash, + }, nil +} + +func (c *converter) MentionToMasto(m *gtsmodel.Mention) (mastotypes.Mention, error) { + target := >smodel.Account{} + if err := c.db.GetByID(m.TargetAccountID, target); err != nil { + return mastotypes.Mention{}, err + } + + var local bool + if target.Domain == "" { + local = true + } + + var acct string + if local { + acct = fmt.Sprintf("@%s", target.Username) + } else { + acct = fmt.Sprintf("@%s@%s", target.Username, target.Domain) + } + + return mastotypes.Mention{ + ID: m.ID, + Username: target.Username, + URL: target.URL, + Acct: acct, + }, nil +} diff --git a/pkg/mastotypes/README.md b/internal/mastotypes/mastomodel/README.md similarity index 100% rename from pkg/mastotypes/README.md rename to internal/mastotypes/mastomodel/README.md diff --git a/pkg/mastotypes/account.go b/internal/mastotypes/mastomodel/account.go similarity index 100% rename from pkg/mastotypes/account.go rename to internal/mastotypes/mastomodel/account.go diff --git a/pkg/mastotypes/activity.go b/internal/mastotypes/mastomodel/activity.go similarity index 100% rename from pkg/mastotypes/activity.go rename to internal/mastotypes/mastomodel/activity.go diff --git a/pkg/mastotypes/admin.go b/internal/mastotypes/mastomodel/admin.go similarity index 100% rename from pkg/mastotypes/admin.go rename to internal/mastotypes/mastomodel/admin.go diff --git a/pkg/mastotypes/announcement.go b/internal/mastotypes/mastomodel/announcement.go similarity index 100% rename from pkg/mastotypes/announcement.go rename to internal/mastotypes/mastomodel/announcement.go diff --git a/pkg/mastotypes/announcementreaction.go b/internal/mastotypes/mastomodel/announcementreaction.go similarity index 100% rename from pkg/mastotypes/announcementreaction.go rename to internal/mastotypes/mastomodel/announcementreaction.go diff --git a/pkg/mastotypes/application.go b/internal/mastotypes/mastomodel/application.go similarity index 100% rename from pkg/mastotypes/application.go rename to internal/mastotypes/mastomodel/application.go diff --git a/pkg/mastotypes/attachment.go b/internal/mastotypes/mastomodel/attachment.go similarity index 96% rename from pkg/mastotypes/attachment.go rename to internal/mastotypes/mastomodel/attachment.go index 4d4d0955a..bda79a8ee 100644 --- a/pkg/mastotypes/attachment.go +++ b/internal/mastotypes/mastomodel/attachment.go @@ -45,8 +45,10 @@ type Attachment struct { URL string `json:"url"` // The location of a scaled-down preview of the attachment. PreviewURL string `json:"preview_url"` - // The location of the full-size original attachment on the remote website. + // The location of the full-size original attachment on the remote server. RemoteURL string `json:"remote_url,omitempty"` + // The location of a scaled-down preview of the attachment on the remote server. + PreviewRemoteURL string `json:"preview_remote_url,omitempty"` // A shorter URL for the attachment. TextURL string `json:"text_url,omitempty"` // Metadata returned by Paperclip. diff --git a/pkg/mastotypes/card.go b/internal/mastotypes/mastomodel/card.go similarity index 100% rename from pkg/mastotypes/card.go rename to internal/mastotypes/mastomodel/card.go diff --git a/pkg/mastotypes/context.go b/internal/mastotypes/mastomodel/context.go similarity index 100% rename from pkg/mastotypes/context.go rename to internal/mastotypes/mastomodel/context.go diff --git a/pkg/mastotypes/conversation.go b/internal/mastotypes/mastomodel/conversation.go similarity index 100% rename from pkg/mastotypes/conversation.go rename to internal/mastotypes/mastomodel/conversation.go diff --git a/pkg/mastotypes/emoji.go b/internal/mastotypes/mastomodel/emoji.go similarity index 100% rename from pkg/mastotypes/emoji.go rename to internal/mastotypes/mastomodel/emoji.go diff --git a/pkg/mastotypes/error.go b/internal/mastotypes/mastomodel/error.go similarity index 100% rename from pkg/mastotypes/error.go rename to internal/mastotypes/mastomodel/error.go diff --git a/pkg/mastotypes/featuredtag.go b/internal/mastotypes/mastomodel/featuredtag.go similarity index 100% rename from pkg/mastotypes/featuredtag.go rename to internal/mastotypes/mastomodel/featuredtag.go diff --git a/pkg/mastotypes/field.go b/internal/mastotypes/mastomodel/field.go similarity index 100% rename from pkg/mastotypes/field.go rename to internal/mastotypes/mastomodel/field.go diff --git a/pkg/mastotypes/filter.go b/internal/mastotypes/mastomodel/filter.go similarity index 100% rename from pkg/mastotypes/filter.go rename to internal/mastotypes/mastomodel/filter.go diff --git a/pkg/mastotypes/history.go b/internal/mastotypes/mastomodel/history.go similarity index 100% rename from pkg/mastotypes/history.go rename to internal/mastotypes/mastomodel/history.go diff --git a/pkg/mastotypes/identityproof.go b/internal/mastotypes/mastomodel/identityproof.go similarity index 100% rename from pkg/mastotypes/identityproof.go rename to internal/mastotypes/mastomodel/identityproof.go diff --git a/pkg/mastotypes/instance.go b/internal/mastotypes/mastomodel/instance.go similarity index 100% rename from pkg/mastotypes/instance.go rename to internal/mastotypes/mastomodel/instance.go diff --git a/pkg/mastotypes/list.go b/internal/mastotypes/mastomodel/list.go similarity index 100% rename from pkg/mastotypes/list.go rename to internal/mastotypes/mastomodel/list.go diff --git a/pkg/mastotypes/marker.go b/internal/mastotypes/mastomodel/marker.go similarity index 100% rename from pkg/mastotypes/marker.go rename to internal/mastotypes/mastomodel/marker.go diff --git a/pkg/mastotypes/mention.go b/internal/mastotypes/mastomodel/mention.go similarity index 100% rename from pkg/mastotypes/mention.go rename to internal/mastotypes/mastomodel/mention.go diff --git a/pkg/mastotypes/notification.go b/internal/mastotypes/mastomodel/notification.go similarity index 100% rename from pkg/mastotypes/notification.go rename to internal/mastotypes/mastomodel/notification.go diff --git a/pkg/mastotypes/oauth.go b/internal/mastotypes/mastomodel/oauth.go similarity index 100% rename from pkg/mastotypes/oauth.go rename to internal/mastotypes/mastomodel/oauth.go diff --git a/pkg/mastotypes/poll.go b/internal/mastotypes/mastomodel/poll.go similarity index 100% rename from pkg/mastotypes/poll.go rename to internal/mastotypes/mastomodel/poll.go diff --git a/pkg/mastotypes/preferences.go b/internal/mastotypes/mastomodel/preferences.go similarity index 100% rename from pkg/mastotypes/preferences.go rename to internal/mastotypes/mastomodel/preferences.go diff --git a/pkg/mastotypes/pushsubscription.go b/internal/mastotypes/mastomodel/pushsubscription.go similarity index 100% rename from pkg/mastotypes/pushsubscription.go rename to internal/mastotypes/mastomodel/pushsubscription.go diff --git a/pkg/mastotypes/relationship.go b/internal/mastotypes/mastomodel/relationship.go similarity index 100% rename from pkg/mastotypes/relationship.go rename to internal/mastotypes/mastomodel/relationship.go diff --git a/pkg/mastotypes/results.go b/internal/mastotypes/mastomodel/results.go similarity index 100% rename from pkg/mastotypes/results.go rename to internal/mastotypes/mastomodel/results.go diff --git a/pkg/mastotypes/scheduledstatus.go b/internal/mastotypes/mastomodel/scheduledstatus.go similarity index 100% rename from pkg/mastotypes/scheduledstatus.go rename to internal/mastotypes/mastomodel/scheduledstatus.go diff --git a/pkg/mastotypes/source.go b/internal/mastotypes/mastomodel/source.go similarity index 100% rename from pkg/mastotypes/source.go rename to internal/mastotypes/mastomodel/source.go diff --git a/pkg/mastotypes/status.go b/internal/mastotypes/mastomodel/status.go similarity index 100% rename from pkg/mastotypes/status.go rename to internal/mastotypes/mastomodel/status.go diff --git a/pkg/mastotypes/tag.go b/internal/mastotypes/mastomodel/tag.go similarity index 100% rename from pkg/mastotypes/tag.go rename to internal/mastotypes/mastomodel/tag.go diff --git a/pkg/mastotypes/token.go b/internal/mastotypes/mastomodel/token.go similarity index 100% rename from pkg/mastotypes/token.go rename to internal/mastotypes/mastomodel/token.go diff --git a/internal/mastotypes/mock_Converter.go b/internal/mastotypes/mock_Converter.go new file mode 100644 index 000000000..881bc48aa --- /dev/null +++ b/internal/mastotypes/mock_Converter.go @@ -0,0 +1,106 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mastotypes + +import ( + mock "github.com/stretchr/testify/mock" + gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" +) + +// MockConverter is an autogenerated mock type for the Converter type +type MockConverter struct { + mock.Mock +} + +// AccountToMastoPublic provides a mock function with given fields: account +func (_m *MockConverter) AccountToMastoPublic(account *gtsmodel.Account) (*mastotypes.Account, error) { + ret := _m.Called(account) + + var r0 *mastotypes.Account + if rf, ok := ret.Get(0).(func(*gtsmodel.Account) *mastotypes.Account); ok { + r0 = rf(account) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mastotypes.Account) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*gtsmodel.Account) error); ok { + r1 = rf(account) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AccountToMastoSensitive provides a mock function with given fields: account +func (_m *MockConverter) AccountToMastoSensitive(account *gtsmodel.Account) (*mastotypes.Account, error) { + ret := _m.Called(account) + + var r0 *mastotypes.Account + if rf, ok := ret.Get(0).(func(*gtsmodel.Account) *mastotypes.Account); ok { + r0 = rf(account) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mastotypes.Account) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*gtsmodel.Account) error); ok { + r1 = rf(account) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AppToMastoPublic provides a mock function with given fields: application +func (_m *MockConverter) AppToMastoPublic(application *gtsmodel.Application) (*mastotypes.Application, error) { + ret := _m.Called(application) + + var r0 *mastotypes.Application + if rf, ok := ret.Get(0).(func(*gtsmodel.Application) *mastotypes.Application); ok { + r0 = rf(application) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mastotypes.Application) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*gtsmodel.Application) error); ok { + r1 = rf(application) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AppToMastoSensitive provides a mock function with given fields: application +func (_m *MockConverter) AppToMastoSensitive(application *gtsmodel.Application) (*mastotypes.Application, error) { + ret := _m.Called(application) + + var r0 *mastotypes.Application + if rf, ok := ret.Get(0).(func(*gtsmodel.Application) *mastotypes.Application); ok { + r0 = rf(application) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mastotypes.Application) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*gtsmodel.Application) error); ok { + r1 = rf(application) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/internal/media/media.go b/internal/media/media.go index d25fd258d..104342e6e 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -28,7 +28,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/storage" ) @@ -37,7 +37,7 @@ type MediaHandler interface { // SetHeaderOrAvatarForAccountID 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. - SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*model.MediaAttachment, error) + SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) } type mediaHandler struct { @@ -68,7 +68,7 @@ type HeaderInfo struct { INTERFACE FUNCTIONS */ -func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*model.MediaAttachment, error) { +func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) { l := mh.log.WithField("func", "SetHeaderForAccountID") if headerOrAvi != "header" && headerOrAvi != "avatar" { @@ -107,7 +107,7 @@ func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID stri HELPER FUNCTIONS */ -func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, headerOrAvi string, accountID string) (*model.MediaAttachment, error) { +func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, headerOrAvi string, accountID string) (*gtsmodel.MediaAttachment, error) { var isHeader bool var isAvatar bool @@ -152,34 +152,38 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string extension := strings.Split(contentType, "/")[1] newMediaID := uuid.NewString() - base := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) + URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) + originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, headerOrAvi, newMediaID, extension) + smallURL := fmt.Sprintf("%s/%s/%s/small/%s.%s", URLbase, accountID, headerOrAvi, newMediaID, extension) // we store the original... - originalPath := fmt.Sprintf("%s/%s/%s/original/%s.%s", base, accountID, headerOrAvi, newMediaID, extension) + originalPath := fmt.Sprintf("%s/%s/%s/original/%s.%s", mh.config.StorageConfig.BasePath, accountID, headerOrAvi, newMediaID, extension) if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { return nil, fmt.Errorf("storage error: %s", err) } + // and a thumbnail... - smallPath := fmt.Sprintf("%s/%s/%s/small/%s.%s", base, accountID, headerOrAvi, newMediaID, extension) + smallPath := fmt.Sprintf("%s/%s/%s/small/%s.%s", mh.config.StorageConfig.BasePath, accountID, headerOrAvi, newMediaID, extension) if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { return nil, fmt.Errorf("storage error: %s", err) } - ma := &model.MediaAttachment{ + ma := >smodel.MediaAttachment{ ID: newMediaID, StatusID: "", + URL: originalURL, RemoteURL: "", CreatedAt: time.Now(), UpdatedAt: time.Now(), - Type: model.FileTypeImage, - FileMeta: model.FileMeta{ - Original: model.Original{ + Type: gtsmodel.FileTypeImage, + FileMeta: gtsmodel.FileMeta{ + Original: gtsmodel.Original{ Width: original.width, Height: original.height, Size: original.size, Aspect: original.aspect, }, - Small: model.Small{ + Small: gtsmodel.Small{ Width: small.width, Height: small.height, Size: small.size, @@ -191,17 +195,18 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string ScheduledStatusID: "", Blurhash: original.blurhash, Processing: 2, - File: model.File{ + File: gtsmodel.File{ Path: originalPath, ContentType: contentType, FileSize: len(original.image), UpdatedAt: time.Now(), }, - Thumbnail: model.Thumbnail{ + Thumbnail: gtsmodel.Thumbnail{ Path: smallPath, ContentType: contentType, FileSize: len(small.image), UpdatedAt: time.Now(), + URL: smallURL, RemoteURL: "", }, Avatar: isAvatar, diff --git a/internal/media/media_test.go b/internal/media/media_test.go index ae5896c38..3ce9dce61 100644 --- a/internal/media/media_test.go +++ b/internal/media/media_test.go @@ -29,7 +29,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/storage" ) @@ -108,8 +108,8 @@ func (suite *MediaTestSuite) TearDownSuite() { func (suite *MediaTestSuite) SetupTest() { // create all the tables we might need in thie suite models := []interface{}{ - &model.Account{}, - &model.MediaAttachment{}, + >smodel.Account{}, + >smodel.MediaAttachment{}, } for _, m := range models { if err := suite.db.CreateTable(m); err != nil { @@ -123,8 +123,8 @@ func (suite *MediaTestSuite) TearDownTest() { // remove all the tables we might have used so it's clear for the next test models := []interface{}{ - &model.Account{}, - &model.MediaAttachment{}, + >smodel.Account{}, + >smodel.MediaAttachment{}, } for _, m := range models { if err := suite.db.DropTable(m); err != nil { diff --git a/internal/media/mock_MediaHandler.go b/internal/media/mock_MediaHandler.go index 0299d307e..6f04f1fe7 100644 --- a/internal/media/mock_MediaHandler.go +++ b/internal/media/mock_MediaHandler.go @@ -4,7 +4,7 @@ package media import ( mock "github.com/stretchr/testify/mock" - model "github.com/superseriousbusiness/gotosocial/internal/db/model" + gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" ) // MockMediaHandler is an autogenerated mock type for the MediaHandler type @@ -13,15 +13,15 @@ type MockMediaHandler struct { } // SetHeaderOrAvatarForAccountID provides a mock function with given fields: img, accountID, headerOrAvi -func (_m *MockMediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*model.MediaAttachment, error) { +func (_m *MockMediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) { ret := _m.Called(img, accountID, headerOrAvi) - var r0 *model.MediaAttachment - if rf, ok := ret.Get(0).(func([]byte, string, string) *model.MediaAttachment); ok { + var r0 *gtsmodel.MediaAttachment + if rf, ok := ret.Get(0).(func([]byte, string, string) *gtsmodel.MediaAttachment); ok { r0 = rf(img, accountID, headerOrAvi) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.MediaAttachment) + r0 = ret.Get(0).(*gtsmodel.MediaAttachment) } } diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 8bac8fc2f..538288922 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -26,7 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/errors" "github.com/superseriousbusiness/oauth2/v4/manage" @@ -34,6 +34,9 @@ import ( ) const ( + // SessionAuthorizedToken is the key set in the gin context for the Token + // of a User who has successfully passed Bearer token authorization. + // The interface returned from grabbing this key should be parsed as oauth2.TokenInfo SessionAuthorizedToken = "authorized_token" // SessionAuthorizedUser is the key set in the gin context for the id of // a User who has successfully passed Bearer token authorization. @@ -65,9 +68,9 @@ type s struct { type Authed struct { Token oauth2.TokenInfo - Application *model.Application - User *model.User - Account *model.Account + Application *gtsmodel.Application + User *gtsmodel.User + Account *gtsmodel.Account } // GetAuthed is a convenience function for returning an Authed struct from a gin context. @@ -96,7 +99,7 @@ func GetAuthed(c *gin.Context) (*Authed, error) { i, ok = ctx.Get(SessionAuthorizedApplication) if ok { - parsed, ok := i.(*model.Application) + parsed, ok := i.(*gtsmodel.Application) if !ok { return nil, errors.New("could not parse application from session context") } @@ -105,7 +108,7 @@ func GetAuthed(c *gin.Context) (*Authed, error) { i, ok = ctx.Get(SessionAuthorizedUser) if ok { - parsed, ok := i.(*model.User) + parsed, ok := i.(*gtsmodel.User) if !ok { return nil, errors.New("could not parse user from session context") } @@ -114,7 +117,7 @@ func GetAuthed(c *gin.Context) (*Authed, error) { i, ok = ctx.Get(SessionAuthorizedAccount) if ok { - parsed, ok := i.(*model.Account) + parsed, ok := i.(*gtsmodel.Account) if !ok { return nil, errors.New("could not parse account from session context") } diff --git a/internal/util/parse.go b/internal/util/parse.go index 92baac6bf..9f3f7fad5 100644 --- a/internal/util/parse.go +++ b/internal/util/parse.go @@ -21,8 +21,8 @@ package util import ( "fmt" - "github.com/superseriousbusiness/gotosocial/internal/db/model" - "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" ) type URIs struct { @@ -64,16 +64,16 @@ func GenerateURIs(username string, protocol string, host string) *URIs { } // ParseGTSVisFromMastoVis converts a mastodon visibility into its gts equivalent. -func ParseGTSVisFromMastoVis(m mastotypes.Visibility) model.Visibility { +func ParseGTSVisFromMastoVis(m mastotypes.Visibility) gtsmodel.Visibility { switch m { case mastotypes.VisibilityPublic: - return model.VisibilityPublic + return gtsmodel.VisibilityPublic case mastotypes.VisibilityUnlisted: - return model.VisibilityUnlocked + return gtsmodel.VisibilityUnlocked case mastotypes.VisibilityPrivate: - return model.VisibilityFollowersOnly + return gtsmodel.VisibilityFollowersOnly case mastotypes.VisibilityDirect: - return model.VisibilityDirect + return gtsmodel.VisibilityDirect default: break } @@ -81,15 +81,15 @@ func ParseGTSVisFromMastoVis(m mastotypes.Visibility) model.Visibility { } // ParseMastoVisFromGTSVis converts a gts visibility into its mastodon equivalent -func ParseMastoVisFromGTSVis(m model.Visibility) mastotypes.Visibility { +func ParseMastoVisFromGTSVis(m gtsmodel.Visibility) mastotypes.Visibility { switch m { - case model.VisibilityPublic: + case gtsmodel.VisibilityPublic: return mastotypes.VisibilityPublic - case model.VisibilityUnlocked: + case gtsmodel.VisibilityUnlocked: return mastotypes.VisibilityUnlisted - case model.VisibilityFollowersOnly, model.VisibilityMutualsOnly: + case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: return mastotypes.VisibilityPrivate - case model.VisibilityDirect: + case gtsmodel.VisibilityDirect: return mastotypes.VisibilityDirect default: break diff --git a/internal/util/status.go b/internal/util/status.go index a6dbb9583..bc091b3d8 100644 --- a/internal/util/status.go +++ b/internal/util/status.go @@ -19,16 +19,13 @@ package util import ( - "fmt" "regexp" "strings" ) -// To play around with these regexes, see: https://regex101.com/r/2km2EK/1 var ( - // mention regex can be played around with here: https://regex101.com/r/2km2EK/1 - hostnameRegexString = `(?:(?:[a-zA-Z]{1})|(?:[a-zA-Z]{1}[a-zA-Z]{1})|(?:[a-zA-Z]{1}[0-9]{1})|(?:[0-9]{1}[a-zA-Z]{1})|(?:[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.(?:[a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,5}))` - mentionRegexString = fmt.Sprintf(`(?: |^|\W)(@[a-zA-Z0-9_]+@%s(?: |\n)`, hostnameRegexString) + // mention regex can be played around with here: https://regex101.com/r/qwM9D3/1 + mentionRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?: |\n)` mentionRegex = regexp.MustCompile(mentionRegexString) // hashtag regex can be played with here: https://regex101.com/r/Vhy8pg/1 hashtagRegexString = `(?: |^|\W)?#([a-zA-Z0-9]{1,30})(?:\b|\r)` @@ -43,7 +40,7 @@ var ( // mentioned in that status. // // It will look for fully-qualified account names in the form "@user@example.org". -// Mentions that are just in the form "@username" will not be detected. +// or the form "@username" for local users. // The case of the returned mentions will be lowered, for consistency. func DeriveMentions(status string) []string { mentionedAccounts := []string{} diff --git a/internal/util/status_test.go b/internal/util/status_test.go index e2079659b..72bd3e885 100644 --- a/internal/util/status_test.go +++ b/internal/util/status_test.go @@ -36,16 +36,17 @@ func (suite *StatusTestSuite) TestDeriveMentionsOK() { @someone_else@testing.best-horse.com can you confirm? @hello@test.lgbt - @thiswontwork though! @NORWILL@THIS.one!! + @thisisalocaluser ! @NORWILL@THIS.one!! here is a duplicate mention: @hello@test.lgbt ` menchies := DeriveMentions(statusText) - assert.Len(suite.T(), menchies, 3) + assert.Len(suite.T(), menchies, 4) assert.Equal(suite.T(), "@dumpsterqueer@example.org", menchies[0]) assert.Equal(suite.T(), "@someone_else@testing.best-horse.com", menchies[1]) assert.Equal(suite.T(), "@hello@test.lgbt", menchies[2]) + assert.Equal(suite.T(), "@thisisalocaluser", menchies[3]) } func (suite *StatusTestSuite) TestDeriveMentionsEmpty() { diff --git a/testrig/db.go b/testrig/db.go index fc2401ec4..176e8dada 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -2,23 +2,23 @@ package testrig import ( "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) var testModels []interface{} = []interface{}{ - &model.Account{}, - &model.Application{}, - &model.Block{}, - &model.DomainBlock{}, - &model.EmailDomainBlock{}, - &model.Follow{}, - &model.FollowRequest{}, - &model.MediaAttachment{}, - &model.Mention{}, - &model.Status{}, - &model.Tag{}, - &model.User{}, + >smodel.Account{}, + >smodel.Application{}, + >smodel.Block{}, + >smodel.DomainBlock{}, + >smodel.EmailDomainBlock{}, + >smodel.Follow{}, + >smodel.FollowRequest{}, + >smodel.MediaAttachment{}, + >smodel.Mention{}, + >smodel.Status{}, + >smodel.Tag{}, + >smodel.User{}, &oauth.Token{}, &oauth.Client{}, } @@ -61,6 +61,12 @@ func StandardDBSetup(db db.DB) error { } } + for _, v := range TestStatuses() { + if err := db.Put(v); err != nil { + return err + } + } + return nil } diff --git a/testrig/models.go b/testrig/models.go index bacb22f9c..f28ed74ed 100644 --- a/testrig/models.go +++ b/testrig/models.go @@ -6,20 +6,20 @@ import ( "net" "time" - "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) func TestTokens() map[string]*oauth.Token { tokens := map[string]*oauth.Token{ "local_account_1": { - ID: "64cf4214-33ab-4220-b5ca-4a6a12263b20", - ClientID: "73b48d42-029d-4487-80fc-329a5cf67869", - UserID: "44e36b79-44a4-4bd8-91e9-097f477fe97b", - RedirectURI: "http://localhost:8080", - Scope: "read write follow push", - Access: "NZAZOTC0OWITMDU0NC0ZODG4LWE4NJITMWUXM2M4MTRHZDEX", - AccessCreateAt: time.Now(), + ID: "64cf4214-33ab-4220-b5ca-4a6a12263b20", + ClientID: "73b48d42-029d-4487-80fc-329a5cf67869", + UserID: "44e36b79-44a4-4bd8-91e9-097f477fe97b", + RedirectURI: "http://localhost:8080", + Scope: "read write follow push", + Access: "NZAZOTC0OWITMDU0NC0ZODG4LWE4NJITMWUXM2M4MTRHZDEX", + AccessCreateAt: time.Now(), AccessExpiresAt: time.Now().Add(72 * time.Hour), }, } @@ -38,8 +38,8 @@ func TestClients() map[string]*oauth.Client { return clients } -func TestApplications() map[string]*model.Application { - apps := map[string]*model.Application{ +func TestApplications() map[string]*gtsmodel.Application { + apps := map[string]*gtsmodel.Application{ "application_1": { ID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", Name: "really cool gts application", @@ -54,8 +54,8 @@ func TestApplications() map[string]*model.Application { return apps } -func TestUsers() map[string]*model.User { - users := map[string]*model.User{ +func TestUsers() map[string]*gtsmodel.User { + users := map[string]*gtsmodel.User{ "unconfirmed_account": { ID: "0f7b1d24-1e49-4ee0-bc7e-fd87b7289eea", Email: "", @@ -181,8 +181,8 @@ func TestUsers() map[string]*model.User { return users } -func TestAccounts() map[string]*model.Account { - accounts := map[string]*model.Account{ +func TestAccounts() map[string]*gtsmodel.Account { + accounts := map[string]*gtsmodel.Account{ "unconfirmed_account": { ID: "59e197f5-87cd-4be8-ac7c-09082ccc4b4d", Username: "weed_lord420", @@ -197,7 +197,7 @@ func TestAccounts() map[string]*model.Account { HeaderUpdatedAt: time.Time{}, HeaderRemoteURL: "", DisplayName: "", - Fields: []model.Field{}, + Fields: []gtsmodel.Field{}, Note: "", Memorial: false, MovedToAccountID: "", @@ -207,7 +207,7 @@ func TestAccounts() map[string]*model.Account { Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.", Locked: false, Discoverable: false, - Privacy: model.VisibilityPublic, + Privacy: gtsmodel.VisibilityPublic, Sensitive: false, Language: "en", URI: "http://localhost:8080/users/weed_lord420", @@ -218,7 +218,7 @@ func TestAccounts() map[string]*model.Account { SharedInboxURL: "", FollowersURL: "http://localhost:8080/users/weed_lord420/followers", FeaturedCollectionURL: "http://localhost:8080/users/weed_lord420/collections/featured", - ActorType: model.ActivityStreamsPerson, + ActorType: gtsmodel.ActivityStreamsPerson, AlsoKnownAs: "", PrivateKey: &rsa.PrivateKey{}, PublicKey: &rsa.PublicKey{}, @@ -242,7 +242,7 @@ func TestAccounts() map[string]*model.Account { HeaderUpdatedAt: time.Time{}, HeaderRemoteURL: "", DisplayName: "", - Fields: []model.Field{}, + Fields: []gtsmodel.Field{}, Note: "", Memorial: false, MovedToAccountID: "", @@ -252,7 +252,7 @@ func TestAccounts() map[string]*model.Account { Reason: "", Locked: false, Discoverable: true, - Privacy: model.VisibilityPublic, + Privacy: gtsmodel.VisibilityPublic, Sensitive: false, Language: "en", URI: "http://localhost:8080/users/admin", @@ -263,7 +263,7 @@ func TestAccounts() map[string]*model.Account { SharedInboxURL: "", FollowersURL: "http://localhost:8080/users/admin/followers", FeaturedCollectionURL: "http://localhost:8080/users/admin/collections/featured", - ActorType: model.ActivityStreamsPerson, + ActorType: gtsmodel.ActivityStreamsPerson, AlsoKnownAs: "", PrivateKey: &rsa.PrivateKey{}, PublicKey: &rsa.PublicKey{}, @@ -287,7 +287,7 @@ func TestAccounts() map[string]*model.Account { HeaderUpdatedAt: time.Time{}, HeaderRemoteURL: "", DisplayName: "original zork (he/they)", - Fields: []model.Field{}, + Fields: []gtsmodel.Field{}, Note: "hey yo this is my profile!", Memorial: false, MovedToAccountID: "", @@ -297,7 +297,7 @@ func TestAccounts() map[string]*model.Account { Reason: "I wanna be on this damned webbed site so bad! Please! Wow", Locked: false, Discoverable: true, - Privacy: model.VisibilityPublic, + Privacy: gtsmodel.VisibilityPublic, Sensitive: false, Language: "en", URI: "http://localhost:8080/users/the_mighty_zork", @@ -308,7 +308,7 @@ func TestAccounts() map[string]*model.Account { SharedInboxURL: "", FollowersURL: "http://localhost:8080/users/the_mighty_zork/followers", FeaturedCollectionURL: "http://localhost:8080/users/the_mighty_zork/collections/featured", - ActorType: model.ActivityStreamsPerson, + ActorType: gtsmodel.ActivityStreamsPerson, AlsoKnownAs: "", PrivateKey: &rsa.PrivateKey{}, PublicKey: &rsa.PublicKey{}, @@ -332,7 +332,7 @@ func TestAccounts() map[string]*model.Account { HeaderUpdatedAt: time.Time{}, HeaderRemoteURL: "", DisplayName: "happy little turtle :3", - Fields: []model.Field{}, + Fields: []gtsmodel.Field{}, Note: "i post about things that concern me", Memorial: false, MovedToAccountID: "", @@ -342,7 +342,7 @@ func TestAccounts() map[string]*model.Account { Reason: "", Locked: true, Discoverable: false, - Privacy: model.VisibilityFollowersOnly, + Privacy: gtsmodel.VisibilityFollowersOnly, Sensitive: false, Language: "en", URI: "http://localhost:8080/users/1happyturtle", @@ -353,7 +353,7 @@ func TestAccounts() map[string]*model.Account { SharedInboxURL: "", FollowersURL: "http://localhost:8080/users/1happyturtle/followers", FeaturedCollectionURL: "http://localhost:8080/users/1happyturtle/collections/featured", - ActorType: model.ActivityStreamsPerson, + ActorType: gtsmodel.ActivityStreamsPerson, AlsoKnownAs: "", PrivateKey: &rsa.PrivateKey{}, PublicKey: &rsa.PublicKey{}, @@ -378,7 +378,7 @@ func TestAccounts() map[string]*model.Account { // HeaderUpdatedAt: time.Time{}, // HeaderRemoteURL: "", DisplayName: "big gerald", - Fields: []model.Field{}, + Fields: []gtsmodel.Field{}, Note: "i post about like, i dunno, stuff, or whatever!!!!", Memorial: false, MovedToAccountID: "", @@ -397,7 +397,7 @@ func TestAccounts() map[string]*model.Account { SharedInboxURL: "", FollowersURL: "https://fossbros-anonymous.io/users/foss_satan/followers", FeaturedCollectionURL: "https://fossbros-anonymous.io/users/foss_satan/collections/featured", - ActorType: model.ActivityStreamsPerson, + ActorType: gtsmodel.ActivityStreamsPerson, AlsoKnownAs: "", PrivateKey: &rsa.PrivateKey{}, PublicKey: nil, @@ -440,53 +440,168 @@ func TestAccounts() map[string]*model.Account { return accounts } -func TestStatuses() map[string]*model.Status { - return map[string]*model.Status{ - "local_account_1_status_1": { - ID: "91b1e795-74ff-4672-a4c4-476616710e2d", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", - URL: "http://localhost:8080/@the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", - Content: "hello everyone!", - CreatedAt: time.Now().Add(-47 * time.Hour), - UpdatedAt: time.Now().Add(-47 * time.Hour), - Local: true, - AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "introduction post", - Visibility: model.VisibilityPublic, - Sensitive: true, - Language: "en", - VisibilityAdvanced: &model.VisibilityAdvanced{ +func TestStatuses() map[string]*gtsmodel.Status { + return map[string]*gtsmodel.Status{ + "admin_account_status_1": { + ID: "502ccd6f-0edf-48d7-9016-2dfa4d3714cd", + URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", + URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", + Content: "hello world! first post on the instance!", + CreatedAt: time.Now().Add(-71 * time.Hour), + UpdatedAt: time.Now().Add(-71 * time.Hour), + Local: true, + AccountID: "0fb02eae-2214-473f-9667-0a43f22d75ff", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: false, + Language: "en", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: true, Replyable: true, - Likeable: true, + Likeable: true, }, - ActivityStreamsType: model.ActivityStreamsNote, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, + }, + "admin_account_status_2": { + ID: "0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", + URI: "http://localhost:8080/users/admin/statuses/0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", + URL: "http://localhost:8080/@admin/statuses/0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", + Content: "🐕🐕🐕🐕🐕", + CreatedAt: time.Now().Add(-70 * time.Hour), + UpdatedAt: time.Now().Add(-70 * time.Hour), + Local: true, + AccountID: "0fb02eae-2214-473f-9667-0a43f22d75ff", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "open to see some puppies", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: true, + Language: "en", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ + Federated: true, + Boostable: true, + Replyable: true, + Likeable: true, + }, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, + }, + "local_account_1_status_1": { + ID: "91b1e795-74ff-4672-a4c4-476616710e2d", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", + URL: "http://localhost:8080/@the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", + Content: "hello everyone!", + CreatedAt: time.Now().Add(-47 * time.Hour), + UpdatedAt: time.Now().Add(-47 * time.Hour), + Local: true, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "introduction post", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: true, + Language: "en", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ + Federated: true, + Boostable: true, + Replyable: true, + Likeable: true, + }, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "local_account_1_status_2": { - ID: "3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", - URL: "http://localhost:8080/@the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", - Content: "this is an unlocked local-only post that shouldn't federate, but it's still boostable, replyable, and likeable", - CreatedAt: time.Now().Add(-47 * time.Hour), - UpdatedAt: time.Now().Add(-47 * time.Hour), - Local: true, - AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", - InReplyToID: "", - BoostOfID: "", + ID: "3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", + URL: "http://localhost:8080/@the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", + Content: "this is an unlocked local-only post that shouldn't federate, but it's still boostable, replyable, and likeable", + CreatedAt: time.Now().Add(-46 * time.Hour), + UpdatedAt: time.Now().Add(-46 * time.Hour), + Local: true, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + InReplyToID: "", + BoostOfID: "", ContentWarning: "", - Visibility: model.VisibilityUnlocked, - Sensitive: false, - Language: "en", - VisibilityAdvanced: &model.VisibilityAdvanced{ + Visibility: gtsmodel.VisibilityUnlocked, + Sensitive: false, + Language: "en", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: false, Boostable: true, Replyable: true, - Likeable: true, + Likeable: true, }, - ActivityStreamsType: model.ActivityStreamsNote, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, + }, + "local_account_1_status_3": { + ID: "5e41963f-8ab9-4147-9f00-52d56e19da65", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/5e41963f-8ab9-4147-9f00-52d56e19da65", + URL: "http://localhost:8080/@the_mighty_zork/statuses/5e41963f-8ab9-4147-9f00-52d56e19da65", + Content: "this is a very personal post that I don't want anyone to interact with at all, and i only want mutuals to see it", + CreatedAt: time.Now().Add(-45 * time.Hour), + UpdatedAt: time.Now().Add(-45 * time.Hour), + Local: true, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "test: you shouldn't be able to interact with this post in any way", + Visibility: gtsmodel.VisibilityMutualsOnly, + Sensitive: false, + Language: "en", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ + Federated: true, + Boostable: false, + Replyable: false, + Likeable: false, + }, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, + }, + "local_account_2_status_1": { + ID: "8945ccf2-3873-45e9-aa13-fd7163f19775", + URI: "http://localhost:8080/users/1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775", + URL: "http://localhost:8080/@1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775", + Content: "🐢 hi everyone i post about turtles 🐢", + CreatedAt: time.Now().Add(-189 * time.Hour), + UpdatedAt: time.Now().Add(-189 * time.Hour), + Local: true, + AccountID: "eecaad73-5703-426d-9312-276641daa31e", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "introduction post", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: true, + Language: "en", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ + Federated: true, + Boostable: true, + Replyable: true, + Likeable: true, + }, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, + }, + "local_account_2_status_2": { + ID: "c7e25a86-f0d3-4705-a73c-c597f687d3dd", + URI: "http://localhost:8080/users/1happyturtle/statuses/c7e25a86-f0d3-4705-a73c-c597f687d3dd", + URL: "http://localhost:8080/@1happyturtle/statuses/c7e25a86-f0d3-4705-a73c-c597f687d3dd", + Content: "🐢 this one is federated, likeable, and boostable but not replyable 🐢", + CreatedAt: time.Now().Add(-1 * time.Minute), + UpdatedAt: time.Now().Add(-1 * time.Minute), + Local: true, + AccountID: "eecaad73-5703-426d-9312-276641daa31e", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: true, + Language: "en", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ + Federated: true, + Boostable: true, + Replyable: false, + Likeable: true, + }, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, } }