diff options
133 files changed, 5030 insertions, 1476 deletions
@@ -130,9 +130,13 @@ The following libraries and frameworks are used by GoToSocial, with gratitude ðŸ  * [go-fed/activity](https://github.com/go-fed/activity); Golang ActivityPub/ActivityStreams library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).  * [go-fed/httpsig](https://github.com/go-fed/httpsig); secure HTTP signature library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).  * [google/uuid](https://github.com/google/uuid); UUID generation. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html) +* [go-playground/validator](https://github.com/go-playground/validator); struct validation. [MIT License](https://spdx.org/licenses/MIT.html)  * [gorilla/websocket](https://github.com/gorilla/websocket); Websocket connectivity. [BSD-2-Clause License](https://spdx.org/licenses/BSD-2-Clause.html).  * [h2non/filetype](https://github.com/h2non/filetype); filetype checking. [MIT License](https://spdx.org/licenses/MIT.html).  * [jackc/pgx](https://github.com/jackc/pgx); Postgres driver. [MIT License](https://spdx.org/licenses/MIT.html). +* [modernc.org/sqlite](https://gitlab.com/cznic/sqlite); cgo-free port of SQLite. [Other License](https://gitlab.com/cznic/sqlite/-/blob/master/LICENSE). +  * [modernc.org/ccgo](https://gitlab.com/cznic/ccgo); c99 AST -> Go translater. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html). +  * [modernc.org/libc](https://gitlab.com/cznic/libc); C-runtime services. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).  * [microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday); HTML user-input sanitization. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).  * [mvdan/xurls](https://github.com/mvdan/xurls); URL parsing regular expressions. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).  * [nfnt/resize](https://github.com/nfnt/resize); convenient image resizing. [ISC License](https://spdx.org/licenses/ISC.html). @@ -148,9 +152,6 @@ The following libraries and frameworks are used by GoToSocial, with gratitude ðŸ  * [uptrace/bun](https://github.com/uptrace/bun); database ORM. [BSD-2-Clause License](https://spdx.org/licenses/BSD-2-Clause.html).  * [urfave/cli](https://github.com/urfave/cli); command-line interface framework. [MIT License](https://spdx.org/licenses/MIT.html).  * [wagslane/go-password-validator](https://github.com/wagslane/go-password-validator); password strength validation. [MIT License](https://spdx.org/licenses/MIT.html). -* [modernc.org/sqlite](sqlite); cgo-free port of SQLite. [Other License](https://gitlab.com/cznic/sqlite/-/blob/master/LICENSE). -  * [modernc.org/ccgo](ccgo); c99 AST -> Go translater. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html). -  * [modernc.org/libc](libc); C-runtime services. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).  ### Image Attribution @@ -21,7 +21,7 @@ require (  	github.com/go-errors/errors v1.4.0 // indirect  	github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f  	github.com/go-fed/httpsig v1.1.0 -	github.com/go-playground/validator/v10 v10.7.0 // indirect +	github.com/go-playground/validator/v10 v10.7.0  	github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect  	github.com/golang/mock v1.6.0 // indirect  	github.com/golang/protobuf v1.5.2 // indirect diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go new file mode 100644 index 000000000..e00bf3f1c --- /dev/null +++ b/internal/ap/activitystreams.go @@ -0,0 +1,72 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package ap + +// https://www.w3.org/TR/activitystreams-vocabulary +const ( +	ActivityAccept          = "Accept"          // ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept +	ActivityAdd             = "Add"             // ActivityStreamsAdd https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add +	ActivityAnnounce        = "Announce"        // ActivityStreamsAnnounce https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce +	ActivityArrive          = "Arrive"          // ActivityStreamsArrive https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive +	ActivityBlock           = "Block"           // ActivityStreamsBlock https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block +	ActivityCreate          = "Create"          // ActivityStreamsCreate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create +	ActivityDelete          = "Delete"          // ActivityStreamsDelete https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete +	ActivityDislike         = "Dislike"         // ActivityStreamsDislike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike +	ActivityFlag            = "Flag"            // ActivityStreamsFlag https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag +	ActivityFollow          = "Follow"          // ActivityStreamsFollow https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow +	ActivityIgnore          = "Ignore"          // ActivityStreamsIgnore https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore +	ActivityInvite          = "Invite"          // ActivityStreamsInvite https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite +	ActivityJoin            = "Join"            // ActivityStreamsJoin https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join +	ActivityLeave           = "Leave"           // ActivityStreamsLeave https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave +	ActivityLike            = "Like"            // ActivityStreamsLike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like +	ActivityListen          = "Listen"          // ActivityStreamsListen https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen +	ActivityMove            = "Move"            // ActivityStreamsMove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move +	ActivityOffer           = "Offer"           // ActivityStreamsOffer https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer +	ActivityQuestion        = "Question"        // ActivityStreamsQuestion https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question +	ActivityReject          = "Reject"          // ActivityStreamsReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject +	ActivityRead            = "Read"            // ActivityStreamsRead https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read +	ActivityRemove          = "Remove"          // ActivityStreamsRemove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove +	ActivityTentativeReject = "TentativeReject" // ActivityStreamsTentativeReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject +	ActivityTentativeAccept = "TentativeAccept" // ActivityStreamsTentativeAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept +	ActivityTravel          = "Travel"          // ActivityStreamsTravel https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel +	ActivityUndo            = "Undo"            // ActivityStreamsUndo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo +	ActivityUpdate          = "Update"          // ActivityStreamsUpdate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update +	ActivityView            = "View"            // ActivityStreamsView https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view + +	ActorApplication  = "Application"  // ActivityStreamsApplication https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application +	ActorGroup        = "Group"        // ActivityStreamsGroup https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group +	ActorOrganization = "Organization" // ActivityStreamsOrganization https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization +	ActorPerson       = "Person"       // ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person +	ActorService      = "Service"      // ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service + +	ObjectArticle        = "Article"        // ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article +	ObjectAudio          = "Audio"          // ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio +	ObjectDocument       = "Document"       // ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document +	ObjectEvent          = "Event"          // ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event +	ObjectImage          = "Image"          // ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image +	ObjectNote           = "Note"           // ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note +	ObjectPage           = "Page"           // ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page +	ObjectPlace          = "Place"          // ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place +	ObjectProfile        = "Profile"        // ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile +	ObjectRelationship   = "Relationship"   // ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship +	ObjectTombstone      = "Tombstone"      // ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone +	ObjectVideo          = "Video"          // ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video +	ObjectCollection     = "Collection"     //ActivityStreamsCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection +	ObjectCollectionPage = "CollectionPage" // ActivityStreamsCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage +) diff --git a/internal/api/client/account/account_test.go b/internal/api/client/account/account_test.go index 5e1959cc2..02817984b 100644 --- a/internal/api/client/account/account_test.go +++ b/internal/api/client/account/account_test.go @@ -9,7 +9,6 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/federation"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth"  	"github.com/superseriousbusiness/gotosocial/internal/processing"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  ) @@ -27,8 +26,8 @@ type AccountStandardTestSuite struct {  	processor processing.Processor  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/api/client/account/accountcreate.go b/internal/api/client/account/accountcreate.go index a9d672f80..3fab1488f 100644 --- a/internal/api/client/account/accountcreate.go +++ b/internal/api/client/account/accountcreate.go @@ -27,7 +27,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/oauth" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  // AccountCreatePOSTHandler swagger:operation POST /api/v1/accounts accountCreate @@ -118,15 +118,15 @@ func validateCreateAccount(form *model.AccountCreateRequest, c *config.AccountsC  		return errors.New("registration is not open for this server")  	} -	if err := util.ValidateUsername(form.Username); err != nil { +	if err := validate.Username(form.Username); err != nil {  		return err  	} -	if err := util.ValidateEmail(form.Email); err != nil { +	if err := validate.Email(form.Email); err != nil {  		return err  	} -	if err := util.ValidateNewPassword(form.Password); err != nil { +	if err := validate.NewPassword(form.Password); err != nil {  		return err  	} @@ -134,11 +134,11 @@ func validateCreateAccount(form *model.AccountCreateRequest, c *config.AccountsC  		return errors.New("agreement to terms and conditions not given")  	} -	if err := util.ValidateLanguage(form.Locale); err != nil { +	if err := validate.Language(form.Locale); err != nil {  		return err  	} -	if err := util.ValidateSignUpReason(form.Reason, c.ReasonRequired); err != nil { +	if err := validate.SignUpReason(form.Reason, c.ReasonRequired); err != nil {  		return err  	} diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go index 859933b16..019298976 100644 --- a/internal/api/client/admin/emojicreate.go +++ b/internal/api/client/admin/emojicreate.go @@ -28,7 +28,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/media"  	"github.com/superseriousbusiness/gotosocial/internal/oauth" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  // emojiCreateRequest swagger:operation POST /api/v1/admin/custom_emojis emojiCreate @@ -132,5 +132,5 @@ func validateCreateEmoji(form *model.EmojiCreateRequest) error {  		return fmt.Errorf("file size limit exceeded: limit is %d bytes but emoji was %d bytes", media.EmojiMaxBytes, form.Image.Size)  	} -	return util.ValidateEmojiShortcode(form.Shortcode) +	return validate.EmojiShortcode(form.Shortcode)  } diff --git a/internal/api/client/auth/auth_test.go b/internal/api/client/auth/auth_test.go index 3d5170f31..295c0e964 100644 --- a/internal/api/client/auth/auth_test.go +++ b/internal/api/client/auth/auth_test.go @@ -41,7 +41,7 @@ type AuthTestSuite struct {  	testAccount     *gtsmodel.Account  	testApplication *gtsmodel.Application  	testUser        *gtsmodel.User -	testClient      *oauth.Client +	testClient      *gtsmodel.Client  	config          *config.Config  } @@ -83,7 +83,7 @@ func (suite *AuthTestSuite) SetupSuite() {  		Email:             "user@example.org",  		AccountID:         acctID,  	} -	suite.testClient = &oauth.Client{ +	suite.testClient = >smodel.Client{  		ID:     "a-known-client-id",  		Secret: "some-secret",  		Domain: fmt.Sprintf("%s://%s", c.Protocol, c.Host), @@ -95,7 +95,6 @@ func (suite *AuthTestSuite) SetupSuite() {  		ClientID:     "a-known-client-id",  		ClientSecret: "some-secret",  		Scopes:       "read", -		VapidKey:     uuid.NewString(),  	}  } @@ -112,8 +111,8 @@ func (suite *AuthTestSuite) SetupTest() {  	suite.db = db  	models := []interface{}{ -		&oauth.Client{}, -		&oauth.Token{}, +		>smodel.Client{}, +		>smodel.Token{},  		>smodel.User{},  		>smodel.Account{},  		>smodel.Application{}, @@ -145,8 +144,8 @@ func (suite *AuthTestSuite) SetupTest() {  // TearDownTest drops the oauth_clients table and closes the pg connection after each test  func (suite *AuthTestSuite) TearDownTest() {  	models := []interface{}{ -		&oauth.Client{}, -		&oauth.Token{}, +		>smodel.Client{}, +		>smodel.Token{},  		>smodel.User{},  		>smodel.Account{},  		>smodel.Application{}, diff --git a/internal/api/client/auth/callback.go b/internal/api/client/auth/callback.go index c2fbfb486..322ba5fc9 100644 --- a/internal/api/client/auth/callback.go +++ b/internal/api/client/auth/callback.go @@ -33,7 +33,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/oidc" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  // CallbackGETHandler parses a token from an external auth provider. @@ -153,7 +153,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i  	}  	// check if we can just use claims.Name as-is -	err = util.ValidateUsername(claims.Name) +	err = validate.Username(claims.Name)  	if err == nil {  		// the name we have on the claims is already a valid username  		username = claims.Name @@ -166,7 +166,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i  		// lowercase the whole thing  		lower := strings.ToLower(underscored)  		// see if this is valid.... -		if err := util.ValidateUsername(lower); err == nil { +		if err := validate.Username(lower); err == nil {  			// we managed to get a valid username  			username = lower  		} else { diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go index 4eec3bae5..579bb9606 100644 --- a/internal/api/client/fileserver/servefile_test.go +++ b/internal/api/client/fileserver/servefile_test.go @@ -57,8 +57,8 @@ type ServeFileTestSuite struct {  	oauthServer  oauth.Server  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index 8433786e4..1b2c84cf9 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -60,8 +60,8 @@ type MediaCreateTestSuite struct {  	processor    processing.Processor  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/api/client/status/status_test.go b/internal/api/client/status/status_test.go index 563a23ce2..f9b4e3671 100644 --- a/internal/api/client/status/status_test.go +++ b/internal/api/client/status/status_test.go @@ -27,7 +27,6 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/federation"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth"  	"github.com/superseriousbusiness/gotosocial/internal/processing"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  ) @@ -45,8 +44,8 @@ type StatusStandardTestSuite struct {  	storage   blob.Storage  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/status/statuscreate.go index 09fc47b5b..c8ea019d2 100644 --- a/internal/api/client/status/statuscreate.go +++ b/internal/api/client/status/statuscreate.go @@ -27,7 +27,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/oauth" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  // StatusCreatePOSTHandler swagger:operation POST /api/v1/statuses statusCreate @@ -157,7 +157,7 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm, config *config.S  	// validate post language  	if form.Language != "" { -		if err := util.ValidateLanguage(form.Language); err != nil { +		if err := validate.Language(form.Language); err != nil {  			return err  		}  	} diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go index fa210e8d8..4a9dcfe52 100644 --- a/internal/api/client/streaming/stream.go +++ b/internal/api/client/streaming/stream.go @@ -146,13 +146,13 @@ func (m *Module) StreamGETHandler(c *gin.Context) {  	}  	defer conn.Close() // whatever happens, when we leave this function we want to close the websocket connection -	// inform the processor that we have a new connection and want a stream for it -	stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType) +	// inform the processor that we have a new connection and want a s for it +	s, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)  	if errWithCode != nil {  		c.JSON(errWithCode.Code(), errWithCode.Safe())  		return  	} -	defer close(stream.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler +	defer close(s.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler  	// spawn a new ticker for pinging the connection periodically  	t := time.NewTicker(30 * time.Second) @@ -161,7 +161,7 @@ func (m *Module) StreamGETHandler(c *gin.Context) {  sendLoop:  	for {  		select { -		case m := <-stream.Messages: +		case m := <-s.Messages:  			// we've got a streaming message!!  			l.Trace("received message from stream")  			if err := conn.WriteJSON(m); err != nil { diff --git a/internal/api/s2s/user/user_test.go b/internal/api/s2s/user/user_test.go index 71d4395eb..ecd5fadc8 100644 --- a/internal/api/s2s/user/user_test.go +++ b/internal/api/s2s/user/user_test.go @@ -10,7 +10,6 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/federation"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth"  	"github.com/superseriousbusiness/gotosocial/internal/processing"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  ) @@ -29,8 +28,8 @@ type UserStandardTestSuite struct {  	securityModule *security.Module  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/cliactions/admin/account/account.go b/internal/cliactions/admin/account/account.go index 46998ec6a..369f2b800 100644 --- a/internal/cliactions/admin/account/account.go +++ b/internal/cliactions/admin/account/account.go @@ -30,7 +30,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/db/bundb"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  	"golang.org/x/crypto/bcrypt"  ) @@ -45,7 +45,7 @@ var Create cliactions.GTSAction = func(ctx context.Context, c *config.Config, lo  	if !ok {  		return errors.New("no username set")  	} -	if err := util.ValidateUsername(username); err != nil { +	if err := validate.Username(username); err != nil {  		return err  	} @@ -53,7 +53,7 @@ var Create cliactions.GTSAction = func(ctx context.Context, c *config.Config, lo  	if !ok {  		return errors.New("no email set")  	} -	if err := util.ValidateEmail(email); err != nil { +	if err := validate.Email(email); err != nil {  		return err  	} @@ -61,7 +61,7 @@ var Create cliactions.GTSAction = func(ctx context.Context, c *config.Config, lo  	if !ok {  		return errors.New("no password set")  	} -	if err := util.ValidateNewPassword(password); err != nil { +	if err := validate.NewPassword(password); err != nil {  		return err  	} @@ -84,7 +84,7 @@ var Confirm cliactions.GTSAction = func(ctx context.Context, c *config.Config, l  	if !ok {  		return errors.New("no username set")  	} -	if err := util.ValidateUsername(username); err != nil { +	if err := validate.Username(username); err != nil {  		return err  	} @@ -119,7 +119,7 @@ var Promote cliactions.GTSAction = func(ctx context.Context, c *config.Config, l  	if !ok {  		return errors.New("no username set")  	} -	if err := util.ValidateUsername(username); err != nil { +	if err := validate.Username(username); err != nil {  		return err  	} @@ -151,7 +151,7 @@ var Demote cliactions.GTSAction = func(ctx context.Context, c *config.Config, lo  	if !ok {  		return errors.New("no username set")  	} -	if err := util.ValidateUsername(username); err != nil { +	if err := validate.Username(username); err != nil {  		return err  	} @@ -183,7 +183,7 @@ var Disable cliactions.GTSAction = func(ctx context.Context, c *config.Config, l  	if !ok {  		return errors.New("no username set")  	} -	if err := util.ValidateUsername(username); err != nil { +	if err := validate.Username(username); err != nil {  		return err  	} @@ -221,7 +221,7 @@ var Password cliactions.GTSAction = func(ctx context.Context, c *config.Config,  	if !ok {  		return errors.New("no username set")  	} -	if err := util.ValidateUsername(username); err != nil { +	if err := validate.Username(username); err != nil {  		return err  	} @@ -229,7 +229,7 @@ var Password cliactions.GTSAction = func(ctx context.Context, c *config.Config,  	if !ok {  		return errors.New("no password set")  	} -	if err := util.ValidateNewPassword(password); err != nil { +	if err := validate.NewPassword(password); err != nil {  		return err  	} diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index 504a4c3c7..0769ade82 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -73,8 +73,8 @@ var models []interface{} = []interface{}{  	>smodel.Instance{},  	>smodel.Notification{},  	>smodel.RouterSession{}, -	&oauth.Token{}, -	&oauth.Client{}, +	>smodel.Token{}, +	>smodel.Client{},  }  // Start creates and starts a gotosocial server diff --git a/internal/db/bundb/admin.go b/internal/db/bundb/admin.go index 6a51ffeb1..dd973ef2d 100644 --- a/internal/db/bundb/admin.go +++ b/internal/db/bundb/admin.go @@ -29,6 +29,7 @@ import (  	"strings"  	"time" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -113,7 +114,7 @@ func (a *adminDB) NewSignup(ctx context.Context, username string, reason string,  			PrivateKey:            key,  			PublicKey:             &key.PublicKey,  			PublicKeyURI:          newAccountURIs.PublicKeyURI, -			ActorType:             gtsmodel.ActivityStreamsPerson, +			ActorType:             ap.ActorPerson,  			URI:                   newAccountURIs.UserURI,  			InboxURI:              newAccountURIs.InboxURI,  			OutboxURI:             newAccountURIs.OutboxURI, @@ -207,7 +208,7 @@ func (a *adminDB) CreateInstanceAccount(ctx context.Context) db.Error {  		PrivateKey:            key,  		PublicKey:             &key.PublicKey,  		PublicKeyURI:          newAccountURIs.PublicKeyURI, -		ActorType:             gtsmodel.ActivityStreamsPerson, +		ActorType:             ap.ActorPerson,  		URI:                   newAccountURIs.UserURI,  		InboxURI:              newAccountURIs.InboxURI,  		OutboxURI:             newAccountURIs.OutboxURI, diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index 6fcc56e51..7ddcab5c7 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -37,11 +37,13 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/cache"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id"  	"github.com/uptrace/bun"  	"github.com/uptrace/bun/dialect/pgdialect"  	"github.com/uptrace/bun/dialect/sqlitedialect" +	"github.com/uptrace/bun/migrate"  	_ "modernc.org/sqlite"  ) @@ -73,6 +75,32 @@ type bunDBService struct {  	conn   *DBConn  } +func doMigration(ctx context.Context, db *bun.DB, log *logrus.Logger) error { +	l := log.WithField("func", "doMigration") + +	migrator := migrate.NewMigrator(db, migrations.Migrations) + +	if err := migrator.Init(ctx); err != nil { +		return err +	} + +	group, err := migrator.Migrate(ctx) +	if err != nil { +		if err.Error() == "migrate: there are no any migrations" { +			return nil +		} +		return err +	} + +	if group.ID == 0 { +		l.Info("there are no new migrations to run") +		return nil +	} + +	l.Infof("MIGRATED DATABASE TO %s", group) +	return nil +} +  // NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface.  // Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection.  func NewBunDBService(ctx context.Context, c *config.Config, log *logrus.Logger) (db.DB, error) { @@ -130,6 +158,10 @@ func NewBunDBService(ctx context.Context, c *config.Config, log *logrus.Logger)  		conn.RegisterModel(t)  	} +	if err := doMigration(ctx, conn.DB, log); err != nil { +		return nil, fmt.Errorf("db migration error: %s", err) +	} +  	accounts := &accountDB{config: c, conn: conn, cache: cache.NewAccountCache()}  	ps := &bunDBService{ diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go index b789375af..faa67456d 100644 --- a/internal/db/bundb/bundb_test.go +++ b/internal/db/bundb/bundb_test.go @@ -24,7 +24,6 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth"  )  type BunDBStandardTestSuite struct { @@ -35,8 +34,8 @@ type BunDBStandardTestSuite struct {  	log    *logrus.Logger  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/db/bundb/migrations/README.md b/internal/db/bundb/migrations/README.md new file mode 100644 index 000000000..fee262936 --- /dev/null +++ b/internal/db/bundb/migrations/README.md @@ -0,0 +1,70 @@ +# Migrations + +## How do I write a migration file? + +[See here](https://bun.uptrace.dev/guide/migrations.html#migration-names) + +As a template, take one of the existing migration files and modify it, or use the below code snippet: + +```go +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package migrations + +import ( +	"context" + +	"github.com/uptrace/bun" +) + +func init() { +	up := func(ctx context.Context, db *bun.DB) error { +		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { +			// your logic here +            return nil +		}) +	} + +	down := func(ctx context.Context, db *bun.DB) error { +		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { +			// your logic here +            return nil +		}) +	} + +	if err := Migrations.Register(up, down); err != nil { +		panic(err) +	} +} +``` + +## File format + +Bun requires a very specific format: 14 digits, then letters or underscores. + +You can use the following bash command on your branch to generate a suitable migration filename. + +```bash +echo "$(date --utc +%Y%m%H%M%S%N | head -c 14)_$(git rev-parse --abbrev-ref HEAD).go" +``` + +## Rules of thumb + +1. **DON'T DROP TABLES**!!!!!!!! +2. Don't make something `NOT NULL` if it's likely to already contain `null` fields. diff --git a/internal/gtsmodel/poll.go b/internal/db/bundb/migrations/main.go index c39497cdd..7f4e76027 100644 --- a/internal/gtsmodel/poll.go +++ b/internal/db/bundb/migrations/main.go @@ -16,4 +16,13 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package gtsmodel +package migrations + +import ( +	"github.com/uptrace/bun/migrate" +) + +var ( +	// Migrations provides migration logic for bun +	Migrations = migrate.NewMigrations() +) diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index 8cae002e8..b16b53fee 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -165,19 +165,19 @@ func (d *deref) dereferenceAccountable(ctx context.Context, username string, rem  	}  	switch t.GetTypeName() { -	case string(gtsmodel.ActivityStreamsPerson): +	case ap.ActorPerson:  		p, ok := t.(vocab.ActivityStreamsPerson)  		if !ok {  			return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams person")  		}  		return p, nil -	case string(gtsmodel.ActivityStreamsApplication): +	case ap.ActorApplication:  		p, ok := t.(vocab.ActivityStreamsApplication)  		if !ok {  			return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams application")  		}  		return p, nil -	case string(gtsmodel.ActivityStreamsService): +	case ap.ActorService:  		p, ok := t.(vocab.ActivityStreamsService)  		if !ok {  			return nil, errors.New("DereferenceAccountable: error resolving type as activitystreams service") diff --git a/internal/federation/dereferencing/collectionpage.go b/internal/federation/dereferencing/collectionpage.go index 6f0beeaf6..c5a54402c 100644 --- a/internal/federation/dereferencing/collectionpage.go +++ b/internal/federation/dereferencing/collectionpage.go @@ -28,7 +28,6 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab"  	"github.com/superseriousbusiness/gotosocial/internal/ap" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  )  // DereferenceCollectionPage returns the activitystreams CollectionPage at the specified IRI, or an error if something goes wrong. @@ -57,7 +56,7 @@ func (d *deref) DereferenceCollectionPage(ctx context.Context, username string,  		return nil, fmt.Errorf("DereferenceCollectionPage: error resolving json into ap vocab type: %s", err)  	} -	if t.GetTypeName() != gtsmodel.ActivityStreamsCollectionPage { +	if t.GetTypeName() != ap.ObjectCollectionPage {  		return nil, fmt.Errorf("DereferenceCollectionPage: type name %s not supported", t.GetTypeName())  	} diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index 7a7f928f1..b8f5bba3b 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -154,55 +154,55 @@ func (d *deref) dereferenceStatusable(ctx context.Context, username string, remo  	// Article, Document, Image, Video, Note, Page, Event, Place, Mention, Profile  	switch t.GetTypeName() { -	case gtsmodel.ActivityStreamsArticle: +	case ap.ObjectArticle:  		p, ok := t.(vocab.ActivityStreamsArticle)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsArticle")  		}  		return p, nil -	case gtsmodel.ActivityStreamsDocument: +	case ap.ObjectDocument:  		p, ok := t.(vocab.ActivityStreamsDocument)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsDocument")  		}  		return p, nil -	case gtsmodel.ActivityStreamsImage: +	case ap.ObjectImage:  		p, ok := t.(vocab.ActivityStreamsImage)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsImage")  		}  		return p, nil -	case gtsmodel.ActivityStreamsVideo: +	case ap.ObjectVideo:  		p, ok := t.(vocab.ActivityStreamsVideo)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsVideo")  		}  		return p, nil -	case gtsmodel.ActivityStreamsNote: +	case ap.ObjectNote:  		p, ok := t.(vocab.ActivityStreamsNote)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsNote")  		}  		return p, nil -	case gtsmodel.ActivityStreamsPage: +	case ap.ObjectPage:  		p, ok := t.(vocab.ActivityStreamsPage)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsPage")  		}  		return p, nil -	case gtsmodel.ActivityStreamsEvent: +	case ap.ObjectEvent:  		p, ok := t.(vocab.ActivityStreamsEvent)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsEvent")  		}  		return p, nil -	case gtsmodel.ActivityStreamsPlace: +	case ap.ObjectPlace:  		p, ok := t.(vocab.ActivityStreamsPlace)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsPlace")  		}  		return p, nil -	case gtsmodel.ActivityStreamsProfile: +	case ap.ObjectProfile:  		p, ok := t.(vocab.ActivityStreamsProfile)  		if !ok {  			return nil, errors.New("DereferenceStatusable: error resolving type as ActivityStreamsProfile") diff --git a/internal/federation/dereferencing/status_test.go b/internal/federation/dereferencing/status_test.go index 43732ac77..1ab4ade53 100644 --- a/internal/federation/dereferencing/status_test.go +++ b/internal/federation/dereferencing/status_test.go @@ -29,6 +29,7 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -133,7 +134,7 @@ func (suite *StatusTestSuite) TestDereferenceSimpleStatus() {  	suite.False(status.Local)  	suite.Empty(status.ContentWarning)  	suite.Equal(gtsmodel.VisibilityPublic, status.Visibility) -	suite.Equal(gtsmodel.ActivityStreamsNote, status.ActivityStreamsType) +	suite.Equal(ap.ObjectNote, status.ActivityStreamsType)  	// status should be in the database  	dbStatus, err := suite.db.GetStatusByURI(context.Background(), status.URI) @@ -171,7 +172,7 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithMention() {  	suite.False(status.Local)  	suite.Empty(status.ContentWarning)  	suite.Equal(gtsmodel.VisibilityPublic, status.Visibility) -	suite.Equal(gtsmodel.ActivityStreamsNote, status.ActivityStreamsType) +	suite.Equal(ap.ObjectNote, status.ActivityStreamsType)  	// status should be in the database  	dbStatus, err := suite.db.GetStatusByURI(context.Background(), status.URI) diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index 0b14e8a6a..477c5e8b9 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -27,8 +27,10 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -67,7 +69,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA  		l.Error("ACCEPT: from federator channel wasn't set on context")  		return nil  	} -	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) +	fromFederatorChan, ok := fromFederatorChanI.(chan messages.FromFederator)  	if !ok {  		l.Error("ACCEPT: from federator channel was set on context but couldn't be parsed")  		return nil @@ -99,9 +101,9 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA  					return err  				} -				fromFederatorChan <- gtsmodel.FromFederator{ -					APObjectType:     gtsmodel.ActivityStreamsFollow, -					APActivityType:   gtsmodel.ActivityStreamsAccept, +				fromFederatorChan <- messages.FromFederator{ +					APObjectType:     ap.ActivityFollow, +					APActivityType:   ap.ActivityAccept,  					GTSModel:         follow,  					ReceivingAccount: targetAcct,  				} @@ -116,7 +118,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA  		}  		switch iter.GetType().GetTypeName() {  		// we have the whole object so we can figure out what we're accepting -		case string(gtsmodel.ActivityStreamsFollow): +		case ap.ActivityFollow:  			// ACCEPT FOLLOW  			asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)  			if !ok { @@ -136,9 +138,9 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA  				return err  			} -			fromFederatorChan <- gtsmodel.FromFederator{ -				APObjectType:     gtsmodel.ActivityStreamsFollow, -				APActivityType:   gtsmodel.ActivityStreamsAccept, +			fromFederatorChan <- messages.FromFederator{ +				APObjectType:     ap.ActivityFollow, +				APActivityType:   ap.ActivityAccept,  				GTSModel:         follow,  				ReceivingAccount: targetAcct,  			} diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go index 5cd34285e..7d7b12cbc 100644 --- a/internal/federation/federatingdb/announce.go +++ b/internal/federation/federatingdb/announce.go @@ -26,7 +26,9 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -65,7 +67,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre  		l.Error("ANNOUNCE: from federator channel wasn't set on context")  		return nil  	} -	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) +	fromFederatorChan, ok := fromFederatorChanI.(chan messages.FromFederator)  	if !ok {  		l.Error("ANNOUNCE: from federator channel was set on context but couldn't be parsed")  		return nil @@ -82,9 +84,9 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre  	}  	// it's a new announce so pass it back to the processor async for dereferencing etc -	fromFederatorChan <- gtsmodel.FromFederator{ -		APObjectType:     gtsmodel.ActivityStreamsAnnounce, -		APActivityType:   gtsmodel.ActivityStreamsCreate, +	fromFederatorChan <- messages.FromFederator{ +		APObjectType:     ap.ActivityAnnounce, +		APActivityType:   ap.ActivityCreate,  		GTSModel:         boost,  		ReceivingAccount: targetAcct,  	} diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 8ea549c5a..88b0d1e8b 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -27,9 +27,11 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -81,14 +83,14 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  		l.Error("CREATE: from federator channel wasn't set on context")  		return nil  	} -	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) +	fromFederatorChan, ok := fromFederatorChanI.(chan messages.FromFederator)  	if !ok {  		l.Error("CREATE: from federator channel was set on context but couldn't be parsed")  		return nil  	}  	switch asType.GetTypeName() { -	case gtsmodel.ActivityStreamsCreate: +	case ap.ActivityCreate:  		// CREATE SOMETHING  		create, ok := asType.(vocab.ActivityStreamsCreate)  		if !ok { @@ -97,7 +99,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  		object := create.GetActivityStreamsObject()  		for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {  			switch objectIter.GetType().GetTypeName() { -			case gtsmodel.ActivityStreamsNote: +			case ap.ObjectNote:  				// CREATE A NOTE  				note := objectIter.GetActivityStreamsNote()  				status, err := f.typeConverter.ASStatusToStatus(ctx, note) @@ -122,15 +124,15 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  					return fmt.Errorf("CREATE: database error inserting status: %s", err)  				} -				fromFederatorChan <- gtsmodel.FromFederator{ -					APObjectType:     gtsmodel.ActivityStreamsNote, -					APActivityType:   gtsmodel.ActivityStreamsCreate, +				fromFederatorChan <- messages.FromFederator{ +					APObjectType:     ap.ObjectNote, +					APActivityType:   ap.ActivityCreate,  					GTSModel:         status,  					ReceivingAccount: targetAcct,  				}  			}  		} -	case gtsmodel.ActivityStreamsFollow: +	case ap.ActivityFollow:  		// FOLLOW SOMETHING  		follow, ok := asType.(vocab.ActivityStreamsFollow)  		if !ok { @@ -152,13 +154,13 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  			return fmt.Errorf("CREATE: database error inserting follow request: %s", err)  		} -		fromFederatorChan <- gtsmodel.FromFederator{ -			APObjectType:     gtsmodel.ActivityStreamsFollow, -			APActivityType:   gtsmodel.ActivityStreamsCreate, +		fromFederatorChan <- messages.FromFederator{ +			APObjectType:     ap.ActivityFollow, +			APActivityType:   ap.ActivityCreate,  			GTSModel:         followRequest,  			ReceivingAccount: targetAcct,  		} -	case gtsmodel.ActivityStreamsLike: +	case ap.ActivityLike:  		// LIKE SOMETHING  		like, ok := asType.(vocab.ActivityStreamsLike)  		if !ok { @@ -180,13 +182,13 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  			return fmt.Errorf("CREATE: database error inserting fave: %s", err)  		} -		fromFederatorChan <- gtsmodel.FromFederator{ -			APObjectType:     gtsmodel.ActivityStreamsLike, -			APActivityType:   gtsmodel.ActivityStreamsCreate, +		fromFederatorChan <- messages.FromFederator{ +			APObjectType:     ap.ActivityLike, +			APActivityType:   ap.ActivityCreate,  			GTSModel:         fave,  			ReceivingAccount: targetAcct,  		} -	case gtsmodel.ActivityStreamsBlock: +	case ap.ActivityBlock:  		// BLOCK SOMETHING  		blockable, ok := asType.(vocab.ActivityStreamsBlock)  		if !ok { @@ -208,9 +210,9 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  			return fmt.Errorf("CREATE: database error inserting block: %s", err)  		} -		fromFederatorChan <- gtsmodel.FromFederator{ -			APObjectType:     gtsmodel.ActivityStreamsBlock, -			APActivityType:   gtsmodel.ActivityStreamsCreate, +		fromFederatorChan <- messages.FromFederator{ +			APObjectType:     ap.ActivityBlock, +			APActivityType:   ap.ActivityCreate,  			GTSModel:         block,  			ReceivingAccount: targetAcct,  		} diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go index 11b818168..abc3715da 100644 --- a/internal/federation/federatingdb/delete.go +++ b/internal/federation/federatingdb/delete.go @@ -24,7 +24,9 @@ import (  	"net/url"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -61,7 +63,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {  		l.Error("DELETE: from federator channel wasn't set on context")  		return nil  	} -	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) +	fromFederatorChan, ok := fromFederatorChanI.(chan messages.FromFederator)  	if !ok {  		l.Error("DELETE: from federator channel was set on context but couldn't be parsed")  		return nil @@ -76,9 +78,9 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {  		if err := f.db.DeleteByID(ctx, s.ID, >smodel.Status{}); err != nil {  			return fmt.Errorf("DELETE: err deleting status: %s", err)  		} -		fromFederatorChan <- gtsmodel.FromFederator{ -			APObjectType:     gtsmodel.ActivityStreamsNote, -			APActivityType:   gtsmodel.ActivityStreamsDelete, +		fromFederatorChan <- messages.FromFederator{ +			APObjectType:     ap.ObjectNote, +			APActivityType:   ap.ActivityDelete,  			GTSModel:         s,  			ReceivingAccount: targetAcct,  		} @@ -91,9 +93,9 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {  		if err := f.db.DeleteByID(ctx, a.ID, >smodel.Account{}); err != nil {  			return fmt.Errorf("DELETE: err deleting account: %s", err)  		} -		fromFederatorChan <- gtsmodel.FromFederator{ -			APObjectType:     gtsmodel.ActivityStreamsProfile, -			APActivityType:   gtsmodel.ActivityStreamsDelete, +		fromFederatorChan <- messages.FromFederator{ +			APObjectType:     ap.ObjectProfile, +			APActivityType:   ap.ActivityDelete,  			GTSModel:         a,  			ReceivingAccount: targetAcct,  		} diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go index 0fa38114d..481c2d787 100644 --- a/internal/federation/federatingdb/undo.go +++ b/internal/federation/federatingdb/undo.go @@ -27,6 +27,7 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/util" @@ -72,7 +73,7 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)  			continue  		}  		switch iter.GetType().GetTypeName() { -		case string(gtsmodel.ActivityStreamsFollow): +		case ap.ActivityFollow:  			// UNDO FOLLOW  			ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)  			if !ok { @@ -101,11 +102,11 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)  			}  			l.Debug("follow undone")  			return nil -		case string(gtsmodel.ActivityStreamsLike): +		case ap.ActivityLike:  			// UNDO LIKE -		case string(gtsmodel.ActivityStreamsAnnounce): +		case ap.ActivityAnnounce:  			// UNDO BOOST/REBLOG/ANNOUNCE -		case string(gtsmodel.ActivityStreamsBlock): +		case ap.ActivityBlock:  			// UNDO BLOCK  			ASBlock, ok := iter.GetType().(vocab.ActivityStreamsBlock)  			if !ok { diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go index e9dfe5315..2bcf2533c 100644 --- a/internal/federation/federatingdb/update.go +++ b/internal/federation/federatingdb/update.go @@ -29,6 +29,7 @@ import (  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -84,50 +85,50 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {  	if fromFederatorChanI == nil {  		l.Error("UPDATE: from federator channel wasn't set on context")  	} -	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) +	fromFederatorChan, ok := fromFederatorChanI.(chan messages.FromFederator)  	if !ok {  		l.Error("UPDATE: from federator channel was set on context but couldn't be parsed")  	}  	typeName := asType.GetTypeName() -	if typeName == gtsmodel.ActivityStreamsApplication || -		typeName == gtsmodel.ActivityStreamsGroup || -		typeName == gtsmodel.ActivityStreamsOrganization || -		typeName == gtsmodel.ActivityStreamsPerson || -		typeName == gtsmodel.ActivityStreamsService { +	if typeName == ap.ActorApplication || +		typeName == ap.ActorGroup || +		typeName == ap.ActorOrganization || +		typeName == ap.ActorPerson || +		typeName == ap.ActorService {  		// it's an UPDATE to some kind of account  		var accountable ap.Accountable  		switch asType.GetTypeName() { -		case gtsmodel.ActivityStreamsApplication: +		case ap.ActorApplication:  			l.Debug("got update for APPLICATION")  			i, ok := asType.(vocab.ActivityStreamsApplication)  			if !ok {  				return errors.New("UPDATE: could not convert type to application")  			}  			accountable = i -		case gtsmodel.ActivityStreamsGroup: +		case ap.ActorGroup:  			l.Debug("got update for GROUP")  			i, ok := asType.(vocab.ActivityStreamsGroup)  			if !ok {  				return errors.New("UPDATE: could not convert type to group")  			}  			accountable = i -		case gtsmodel.ActivityStreamsOrganization: +		case ap.ActorOrganization:  			l.Debug("got update for ORGANIZATION")  			i, ok := asType.(vocab.ActivityStreamsOrganization)  			if !ok {  				return errors.New("UPDATE: could not convert type to organization")  			}  			accountable = i -		case gtsmodel.ActivityStreamsPerson: +		case ap.ActorPerson:  			l.Debug("got update for PERSON")  			i, ok := asType.(vocab.ActivityStreamsPerson)  			if !ok {  				return errors.New("UPDATE: could not convert type to person")  			}  			accountable = i -		case gtsmodel.ActivityStreamsService: +		case ap.ActorService:  			l.Debug("got update for SERVICE")  			i, ok := asType.(vocab.ActivityStreamsService)  			if !ok { @@ -157,9 +158,9 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {  			return fmt.Errorf("UPDATE: database error inserting updated account: %s", err)  		} -		fromFederatorChan <- gtsmodel.FromFederator{ -			APObjectType:     gtsmodel.ActivityStreamsProfile, -			APActivityType:   gtsmodel.ActivityStreamsUpdate, +		fromFederatorChan <- messages.FromFederator{ +			APObjectType:     ap.ObjectProfile, +			APActivityType:   ap.ActivityUpdate,  			GTSModel:         updatedAcct,  			ReceivingAccount: targetAcct,  		} diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index b5befc613..d8c7d8e8a 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -28,6 +28,7 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" @@ -78,7 +79,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,  	l.Debugf("received NEWID request for asType %s", string(b))  	switch t.GetTypeName() { -	case gtsmodel.ActivityStreamsFollow: +	case ap.ActivityFollow:  		// FOLLOW  		// ID might already be set on a follow we've created, so check it here and return it if it is  		follow, ok := t.(vocab.ActivityStreamsFollow) @@ -108,7 +109,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,  				}  			}  		} -	case gtsmodel.ActivityStreamsNote: +	case ap.ObjectNote:  		// NOTE aka STATUS  		// ID might already be set on a note we've created, so check it here and return it if it is  		note, ok := t.(vocab.ActivityStreamsNote) @@ -121,7 +122,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,  				return idProp.GetIRI(), nil  			}  		} -	case gtsmodel.ActivityStreamsLike: +	case ap.ActivityLike:  		// LIKE aka FAVE  		// ID might already be set on a fave we've created, so check it here and return it if it is  		fave, ok := t.(vocab.ActivityStreamsLike) @@ -134,7 +135,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,  				return idProp.GetIRI(), nil  			}  		} -	case gtsmodel.ActivityStreamsAnnounce: +	case ap.ActivityAnnounce:  		// ANNOUNCE aka BOOST  		// ID might already be set on an announce we've created, so check it here and return it if it is  		announce, ok := t.(vocab.ActivityStreamsAnnounce) @@ -147,7 +148,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,  				return idProp.GetIRI(), nil  			}  		} -	case gtsmodel.ActivityStreamsUpdate: +	case ap.ActivityUpdate:  		// UPDATE  		// ID might already be set on an update we've created, so check it here and return it if it is  		update, ok := t.(vocab.ActivityStreamsUpdate) @@ -160,7 +161,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,  				return idProp.GetIRI(), nil  			}  		} -	case gtsmodel.ActivityStreamsBlock: +	case ap.ActivityBlock:  		// BLOCK  		// ID might already be set on a block we've created, so check it here and return it if it is  		block, ok := t.(vocab.ActivityStreamsBlock) @@ -173,7 +174,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,  				return idProp.GetIRI(), nil  			}  		} -	case gtsmodel.ActivityStreamsUndo: +	case ap.ActivityUndo:  		// UNDO  		// ID might already be set on an undo we've created, so check it here and return it if it is  		undo, ok := t.(vocab.ActivityStreamsUndo) diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index a59746be5..67ce05d0b 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -27,124 +27,73 @@ import (  	"time"  ) -// Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc) +// Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc).  type Account struct { -	/* -		BASIC INFO -	*/ - -	// id of this account in the local database -	ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` -	// 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 `bun:",notnull,unique:userdomain,nullzero"` // 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 `bun:",unique:userdomain,nullzero"` // username and domain should be unique *with* each other - -	/* -		ACCOUNT METADATA -	*/ - -	// ID of the avatar as a media attachment -	AvatarMediaAttachmentID string           `bun:"type:CHAR(26),nullzero"` -	AvatarMediaAttachment   *MediaAttachment `bun:"rel:belongs-to"` -	// For a non-local account, where can the header be fetched? -	AvatarRemoteURL string `bun:",nullzero"` -	// ID of the header as a media attachment -	HeaderMediaAttachmentID string           `bun:"type:CHAR(26),nullzero"` -	HeaderMediaAttachment   *MediaAttachment `bun:"rel:belongs-to"` -	// For a non-local account, where can the header be fetched? -	HeaderRemoteURL string `bun:",nullzero"` -	// DisplayName for this account. Can be empty, then just the Username will be used for display purposes. -	DisplayName string `bun:",nullzero"` -	// a key/value map of fields that this account has added to their profile -	Fields []Field -	// A note that this account has on their profile (ie., the account's bio/description of themselves) -	Note string `bun:",nullzero"` -	// Is this a memorial account, ie., has the user passed away? -	Memorial bool `bun:",nullzero"` -	// This account has moved this account id in the database -	MovedToAccountID string `bun:"type:CHAR(26),nullzero"` -	// When was this account created? -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this account last updated? -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Does this account identify itself as a bot? -	Bot bool -	// What reason was given for signing up when this account was created? -	Reason string `bun:",nullzero"` - -	/* -		USER AND PRIVACY PREFERENCES -	*/ - -	// Does this account need an approval for new followers? -	Locked bool `bun:",default:true"` -	// Should this account be shown in the instance's profile directory? -	Discoverable bool `bun:",default:false"` -	// Default post privacy for this account -	Privacy Visibility `bun:",default:'public'"` -	// Set posts from this account to sensitive by default? -	Sensitive bool `bun:",default:false"` -	// What language does this account post in? -	Language string `bun:",default:'en'"` - -	/* -		ACTIVITYPUB THINGS -	*/ - -	// What is the activitypub URI for this account discovered by webfinger? -	URI string `bun:",unique,nullzero"` -	// At which URL can we see the user account in a web browser? -	URL string `bun:",unique,nullzero"` -	// Last time this account was located using the webfinger API. -	LastWebfingeredAt time.Time `bun:",nullzero"` -	// Address of this account's activitypub inbox, for sending activity to -	InboxURI string `bun:",unique,nullzero"` -	// Address of this account's activitypub outbox -	OutboxURI string `bun:",unique,nullzero"` -	// URI for getting the following list of this account -	FollowingURI string `bun:",unique,nullzero"` -	// URI for getting the followers list of this account -	FollowersURI string `bun:",unique,nullzero"` -	// URL for getting the featured collection list of this account -	FeaturedCollectionURI string `bun:",unique,nullzero"` -	// What type of activitypub actor is this account? -	ActorType string `bun:",nullzero"` -	// This account is associated with x account id -	AlsoKnownAs string `bun:",nullzero"` - -	/* -		CRYPTO FIELDS -	*/ - -	// Privatekey for validating activitypub requests, will only be defined for local accounts -	PrivateKey *rsa.PrivateKey -	// Publickey for encoding activitypub requests, will be defined for both local and remote accounts -	PublicKey *rsa.PublicKey -	// Web-reachable location of this account's public key -	PublicKeyURI string `bun:",nullzero"` - -	/* -		ADMIN FIELDS -	*/ - -	// When was this account set to have all its media shown as sensitive? -	SensitizedAt time.Time `bun:",nullzero"` -	// When was this account silenced (eg., statuses only visible to followers, not public)? -	SilencedAt time.Time `bun:",nullzero"` -	// When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) -	SuspendedAt time.Time `bun:",nullzero"` -	// Should we hide this account's collections? -	HideCollections bool -	// id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID -	SuspensionOrigin string `bun:"type:CHAR(26),nullzero"` +	ID                      string           `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                                               // id of this item in the database +	CreatedAt               time.Time        `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                                          // when was item created +	UpdatedAt               time.Time        `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                                          // when was item last updated +	Username                string           `validate:"required" bun:",nullzero,notnull,unique:userdomain"`                                                         // Username of the account, should just be a string of [a-zA-Z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``. Username and domain should be unique *with* each other +	Domain                  string           `validate:"omitempty,fqdn" bun:",nullzero,unique:userdomain"`                                                           // 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. +	AvatarMediaAttachmentID string           `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                                                // Database ID of the media attachment, if present +	AvatarMediaAttachment   *MediaAttachment `validate:"-" bun:"rel:belongs-to"`                                                                                     // MediaAttachment corresponding to avatarMediaAttachmentID +	AvatarRemoteURL         string           `validate:"omitempty,url" bun:",nullzero"`                                                                              // For a non-local account, where can the header be fetched? +	HeaderMediaAttachmentID string           `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                                                // Database ID of the media attachment, if present +	HeaderMediaAttachment   *MediaAttachment `validate:"-" bun:"rel:belongs-to"`                                                                                     // MediaAttachment corresponding to headerMediaAttachmentID +	HeaderRemoteURL         string           `validate:"omitempty,url" bun:",nullzero"`                                                                              // For a non-local account, where can the header be fetched? +	DisplayName             string           `validate:"-" bun:",nullzero"`                                                                                          // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. +	Fields                  []Field          `validate:"-"`                                                                                                          // a key/value map of fields that this account has added to their profile +	Note                    string           `validate:"-" bun:",nullzero"`                                                                                          // A note that this account has on their profile (ie., the account's bio/description of themselves) +	Memorial                bool             `validate:"-" bun:",nullzero,default:false"`                                                                            // Is this a memorial account, ie., has the user passed away? +	AlsoKnownAs             string           `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                                                // This account is associated with x account id +	MovedToAccountID        string           `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                                                // This account has moved this account id in the database +	Bot                     bool             `validate:"-" bun:",nullzero,default:false"`                                                                            // Does this account identify itself as a bot? +	Reason                  string           `validate:"-" bun:",nullzero"`                                                                                          // What reason was given for signing up when this account was created? +	Locked                  bool             `validate:"-" bun:",nullzero,default:true"`                                                                             // Does this account need an approval for new followers? +	Discoverable            bool             `validate:"-" bun:",nullzero,default:false"`                                                                            // Should this account be shown in the instance's profile directory? +	Privacy                 Visibility       `validate:"required_without=Domain,omitempty,oneof=public unlocked followers_only mutuals_only direct" bun:",nullzero"` // Default post privacy for this account +	Sensitive               bool             `validate:"-" bun:",nullzero,default:false"`                                                                            // Set posts from this account to sensitive by default? +	Language                string           `validate:"omitempty,bcp47_language_tag" bun:",nullzero,notnull,default:'en'"`                                          // What language does this account post in? +	URI                     string           `validate:"required,url" bun:",nullzero,notnull,unique"`                                                                // ActivityPub URI for this account. +	URL                     string           `validate:"required_without=Domain,omitempty,url" bun:",nullzero,unique"`                                               // Web URL for this account's profile +	LastWebfingeredAt       time.Time        `validate:"required_with=Domain" bun:"type:timestamp,nullzero"`                                                         // Last time this account was refreshed/located with webfinger. +	InboxURI                string           `validate:"required_without=Domain,omitempty,url" bun:",nullzero,unique"`                                               // Address of this account's ActivityPub inbox, for sending activity to +	OutboxURI               string           `validate:"required_without=Domain,omitempty,url" bun:",nullzero,unique"`                                               // Address of this account's activitypub outbox +	FollowingURI            string           `validate:"required_without=Domain,omitempty,url" bun:",nullzero,unique"`                                               // URI for getting the following list of this account +	FollowersURI            string           `validate:"required_without=Domain,omitempty,url" bun:",nullzero,unique"`                                               // URI for getting the followers list of this account +	FeaturedCollectionURI   string           `validate:"required_without=Domain,omitempty,url" bun:",nullzero,unique"`                                               // URL for getting the featured collection list of this account +	ActorType               string           `validate:"oneof=Application Group Organization Person Service" bun:",nullzero,notnull"`                                // What type of activitypub actor is this account? +	PrivateKey              *rsa.PrivateKey  `validate:"required_without=Domain"`                                                                                    // Privatekey for validating activitypub requests, will only be defined for local accounts +	PublicKey               *rsa.PublicKey   `validate:"required"`                                                                                                   // Publickey for encoding activitypub requests, will be defined for both local and remote accounts +	PublicKeyURI            string           `validate:"required,url" bun:",nullzero,notnull,unique"`                                                                // Web-reachable location of this account's public key +	SensitizedAt            time.Time        `validate:"-" bun:"type:timestamp,nullzero"`                                                                            // When was this account set to have all its media shown as sensitive? +	SilencedAt              time.Time        `validate:"-" bun:"type:timestamp,nullzero"`                                                                            // When was this account silenced (eg., statuses only visible to followers, not public)? +	SuspendedAt             time.Time        `validate:"-" bun:"type:timestamp,nullzero"`                                                                            // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) +	HideCollections         bool             `validate:"-" bun:",nullzero,default:false"`                                                                            // Hide this account's collections +	SuspensionOrigin        string           `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                                                // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID  }  // Field represents a key value field on an account, for things like pronouns, website, etc.  // VerifiedAt is optional, to be used only if Value is a URL to a webpage that contains the  // username of the user.  type Field struct { -	Name       string -	Value      string -	VerifiedAt time.Time `bun:",nullzero"` +	Name       string    `validate:"required"`          // Name of this field. +	Value      string    `validate:"required"`          // Value of this field. +	VerifiedAt time.Time `validate:"-" bun:",nullzero"` // This field was verified at (optional). +} + +// Relationship describes a requester's relationship with another account. +type Relationship struct { +	ID                  string // The account id. +	Following           bool   // Are you following this user? +	ShowingReblogs      bool   // Are you receiving this user's boosts in your home timeline? +	Notifying           bool   // Have you enabled notifications for this user? +	FollowedBy          bool   // Are you followed by this user? +	Blocking            bool   // Are you blocking this user? +	BlockedBy           bool   // Is this user blocking you? +	Muting              bool   // Are you muting this user? +	MutingNotifications bool   // Are you muting notifications from this user? +	Requested           bool   // Do you have a pending follow request for this user? +	DomainBlocking      bool   // Are you blocking this user's domain? +	Endorsed            bool   // Are you featuring this user on your profile? +	Note                string // Your note on this account.  } diff --git a/internal/gtsmodel/activitystreams.go b/internal/gtsmodel/activitystreams.go deleted file mode 100644 index 5cd92015c..000000000 --- a/internal/gtsmodel/activitystreams.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package gtsmodel - -const ( -	// ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article -	ActivityStreamsArticle = "Article" -	// ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio -	ActivityStreamsAudio = "Audio" -	// ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document -	ActivityStreamsDocument = "Document" -	// ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event -	ActivityStreamsEvent = "Event" -	// ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image -	ActivityStreamsImage = "Image" -	// ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note -	ActivityStreamsNote = "Note" -	// ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page -	ActivityStreamsPage = "Page" -	// ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place -	ActivityStreamsPlace = "Place" -	// ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile -	ActivityStreamsProfile = "Profile" -	// ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship -	ActivityStreamsRelationship = "Relationship" -	// ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone -	ActivityStreamsTombstone = "Tombstone" -	// ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video -	ActivityStreamsVideo = "Video" -	//ActivityStreamsCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection -	ActivityStreamsCollection = "Collection" -	// ActivityStreamsCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage -	ActivityStreamsCollectionPage = "CollectionPage" -) - -const ( -	// ActivityStreamsApplication https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application -	ActivityStreamsApplication = "Application" -	// ActivityStreamsGroup https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group -	ActivityStreamsGroup = "Group" -	// ActivityStreamsOrganization https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization -	ActivityStreamsOrganization = "Organization" -	// ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person -	ActivityStreamsPerson = "Person" -	// ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service -	ActivityStreamsService = "Service" -) - -const ( -	// ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept -	ActivityStreamsAccept = "Accept" -	// ActivityStreamsAdd https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add -	ActivityStreamsAdd = "Add" -	// ActivityStreamsAnnounce https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce -	ActivityStreamsAnnounce = "Announce" -	// ActivityStreamsArrive https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive -	ActivityStreamsArrive = "Arrive" -	// ActivityStreamsBlock https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block -	ActivityStreamsBlock = "Block" -	// ActivityStreamsCreate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create -	ActivityStreamsCreate = "Create" -	// ActivityStreamsDelete https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete -	ActivityStreamsDelete = "Delete" -	// ActivityStreamsDislike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike -	ActivityStreamsDislike = "Dislike" -	// ActivityStreamsFlag https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag -	ActivityStreamsFlag = "Flag" -	// ActivityStreamsFollow https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow -	ActivityStreamsFollow = "Follow" -	// ActivityStreamsIgnore https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore -	ActivityStreamsIgnore = "Ignore" -	// ActivityStreamsInvite https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite -	ActivityStreamsInvite = "Invite" -	// ActivityStreamsJoin https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join -	ActivityStreamsJoin = "Join" -	// ActivityStreamsLeave https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave -	ActivityStreamsLeave = "Leave" -	// ActivityStreamsLike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like -	ActivityStreamsLike = "Like" -	// ActivityStreamsListen https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen -	ActivityStreamsListen = "Listen" -	// ActivityStreamsMove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move -	ActivityStreamsMove = "Move" -	// ActivityStreamsOffer https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer -	ActivityStreamsOffer = "Offer" -	// ActivityStreamsQuestion https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question -	ActivityStreamsQuestion = "Question" -	// ActivityStreamsReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject -	ActivityStreamsReject = "Reject" -	// ActivityStreamsRead https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read -	ActivityStreamsRead = "Read" -	// ActivityStreamsRemove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove -	ActivityStreamsRemove = "Remove" -	// ActivityStreamsTentativeReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject -	ActivityStreamsTentativeReject = "TentativeReject" -	// ActivityStreamsTentativeAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept -	ActivityStreamsTentativeAccept = "TentativeAccept" -	// ActivityStreamsTravel https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel -	ActivityStreamsTravel = "Travel" -	// ActivityStreamsUndo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo -	ActivityStreamsUndo = "Undo" -	// ActivityStreamsUpdate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update -	ActivityStreamsUpdate = "Update" -	// ActivityStreamsView https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view -	ActivityStreamsView = "View" -) diff --git a/internal/gtsmodel/application.go b/internal/gtsmodel/application.go index 12a21d298..7ec71ec2e 100644 --- a/internal/gtsmodel/application.go +++ b/internal/gtsmodel/application.go @@ -18,23 +18,18 @@  package gtsmodel +import "time" +  // 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.  type Application struct { -	// id of this application in the db -	ID string `bun:"type:CHAR(26),pk,notnull"` -	// name of the application given when it was created (eg., 'tusky') -	Name string `bun:",nullzero"` -	// website for the application given when it was created (eg., 'https://tusky.app') -	Website string `bun:",nullzero"` -	// redirect uri requested by the application for oauth2 flow -	RedirectURI string `bun:",nullzero"` -	// id of the associated oauth client entity in the db -	ClientID string `bun:"type:CHAR(26),nullzero"` -	// secret of the associated oauth client entity in the db -	ClientSecret string `bun:",nullzero"` -	// scopes requested when this app was created -	Scopes string `bun:",nullzero"` -	// a vapid key generated for this app when it was created -	VapidKey string `bun:",nullzero"` +	ID           string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt    time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt    time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	Name         string    `validate:"required" bun:",nullzero,notnull"`                                  // name of the application given when it was created (eg., 'tusky') +	Website      string    `validate:"omitempty,url" bun:",nullzero"`                                     // website for the application given when it was created (eg., 'https://tusky.app') +	RedirectURI  string    `validate:"required,uri" bun:",nullzero,notnull"`                              // redirect uri requested by the application for oauth2 flow +	ClientID     string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // id of the associated oauth client entity in the db +	ClientSecret string    `validate:"required,uuid" bun:",nullzero,notnull"`                             // secret of the associated oauth client entity in the db +	Scopes       string    `validate:"required" bun:",nullzero,notnull"`                                  // scopes requested when this app was created  } diff --git a/internal/gtsmodel/block.go b/internal/gtsmodel/block.go index 0c762837d..2cc089e6b 100644 --- a/internal/gtsmodel/block.go +++ b/internal/gtsmodel/block.go @@ -1,21 +1,33 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package gtsmodel  import "time"  // Block refers to the blocking of one account by another.  type Block struct { -	// id of this block in the database -	ID string `bun:"type:CHAR(26),pk,notnull"` -	// When was this block created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this block updated -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Who created this block? -	AccountID string   `bun:"type:CHAR(26),notnull"` -	Account   *Account `bun:"rel:belongs-to"` -	// Who is targeted by this block? -	TargetAccountID string   `bun:"type:CHAR(26),notnull"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// Activitypub URI for this block -	URI string `bun:",notnull"` +	ID              string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	URI             string    `validate:"required,url" bun:",notnull,nullzero,unique"`                       // ActivityPub uri of this block. +	AccountID       string    `validate:"required,ulid" bun:"type:CHAR(26),unique:blocksrctarget,notnull"`   // Who does this block originate from? +	Account         *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to accountID +	TargetAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),unique:blocksrctarget,notnull"`   // Who is the target of this block ? +	TargetAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to targetAccountID  } diff --git a/internal/gtsmodel/client.go b/internal/gtsmodel/client.go new file mode 100644 index 000000000..e924bd190 --- /dev/null +++ b/internal/gtsmodel/client.go @@ -0,0 +1,31 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// Client is a wrapper for OAuth client details. +type Client struct { +	ID        string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	Secret    string    `validate:"required,uuid" bun:",nullzero,notnull"`                             // secret generated when client was created +	Domain    string    `validate:"required,uri" bun:",nullzero,notnull"`                              // domain requested for client +	UserID    string    `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                       // id of the user that this client acts on behalf of +} diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index 784e665a5..4c72b842a 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -22,23 +22,14 @@ import "time"  // DomainBlock represents a federation block against a particular domain  type DomainBlock struct { -	// ID of this block in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// blocked domain -	Domain string `bun:",pk,notnull,unique"` -	// When was this block created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this block updated -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Account ID of the creator of this block -	CreatedByAccountID string   `bun:"type:CHAR(26),notnull"` -	CreatedByAccount   *Account `bun:"rel:belongs-to"` -	// Private comment on this block, viewable to admins -	PrivateComment string `bun:",nullzero"` -	// Public comment on this block, viewable (optionally) by everyone -	PublicComment string `bun:",nullzero"` -	// whether the domain name should appear obfuscated when displaying it publicly -	Obfuscate bool -	// if this block was created through a subscription, what's the subscription ID? -	SubscriptionID string `bun:"type:CHAR(26),nullzero"` +	ID                 string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt          time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt          time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	Domain             string    `validate:"required,fqdn" bun:",nullzero,notnull"`                             // domain to block. Eg. 'whatever.com' +	CreatedByAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // Account ID of the creator of this block +	CreatedByAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to createdByAccountID +	PrivateComment     string    `validate:"-" bun:",nullzero"`                                                 // Private comment on this block, viewable to admins +	PublicComment      string    `validate:"-" bun:",nullzero"`                                                 // Public comment on this block, viewable (optionally) by everyone +	Obfuscate          bool      `validate:"-" bun:",nullzero,default:false"`                                   // whether the domain name should appear obfuscated when displaying it publicly +	SubscriptionID     string    `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                       // if this block was created through a subscription, what's the subscription ID?  } diff --git a/internal/gtsmodel/emaildomainblock.go b/internal/gtsmodel/emaildomainblock.go index 1919172fa..2118068f2 100644 --- a/internal/gtsmodel/emaildomainblock.go +++ b/internal/gtsmodel/emaildomainblock.go @@ -22,15 +22,10 @@ import "time"  // EmailDomainBlock represents a domain that the server should automatically reject sign-up requests from.  type EmailDomainBlock struct { -	// ID of this block in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// Email domain to block. Eg. 'gmail.com' or 'hotmail.com' -	Domain string `bun:",notnull"` -	// When was this block created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this block updated -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Account ID of the creator of this block -	CreatedByAccountID string   `bun:"type:CHAR(26),notnull"` -	CreatedByAccount   *Account `bun:"rel:belongs-to"` +	ID                 string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt          time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt          time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	Domain             string    `validate:"required,fqdn" bun:",nullzero,notnull"`                             // Email domain to block. Eg. 'gmail.com' or 'hotmail.com' +	CreatedByAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // Account ID of the creator of this block +	CreatedByAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to createdByAccountID  } diff --git a/internal/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go index 9723f0790..93c43c0f7 100644 --- a/internal/gtsmodel/emoji.go +++ b/internal/gtsmodel/emoji.go @@ -22,56 +22,24 @@ import "time"  // Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens.  type Emoji struct { -	// database ID of this emoji -	ID string `bun:"type:CHAR(26),pk,notnull"` -	// String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_ -	// eg., 'blob_hug' 'purple_heart' Must be unique with domain. -	Shortcode string `bun:",notnull,unique:shortcodedomain"` -	// Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis. -	Domain string `bun:",notnull,default:'',unique:shortcodedomain"` -	// When was this emoji created. Must be unique with shortcode. -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this emoji updated -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Where can this emoji be retrieved remotely? Null for local emojis. -	// For remote emojis, it'll be something like: -	// https://hackers.town/system/custom_emojis/images/000/049/842/original/1b74481204feabfd.png -	ImageRemoteURL string `bun:",nullzero"` -	// Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis. -	// For remote emojis, it'll be something like: -	// https://hackers.town/system/custom_emojis/images/000/049/842/static/1b74481204feabfd.png -	ImageStaticRemoteURL string `bun:",nullzero"` -	// Where can this emoji be retrieved from the local server? Null for remote emojis. -	// Assuming our server is hosted at 'example.org', this will be something like: -	// 'https://example.org/fileserver/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/original/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' -	ImageURL string `bun:",nullzero"` -	// Where can a static version of this emoji be retrieved from the local server? Null for remote emojis. -	// Assuming our server is hosted at 'example.org', this will be something like: -	// 'https://example.org/fileserver/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/small/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' -	ImageStaticURL string `bun:",nullzero"` -	// Path of the emoji image in the server storage system. Will be something like: -	// '/gotosocial/storage/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/original/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' -	ImagePath string `bun:",notnull"` -	// Path of a static version of the emoji image in the server storage system. Will be something like: -	// '/gotosocial/storage/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/small/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' -	ImageStaticPath string `bun:",notnull"` -	// MIME content type of the emoji image -	// Probably "image/png" -	ImageContentType string `bun:",notnull"` -	// MIME content type of the static version of the emoji image. -	ImageStaticContentType string `bun:",notnull"` -	// Size of the emoji image file in bytes, for serving purposes. -	ImageFileSize int `bun:",notnull"` -	// Size of the static version of the emoji image file in bytes, for serving purposes. -	ImageStaticFileSize int `bun:",notnull"` -	// When was the emoji image last updated? -	ImageUpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Has a moderation action disabled this emoji from being shown? -	Disabled bool `bun:",notnull,default:false"` -	// ActivityStreams uri of this emoji. Something like 'https://example.org/emojis/1234' -	URI string `bun:",notnull,unique"` -	// Is this emoji visible in the admin emoji picker? -	VisibleInPicker bool `bun:",notnull,default:true"` -	// In which emoji category is this emoji visible? -	CategoryID string `bun:"type:CHAR(26),nullzero"` +	ID                     string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                                // id of this item in the database +	CreatedAt              time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                           // when was item created +	UpdatedAt              time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                           // when was item last updated +	Shortcode              string    `validate:"required" bun:",notnull,unique:shortcodedomain"`                                              // String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_  eg., 'blob_hug' 'purple_heart' Must be unique with domain. +	Domain                 string    `validate:"omitempty,fqdn" bun:",notnull,default:'',unique:shortcodedomain"`                             // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis. +	ImageRemoteURL         string    `validate:"required_without=ImageURL,omitempty,url" bun:",nullzero"`                                     // Where can this emoji be retrieved remotely? Null for local emojis. +	ImageStaticRemoteURL   string    `validate:"required_without=ImageStaticURL,omitempty,url" bun:",nullzero"`                               // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis. +	ImageURL               string    `validate:"required_without=ImageRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"`       // Where can this emoji be retrieved from the local server? Null for remote emojis. +	ImageStaticURL         string    `validate:"required_without=ImageStaticRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis. +	ImagePath              string    `validate:"required,file" bun:",nullzero,notnull"`                                                       // Path of the emoji image in the server storage system. +	ImageStaticPath        string    `validate:"required,file" bun:",nullzero,notnull"`                                                       // Path of a static version of the emoji image in the server storage system +	ImageContentType       string    `validate:"required" bun:",nullzero,notnull"`                                                            // MIME content type of the emoji image +	ImageStaticContentType string    `validate:"required" bun:",nullzero,notnull"`                                                            // MIME content type of the static version of the emoji image. +	ImageFileSize          int       `validate:"required,min=1" bun:",nullzero,notnull"`                                                      // Size of the emoji image file in bytes, for serving purposes. +	ImageStaticFileSize    int       `validate:"required,min=1" bun:",nullzero,notnull"`                                                      // Size of the static version of the emoji image file in bytes, for serving purposes. +	ImageUpdatedAt         time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                           // When was the emoji image last updated? +	Disabled               bool      `validate:"-" bun:",notnull,default:false"`                                                              // Has a moderation action disabled this emoji from being shown? +	URI                    string    `validate:"url" bun:",nullzero,notnull,unique"`                                                          // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234' +	VisibleInPicker        bool      `validate:"-" bun:",notnull,default:true"`                                                               // Is this emoji visible in the admin emoji picker? +	CategoryID             string    `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                                 // In which emoji category is this emoji visible?  } diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go index 1e1095af9..8c4617f48 100644 --- a/internal/gtsmodel/follow.go +++ b/internal/gtsmodel/follow.go @@ -22,22 +22,14 @@ import "time"  // Follow represents one account following another, and the metadata around that follow.  type Follow struct { -	// id of this follow in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// When was this follow created? -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this follow last updated? -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Who does this follow belong to? -	AccountID string   `bun:"type:CHAR(26),unique:srctarget,notnull"` -	Account   *Account `bun:"rel:belongs-to"` -	// Who does AccountID follow? -	TargetAccountID string   `bun:"type:CHAR(26),unique:srctarget,notnull"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// Does this follow also want to see reblogs and not just posts? -	ShowReblogs bool `bun:"default:true"` -	// What is the activitypub URI of this follow? -	URI string `bun:",unique,nullzero"` -	// does the following account want to be notified when the followed account posts? -	Notify bool +	ID              string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	URI             string    `validate:"required,url" bun:",notnull,nullzero,unique"`                       // ActivityPub uri of this follow. +	AccountID       string    `validate:"required,ulid" bun:"type:CHAR(26),unique:srctarget,notnull"`        // Who does this follow originate from? +	Account         *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to accountID +	TargetAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),unique:srctarget,notnull"`        // Who is the target of this follow ? +	TargetAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to targetAccountID +	ShowReblogs     bool      `validate:"-" bun:",nullzero,default:true"`                                    // Does this follow also want to see reblogs and not just posts? +	Notify          bool      `validate:"-" bun:",nullzero,default:false"`                                   // does the following account want to be notified when the followed account posts?  } diff --git a/internal/gtsmodel/followrequest.go b/internal/gtsmodel/followrequest.go index 5a6cb5e02..9ffdb5938 100644 --- a/internal/gtsmodel/followrequest.go +++ b/internal/gtsmodel/followrequest.go @@ -22,22 +22,14 @@ import "time"  // FollowRequest represents one account requesting to follow another, and the metadata around that request.  type FollowRequest struct { -	// id of this follow request in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// When was this follow request created? -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this follow request last updated? -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Who does this follow request originate from? -	AccountID string   `bun:"type:CHAR(26),unique:frsrctarget,notnull"` -	Account   *Account `bun:"rel:belongs-to"` -	// Who is the target of this follow request? -	TargetAccountID string   `bun:"type:CHAR(26),unique:frsrctarget,notnull"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// Does this follow also want to see reblogs and not just posts? -	ShowReblogs bool `bun:"default:true"` -	// What is the activitypub URI of this follow request? -	URI string `bun:",unique,nullzero"` -	// does the following account want to be notified when the followed account posts? -	Notify bool +	ID              string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	URI             string    `validate:"required,url" bun:",notnull,nullzero,unique"`                       // ActivityPub uri of this follow (request). +	AccountID       string    `validate:"required,ulid" bun:"type:CHAR(26),unique:frsrctarget,notnull"`      // Who does this follow request originate from? +	Account         *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to accountID +	TargetAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),unique:frsrctarget,notnull"`      // Who is the target of this follow request? +	TargetAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // Account corresponding to targetAccountID +	ShowReblogs     bool      `validate:"-" bun:",nullzero,default:true"`                                    // Does this follow also want to see reblogs and not just posts? +	Notify          bool      `validate:"-" bun:",nullzero,default:false"`                                   // does the following account want to be notified when the followed account posts?  } diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index ca2857b95..a7cc8a034 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -1,41 +1,43 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package gtsmodel  import "time"  // Instance represents a federated instance, either local or remote.  type Instance struct { -	// ID of this instance in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// Instance domain eg example.org -	Domain string `bun:",pk,notnull,unique"` -	// Title of this instance as it would like to be displayed. -	Title string `bun:",nullzero"` -	// base URI of this instance eg https://example.org -	URI string `bun:",notnull,unique"` -	// When was this instance created in the db? -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this instance last updated in the db? -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this instance suspended, if at all? -	SuspendedAt time.Time `bun:",nullzero"` -	// ID of any existing domain block for this instance in the database -	DomainBlockID string       `bun:"type:CHAR(26),nullzero"` -	DomainBlock   *DomainBlock `bun:"rel:belongs-to"` -	// Short description of this instance -	ShortDescription string `bun:",nullzero"` -	// Longer description of this instance -	Description string `bun:",nullzero"` -	// Terms and conditions of this instance -	Terms string `bun:",nullzero"` -	// Contact email address for this instance -	ContactEmail string `bun:",nullzero"` -	// Username of the contact account for this instance -	ContactAccountUsername string `bun:",nullzero"` -	// Contact account ID in the database for this instance -	ContactAccountID string   `bun:"type:CHAR(26),nullzero"` -	ContactAccount   *Account `bun:"rel:belongs-to"` -	// Reputation score of this instance -	Reputation int64 `bun:",notnull,default:0"` -	// Version of the software used on this instance -	Version string `bun:",nullzero"` +	ID                     string       `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                     // id of this item in the database +	CreatedAt              time.Time    `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                // when was item created +	UpdatedAt              time.Time    `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                // when was item last updated +	Domain                 string       `validate:"required,fqdn" bun:",nullzero,notnull,unique"`                                     // Instance domain eg example.org +	Title                  string       `validate:"-" bun:",nullzero"`                                                                // Title of this instance as it would like to be displayed. +	URI                    string       `validate:"required,url" bun:",nullzero,notnull,unique"`                                      // base URI of this instance eg https://example.org +	SuspendedAt            time.Time    `validate:"-" bun:"type:timestamp,nullzero"`                                                  // When was this instance suspended, if at all? +	DomainBlockID          string       `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                      // ID of any existing domain block for this instance in the database +	DomainBlock            *DomainBlock `validate:"-" bun:"rel:belongs-to"`                                                           // Domain block corresponding to domainBlockID +	ShortDescription       string       `validate:"-" bun:",nullzero"`                                                                // Short description of this instance +	Description            string       `validate:"-" bun:",nullzero"`                                                                // Longer description of this instance +	Terms                  string       `validate:"-" bun:",nullzero"`                                                                // Terms and conditions of this instance +	ContactEmail           string       `validate:"omitempty,email" bun:",nullzero"`                                                  // Contact email address for this instance +	ContactAccountUsername string       `validate:"required_with=ContactAccountID" bun:",nullzero"`                                   // Username of the contact account for this instance +	ContactAccountID       string       `validate:"required_with=ContactAccountUsername,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // Contact account ID in the database for this instance +	ContactAccount         *Account     `validate:"-" bun:"rel:belongs-to"`                                                           // account corresponding to contactAccountID +	Reputation             int64        `validate:"-" bun:",notnull,default:0"`                                                       // Reputation score of this instance +	Version                string       `validate:"-" bun:",nullzero"`                                                                // Version of the software used on this instance  } diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go index 2acf6a6fc..59cf8aac1 100644 --- a/internal/gtsmodel/mediaattachment.go +++ b/internal/gtsmodel/mediaattachment.go @@ -25,127 +25,93 @@ import (  // MediaAttachment represents a user-uploaded media attachment: an image/video/audio/gif that is  // somewhere in storage and that can be retrieved and served by the router.  type MediaAttachment struct { -	// ID of the attachment in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// ID of the status to which this is attached -	StatusID string `bun:"type:CHAR(26),nullzero"` -	// Where can the attachment be retrieved on *this* server -	URL string `bun:",nullzero"` -	// Where can the attachment be retrieved on a remote server (empty for local media) -	RemoteURL string `bun:",nullzero"` -	// When was the attachment created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was the attachment last updated -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Type of file (image/gif/audio/video) -	Type FileType `bun:",notnull"` -	// Metadata about the file -	FileMeta FileMeta -	// To which account does this attachment belong -	AccountID string   `bun:"type:CHAR(26),notnull"` -	Account   *Account `bun:"rel:has-one"` -	// Description of the attachment (for screenreaders) -	Description string `bun:",nullzero"` -	// To which scheduled status does this attachment belong -	ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` -	// What is the generated blurhash of this attachment -	Blurhash string `bun:",nullzero"` -	// What is the processing status of this attachment -	Processing ProcessingStatus -	// metadata for the whole file -	File File -	// small image thumbnail derived from a larger image, video, or audio file. -	Thumbnail Thumbnail -	// Is this attachment being used as an avatar? -	Avatar bool -	// Is this attachment being used as a header? -	Header bool +	ID                string           `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                       // id of this item in the database +	CreatedAt         time.Time        `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                  // when was item created +	UpdatedAt         time.Time        `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                  // when was item last updated +	StatusID          string           `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                        // ID of the status to which this is attached +	URL               string           `validate:"required_without=RemoteURL,omitempty,url" bun:",nullzero"`                           // Where can the attachment be retrieved on *this* server +	RemoteURL         string           `validate:"required_without=URL,omitempty,url" bun:",nullzero"`                                 // Where can the attachment be retrieved on a remote server (empty for local media) +	Type              FileType         `validate:"oneof=Image Gif Audio Video Unknown" bun:",notnull"`                                 // Type of file (image/gif/audio/video) +	FileMeta          FileMeta         `validate:"required" bun:",nullzero,notnull"`                                                   // Metadata about the file +	AccountID         string           `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                                 // To which account does this attachment belong +	Account           *Account         `validate:"-" bun:"rel:has-one"`                                                                // Account corresponding to accountID +	Description       string           `validate:"-" bun:",nullzero"`                                                                  // Description of the attachment (for screenreaders) +	ScheduledStatusID string           `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                        // To which scheduled status does this attachment belong +	Blurhash          string           `validate:"required_if=Type Image,required_if=Type Gif,required_if=Type Video" bun:",nullzero"` // What is the generated blurhash of this attachment +	Processing        ProcessingStatus `validate:"oneof=0 1 2 666" bun:",notnull,default:2"`                                           // What is the processing status of this attachment +	File              File             `validate:"required" bun:",notnull,nullzero"`                                                   // metadata for the whole file +	Thumbnail         Thumbnail        `validate:"required" bun:",notnull,nullzero"`                                                   // small image thumbnail derived from a larger image, video, or audio file. +	Avatar            bool             `validate:"-" bun:",notnull,default:false"`                                                     // Is this attachment being used as an avatar? +	Header            bool             `validate:"-" bun:",notnull,default:false"`                                                     // Is this attachment being used as a header?  }  // File refers to the metadata for the whole file  type File struct { -	// What is the path of the file in storage. -	Path string `bun:",nullzero"` -	// What is the MIME content type of the file. -	ContentType string `bun:",nullzero"` -	// What is the size of the file in bytes. -	FileSize int -	// When was the file last updated. -	UpdatedAt time.Time `bun:",notnull,default:current_timestamp"` +	Path        string    `validate:"required,file" bun:",nullzero,notnull"`                             // Path of the file in storage. +	ContentType string    `validate:"required" bun:",nullzero,notnull"`                                  // MIME content type of the file. +	FileSize    int       `validate:"required" bun:",nullzero,notnull"`                                  // File size in bytes +	UpdatedAt   time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // When was the file last updated.  }  // Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.  type Thumbnail struct { -	// What is the path of the file in storage -	Path string `bun:",nullzero"` -	// What is the MIME content type of the file. -	ContentType string `bun:",nullzero"` -	// What is the size of the file in bytes -	FileSize int -	// When was the file last updated -	UpdatedAt time.Time `bun:",notnull,default:current_timestamp"` -	// What is the URL of the thumbnail on the local server -	URL string `bun:",nullzero"` -	// What is the remote URL of the thumbnail (empty for local media) -	RemoteURL string `bun:",nullzero"` +	Path        string    `validate:"required,file" bun:",nullzero,notnull"`                             // Path of the file in storage. +	ContentType string    `validate:"required" bun:",nullzero,notnull"`                                  // MIME content type of the file. +	FileSize    int       `validate:"required" bun:",nullzero,notnull"`                                  // File size in bytes +	UpdatedAt   time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // When was the file last updated. +	URL         string    `validate:"required_without=RemoteURL,omitempty,url" bun:",nullzero"`          // What is the URL of the thumbnail on the local server +	RemoteURL   string    `validate:"required_without=URL,omitempty,url" bun:",nullzero"`                // What is the remote URL of the thumbnail (empty for local media)  }  // ProcessingStatus refers to how far along in the processing stage the attachment is.  type ProcessingStatus int +// MediaAttachment processing states.  const ( -	// ProcessingStatusReceived indicates the attachment has been received and is awaiting processing. No thumbnail available yet. -	ProcessingStatusReceived ProcessingStatus = 0 -	// ProcessingStatusProcessing indicates the attachment is currently being processed. Thumbnail is available but full media is not. -	ProcessingStatusProcessing ProcessingStatus = 1 -	// ProcessingStatusProcessed indicates the attachment has been fully processed and is ready to be served. -	ProcessingStatusProcessed ProcessingStatus = 2 -	// ProcessingStatusError indicates something went wrong processing the attachment and it won't be tried again--these can be deleted. -	ProcessingStatusError ProcessingStatus = 666 +	ProcessingStatusReceived   ProcessingStatus = 0   // ProcessingStatusReceived indicates the attachment has been received and is awaiting processing. No thumbnail available yet. +	ProcessingStatusProcessing ProcessingStatus = 1   // ProcessingStatusProcessing indicates the attachment is currently being processed. Thumbnail is available but full media is not. +	ProcessingStatusProcessed  ProcessingStatus = 2   // ProcessingStatusProcessed indicates the attachment has been fully processed and is ready to be served. +	ProcessingStatusError      ProcessingStatus = 666 // ProcessingStatusError indicates something went wrong processing the attachment and it won't be tried again--these can be deleted.  )  // FileType refers to the file type of the media attaachment.  type FileType string +// MediaAttachment file types.  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" -	// 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" +	FileTypeImage   FileType = "Image"   // FileTypeImage is for jpegs and pngs +	FileTypeGif     FileType = "Gif"     // FileTypeGif is for native gifs and soundless videos that have been converted to gifs +	FileTypeAudio   FileType = "Audio"   // FileTypeAudio is for audio-only files (no video) +	FileTypeVideo   FileType = "Video"   // FileTypeVideo is for files with audio + visual +	FileTypeUnknown FileType = "Unknown" // FileTypeUnknown is for unknown file types (surprise surprise!)  )  // FileMeta describes metadata about the actual contents of the file.  type FileMeta struct { -	Original Original +	Original Original `validate:"required"`  	Small    Small  	Focus    Focus  }  // Small can be used for a thumbnail of any media type  type Small struct { -	Width  int -	Height int -	Size   int -	Aspect float64 +	Width  int     `validate:"required_with=Height Size Aspect"`  // width in pixels +	Height int     `validate:"required_with=Width Size Aspect"`   // height in pixels +	Size   int     `validate:"required_with=Width Height Aspect"` // size in pixels (width * height) +	Aspect float64 `validate:"required_with=Widhth Height Size"`  // aspect ratio (width / height)  }  // Original can be used for original metadata for any media type  type Original struct { -	Width  int -	Height int -	Size   int -	Aspect float64 +	Width  int     `validate:"required_with=Height Size Aspect"`  // width in pixels +	Height int     `validate:"required_with=Width Size Aspect"`   // height in pixels +	Size   int     `validate:"required_with=Width Height Aspect"` // size in pixels (width * height) +	Aspect float64 `validate:"required_with=Widhth Height Size"`  // aspect ratio (width / height)  }  // Focus describes the 'center' of the image for display purposes.  // X and Y should each be between -1 and 1  type Focus struct { -	X float32 -	Y float32 +	X float32 `validate:"omitempty,max=1,min=-1"` +	Y float32 `validate:"omitempty,max=1,min=-1"`  } diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 79556500f..492740d77 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -22,25 +22,17 @@ import "time"  // Mention refers to the 'tagging' or 'mention' of a user within a status.  type Mention struct { -	// ID of this mention in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// ID of the status this mention originates from -	StatusID string  `bun:"type:CHAR(26),notnull,nullzero"` -	Status   *Status `bun:"rel:belongs-to"` -	// When was this mention created? -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When was this mention last updated? -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// What's the internal account ID of the originator of the mention? -	OriginAccountID string   `bun:"type:CHAR(26),notnull,nullzero"` -	OriginAccount   *Account `bun:"rel:belongs-to"` -	// What's the AP URI of the originator of the mention? -	OriginAccountURI string `bun:",notnull"` -	// What's the internal account ID of the mention target? -	TargetAccountID string   `bun:"type:CHAR(26),notnull,nullzero"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// Prevent this mention from generating a notification? -	Silent bool +	ID               string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt        time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt        time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	StatusID         string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // ID of the status this mention originates from +	Status           *Status   `validate:"-" bun:"rel:belongs-to"`                                            // status referred to by statusID +	OriginAccountID  string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // ID of the mention creator account +	OriginAccountURI string    `validate:"url" bun:",nullzero,notnull"`                                       // ActivityPub URI of the originator/creator of the mention +	OriginAccount    *Account  `validate:"-" bun:"rel:belongs-to"`                                            // account referred to by originAccountID +	TargetAccountID  string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // Mention target/receiver account ID +	TargetAccount    *Account  `validate:"-" bun:"rel:belongs-to"`                                            // account referred to by targetAccountID +	Silent           bool      `validate:"-" bun:",notnull,default:false"`                                    // Prevent this mention from generating a notification?  	/*  		NON-DATABASE CONVENIENCE FIELDS @@ -54,15 +46,14 @@ type Mention struct {  	// @whatever_username@example.org  	//  	// This will not be put in the database, it's just for convenience. -	NameString string `bun:"-"` +	NameString string `validate:"-" bun:"-"`  	// TargetAccountURI is the AP ID (uri) of the user mentioned.  	//  	// This will not be put in the database, it's just for convenience. -	TargetAccountURI string `bun:"-"` +	TargetAccountURI string `validate:"-" bun:"-"`  	// TargetAccountURL is the web url of the user mentioned.  	//  	// This will not be put in the database, it's just for convenience. -	TargetAccountURL string `bun:"-"` +	TargetAccountURL string `validate:"-" bun:"-"`  	// A pointer to the gtsmodel account of the mentioned account. -  } diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index 14ab90802..1b1f39a77 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -22,41 +22,29 @@ import "time"  // Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc.  type Notification struct { -	// ID of this notification in the database -	ID string `bun:"type:CHAR(26),pk,notnull"` -	// Type of this notification -	NotificationType NotificationType `bun:",notnull"` -	// Creation time of this notification -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// Which account does this notification target (ie., who will receive the notification?) -	TargetAccountID string   `bun:"type:CHAR(26),notnull"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// Which account performed the action that created this notification? -	OriginAccountID string   `bun:"type:CHAR(26),notnull"` -	OriginAccount   *Account `bun:"rel:belongs-to"` -	// If the notification pertains to a status, what is the database ID of that status? -	StatusID string  `bun:"type:CHAR(26),nullzero"` -	Status   *Status `bun:"rel:belongs-to"` -	// Has this notification been read already? -	Read bool +	ID               string           `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                                                                                                                                    // id of this item in the database +	CreatedAt        time.Time        `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                                                                                                                               // when was item created +	UpdatedAt        time.Time        `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                                                                                                                               // when was item last updated                                                                                                                            // when was item created +	NotificationType NotificationType `validate:"oneof=follow follow_request mention reblog favourite poll status" bun:",nullzero,notnull"`                                                                                                        // Type of this notification +	TargetAccountID  string           `validate:"ulid" bun:"type:CHAR(26),nullzero,notnull"`                                                                                                                                                       // Which account does this notification target (ie., who will receive the notification?) +	TargetAccount    *Account         `validate:"-" bun:"rel:belongs-to"`                                                                                                                                                                          // Which account performed the action that created this notification? +	OriginAccountID  string           `validate:"ulid" bun:"type:CHAR(26),nullzero,notnull"`                                                                                                                                                       // ID of the account that performed the action that created the notification. +	OriginAccount    *Account         `validate:"-" bun:"rel:belongs-to"`                                                                                                                                                                          // Account corresponding to originAccountID +	StatusID         string           `validate:"required_if=NotificationType mention,required_if=NotificationType reblog,required_if=NotificationType favourite,required_if=NotificationType status,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // If the notification pertains to a status, what is the database ID of that status? +	Status           *Status          `validate:"-" bun:"rel:belongs-to"`                                                                                                                                                                          // Status corresponding to statusID +	Read             bool             `validate:"-" bun:",notnull,default:false"`                                                                                                                                                                  // Notification has been seen/read  }  // NotificationType describes the reason/type of this notification.  type NotificationType string +// Notification Types  const ( -	// NotificationFollow -- someone followed you -	NotificationFollow NotificationType = "follow" -	// NotificationFollowRequest -- someone requested to follow you -	NotificationFollowRequest NotificationType = "follow_request" -	// NotificationMention -- someone mentioned you in their status -	NotificationMention NotificationType = "mention" -	// NotificationReblog -- someone boosted one of your statuses -	NotificationReblog NotificationType = "reblog" -	// NotificationFave -- someone faved/liked one of your statuses -	NotificationFave NotificationType = "favourite" -	// NotificationPoll -- a poll you voted in or created has ended -	NotificationPoll NotificationType = "poll" -	// NotificationStatus -- someone you enabled notifications for has posted a status. -	NotificationStatus NotificationType = "status" +	NotificationFollow        NotificationType = "follow"         // NotificationFollow -- someone followed you +	NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you +	NotificationMention       NotificationType = "mention"        // NotificationMention -- someone mentioned you in their status +	NotificationReblog        NotificationType = "reblog"         // NotificationReblog -- someone boosted one of your statuses +	NotificationFave          NotificationType = "favourite"      // NotificationFave -- someone faved/liked one of your statuses +	NotificationPoll          NotificationType = "poll"           // NotificationPoll -- a poll you voted in or created has ended +	NotificationStatus        NotificationType = "status"         // NotificationStatus -- someone you enabled notifications for has posted a status.  ) diff --git a/internal/gtsmodel/relationship.go b/internal/gtsmodel/relationship.go deleted file mode 100644 index 4e6cc03f6..000000000 --- a/internal/gtsmodel/relationship.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package gtsmodel - -// Relationship describes a requester's relationship with another account. -type Relationship struct { -	// The account id. -	ID string -	// Are you following this user? -	Following bool -	// Are you receiving this user's boosts in your home timeline? -	ShowingReblogs bool -	// Have you enabled notifications for this user? -	Notifying bool -	// Are you followed by this user? -	FollowedBy bool -	// Are you blocking this user? -	Blocking bool -	// Is this user blocking you? -	BlockedBy bool -	// Are you muting this user? -	Muting bool -	// Are you muting notifications from this user? -	MutingNotifications bool -	// Do you have a pending follow request for this user? -	Requested bool -	// Are you blocking this user's domain? -	DomainBlocking bool -	// Are you featuring this user on your profile? -	Endorsed bool -	// Your note on this account. -	Note string -} diff --git a/internal/gtsmodel/routersession.go b/internal/gtsmodel/routersession.go index fbc1c7768..3edb8bc36 100644 --- a/internal/gtsmodel/routersession.go +++ b/internal/gtsmodel/routersession.go @@ -18,9 +18,13 @@  package gtsmodel +import "time" +  // RouterSession is used to store and retrieve settings for a router session.  type RouterSession struct { -	ID    string `bun:"type:CHAR(26),pk,notnull"` -	Auth  []byte `bun:"type:bytea,notnull,nullzero"` -	Crypt []byte `bun:"type:bytea,notnull,nullzero"` +	ID        string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	Auth      []byte    `validate:"required,len=32" bun:"type:bytea,notnull,nullzero"` +	Crypt     []byte    `validate:"required,len=32" bun:"type:bytea,notnull,nullzero"`  } diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 1997ad5df..f298e71cd 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -24,87 +24,59 @@ import (  // Status represents a user-created 'post' or 'status' in the database, either remote or local  type Status struct { -	// id of the status in the database -	ID string `bun:"type:CHAR(26),pk,notnull"` -	// uri at which this status is reachable -	URI string `bun:",unique,nullzero"` -	// web url for viewing this status -	URL string `bun:",unique,nullzero"` -	// the html-formatted content of this status -	Content string `bun:",nullzero"` -	// Database IDs of any media attachments associated with this status -	AttachmentIDs []string           `bun:"attachments,array"` -	Attachments   []*MediaAttachment `bun:"attached_media,rel:has-many"` -	// Database IDs of any tags used in this status -	TagIDs []string `bun:"tags,array"` -	Tags   []*Tag   `bun:"attached_tags,m2m:status_to_tags"` // https://bun.uptrace.dev/guide/relations.html#many-to-many-relation -	// Database IDs of any mentions in this status -	MentionIDs []string   `bun:"mentions,array"` -	Mentions   []*Mention `bun:"attached_mentions,rel:has-many"` -	// Database IDs of any emojis used in this status -	EmojiIDs []string `bun:"emojis,array"` -	Emojis   []*Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // https://bun.uptrace.dev/guide/relations.html#many-to-many-relation -	// when was this status created? -	CreatedAt time.Time `bun:",notnull,nullzero,default:current_timestamp"` -	// when was this status updated? -	UpdatedAt time.Time `bun:",notnull,nullzero,default:current_timestamp"` -	// is this status from a local account? -	Local bool -	// which account posted this status? -	AccountID string   `bun:"type:CHAR(26),notnull"` -	Account   *Account `bun:"rel:belongs-to"` -	// AP uri of the owner of this status -	AccountURI string `bun:",nullzero"` -	// id of the status this status is a reply to -	InReplyToID string  `bun:"type:CHAR(26),nullzero"` -	InReplyTo   *Status `bun:"-"` -	// AP uri of the status this status is a reply to -	InReplyToURI string `bun:",nullzero"` -	// id of the account that this status replies to -	InReplyToAccountID string   `bun:"type:CHAR(26),nullzero"` -	InReplyToAccount   *Account `bun:"rel:belongs-to"` -	// id of the status this status is a boost of -	BoostOfID string  `bun:"type:CHAR(26),nullzero"` -	BoostOf   *Status `bun:"-"` -	// id of the account that owns the boosted status -	BoostOfAccountID string   `bun:"type:CHAR(26),nullzero"` -	BoostOfAccount   *Account `bun:"rel:belongs-to"` -	// cw string for this status -	ContentWarning string `bun:",nullzero"` -	// visibility entry for this status -	Visibility Visibility `bun:",notnull"` -	// mark the status as sensitive? -	Sensitive bool -	// what language is this status written in? -	Language string `bun:",nullzero"` -	// Which application was used to create this status? -	CreatedWithApplicationID string       `bun:"type:CHAR(26),nullzero"` -	CreatedWithApplication   *Application `bun:"rel:belongs-to"` -	// advanced visibility for this status -	VisibilityAdvanced *VisibilityAdvanced -	// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types -	// Will probably almost always be Note but who knows!. -	ActivityStreamsType string `bun:",nullzero"` -	// Original text of the status without formatting -	Text string `bun:",nullzero"` -	// Has this status been pinned by its owner? -	Pinned bool +	ID                       string             `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                              // id of this item in the database +	CreatedAt                time.Time          `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                         // when was item created +	UpdatedAt                time.Time          `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"`                         // when was item last updated +	URI                      string             `validate:"required,url" bun:",unique,nullzero,notnull"`                                               // activitypub URI of this status +	URL                      string             `validate:"url" bun:",nullzero"`                                                                       // web url for viewing this status +	Content                  string             `validate:"-" bun:",nullzero"`                                                                         // content of this status; likely html-formatted but not guaranteed +	AttachmentIDs            []string           `validate:"dive,ulid" bun:"attachments,array"`                                                         // Database IDs of any media attachments associated with this status +	Attachments              []*MediaAttachment `validate:"-" bun:"attached_media,rel:has-many"`                                                       // Attachments corresponding to attachmentIDs +	TagIDs                   []string           `validate:"dive,ulid" bun:"tags,array"`                                                                // Database IDs of any tags used in this status +	Tags                     []*Tag             `validate:"-" bun:"attached_tags,m2m:status_to_tags"`                                                  // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation +	MentionIDs               []string           `validate:"dive,ulid" bun:"mentions,array"`                                                            // Database IDs of any mentions in this status +	Mentions                 []*Mention         `validate:"-" bun:"attached_mentions,rel:has-many"`                                                    // Mentions corresponding to mentionIDs +	EmojiIDs                 []string           `validate:"dive,ulid" bun:"emojis,array"`                                                              // Database IDs of any emojis used in this status +	Emojis                   []*Emoji           `validate:"-" bun:"attached_emojis,m2m:status_to_emojis"`                                              // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation +	Local                    bool               `validate:"-" bun:",notnull,default:false"`                                                            // is this status from a local account? +	AccountID                string             `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                                        // which account posted this status? +	Account                  *Account           `validate:"-" bun:"rel:belongs-to"`                                                                    // account corresponding to accountID +	AccountURI               string             `validate:"required,url" bun:",nullzero,notnull"`                                                      // activitypub uri of the owner of this status +	InReplyToID              string             `validate:"required_with=InReplyToURI InReplyToAccountID,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // id of the status this status replies to +	InReplyToURI             string             `validate:"required_with=InReplyToID InReplyToAccountID,omitempty,url" bun:",nullzero"`                // activitypub uri of the status this status is a reply to +	InReplyToAccountID       string             `validate:"required_with=InReplyToID InReplyToURI,omitempty,ulid" bun:"type:CHAR(26),nullzero"`        // id of the account that this status replies to +	InReplyTo                *Status            `validate:"-" bun:"-"`                                                                                 // status corresponding to inReplyToID +	InReplyToAccount         *Account           `validate:"-" bun:"rel:belongs-to"`                                                                    // account corresponding to inReplyToAccountID +	BoostOfID                string             `validate:"required_with=BoostOfAccountID,omitempty,ulid" bun:"type:CHAR(26),nullzero"`                // id of the status this status is a boost of +	BoostOfAccountID         string             `validate:"required_with=BoostOfID,omitempty,ulid" bun:"type:CHAR(26),nullzero"`                       // id of the account that owns the boosted status +	BoostOf                  *Status            `validate:"-" bun:"-"`                                                                                 // status that corresponds to boostOfID +	BoostOfAccount           *Account           `validate:"-" bun:"rel:belongs-to"`                                                                    // account that corresponds to boostOfAccountID +	ContentWarning           string             `validate:"-" bun:",nullzero"`                                                                         // cw string for this status +	Visibility               Visibility         `validate:"-" bun:",nullzero,notnull"`                                                                 // visibility entry for this status +	Sensitive                bool               `validate:"-" bun:",notnull,default:false"`                                                            // mark the status as sensitive? +	Language                 string             `validate:"-" bun:",nullzero"`                                                                         // what language is this status written in? +	CreatedWithApplicationID string             `validate:"required_if=Local true,omitempty,ulid" bun:"type:CHAR(26),nullzero"`                        // Which application was used to create this status? +	CreatedWithApplication   *Application       `validate:"-" bun:"rel:belongs-to"`                                                                    // application corresponding to createdWithApplicationID +	VisibilityAdvanced       VisibilityAdvanced `validate:"required" bun:",nullzero,notnull" `                                                         // advanced visibility for this status +	ActivityStreamsType      string             `validate:"required" bun:",nullzero,notnull"`                                                          // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!. +	Text                     string             `validate:"-" bun:",nullzero"`                                                                         // Original text of the status without formatting +	Pinned                   bool               `validate:"-" bun:",notnull,default:false" `                                                           // Has this status been pinned by its owner?  }  // StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags.  type StatusToTag struct { -	StatusID string  `bun:"type:CHAR(26),unique:statustag,nullzero"` -	Status   *Status `bun:"rel:belongs-to"` -	TagID    string  `bun:"type:CHAR(26),unique:statustag,nullzero"` -	Tag      *Tag    `bun:"rel:belongs-to"` +	StatusID string  `validate:"ulid,required" bun:"type:CHAR(26),unique:statustag,nullzero,notnull"` +	Status   *Status `validate:"-" bun:"rel:belongs-to"` +	TagID    string  `validate:"ulid,required" bun:"type:CHAR(26),unique:statustag,nullzero,notnull"` +	Tag      *Tag    `validate:"-" bun:"rel:belongs-to"`  }  // StatusToEmoji is an intermediate struct to facilitate the many2many relationship between a status and one or more emojis.  type StatusToEmoji struct { -	StatusID string  `bun:"type:CHAR(26),unique:statusemoji,nullzero"` -	Status   *Status `bun:"rel:belongs-to"` -	EmojiID  string  `bun:"type:CHAR(26),unique:statusemoji,nullzero"` -	Emoji    *Emoji  `bun:"rel:belongs-to"` +	StatusID string  `validate:"ulid,required" bun:"type:CHAR(26),unique:statusemoji,nullzero,notnull"` +	Status   *Status `validate:"-" bun:"rel:belongs-to"` +	EmojiID  string  `validate:"ulid,required" bun:"type:CHAR(26),unique:statusemoji,nullzero,notnull"` +	Emoji    *Emoji  `validate:"-" bun:"rel:belongs-to"`  }  // Visibility represents the visibility granularity of a status. @@ -137,12 +109,8 @@ const (  //  // If DIRECT is selected, boostable will be FALSE, and all other flags will be TRUE.  type VisibilityAdvanced struct { -	// This status will be federated beyond the local timeline(s) -	Federated bool `bun:"default:true"` -	// This status can be boosted/reblogged -	Boostable bool `bun:"default:true"` -	// This status can be replied to -	Replyable bool `bun:"default:true"` -	// This status can be liked/faved -	Likeable bool `bun:"default:true"` +	Federated bool `validate:"-" bun:",notnull,default:true"` // This status will be federated beyond the local timeline(s) +	Boostable bool `validate:"-" bun:",notnull,default:true"` // This status can be boosted/reblogged +	Replyable bool `validate:"-" bun:",notnull,default:true"` // This status can be replied to +	Likeable  bool `validate:"-" bun:",notnull,default:true"` // This status can be liked/faved  } diff --git a/internal/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go index 26dafa420..3dcf4cb92 100644 --- a/internal/gtsmodel/statusbookmark.go +++ b/internal/gtsmodel/statusbookmark.go @@ -20,18 +20,15 @@ package gtsmodel  import "time" -// StatusBookmark refers to one account having a 'bookmark' of the status of another account +// StatusBookmark refers to one account having a 'bookmark' of the status of another account.  type StatusBookmark struct { -	// id of this bookmark in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// when was this bookmark created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// id of the account that created ('did') the bookmarking -	AccountID string   `bun:"type:CHAR(26),notnull"` -	Account   *Account `bun:"rel:belongs-to"` -	// id the account owning the bookmarked status -	TargetAccountID string   `bun:"type:CHAR(26),notnull"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// database id of the status that has been bookmarked -	StatusID string `bun:"type:CHAR(26),notnull"` +	ID              string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	AccountID       string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // id of the account that created ('did') the bookmark +	Account         *Account  `validate:"-" bun:"rel:belongs-to"`                                            // account that created the bookmark +	TargetAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // id the account owning the bookmarked status +	TargetAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // account owning the bookmarked status +	StatusID        string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // database id of the status that has been bookmarked +	Status          *Status   `validate:"-" bun:"rel:belongs-to"`                                            // the bookmarked status  } diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index 3b816af56..93bcda0e6 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -22,19 +22,14 @@ import "time"  // StatusFave refers to a 'fave' or 'like' in the database, from one account, targeting the status of another account  type StatusFave struct { -	// id of this fave in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// when was this fave created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// id of the account that created ('did') the fave -	AccountID string   `bun:"type:CHAR(26),notnull"` -	Account   *Account `bun:"rel:belongs-to"` -	// id the account owning the faved status -	TargetAccountID string   `bun:"type:CHAR(26),notnull"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// database id of the status that has been 'faved' -	StatusID string  `bun:"type:CHAR(26),notnull"` -	Status   *Status `bun:"rel:belongs-to"` -	// ActivityPub URI of this fave -	URI string `bun:",notnull"` +	ID              string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	AccountID       string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // id of the account that created ('did') the fave +	Account         *Account  `validate:"-" bun:"rel:belongs-to"`                                            // account that created the fave +	TargetAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // id the account owning the faved status +	TargetAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // account owning the faved status +	StatusID        string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // database id of the status that has been 'faved' +	Status          *Status   `validate:"-" bun:"rel:belongs-to"`                                            // the faved status +	URI             string    `validate:"required,url" bun:",nullzero,notnull"`                              // ActivityPub URI of this fave  } diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go index 56a792ab4..2c03b8085 100644 --- a/internal/gtsmodel/statusmute.go +++ b/internal/gtsmodel/statusmute.go @@ -20,19 +20,15 @@ package gtsmodel  import "time" -// StatusMute refers to one account having muted the status of another account or its own +// StatusMute refers to one account having muted the status of another account or its own.  type StatusMute struct { -	// id of this mute in the database -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// when was this mute created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// id of the account that created ('did') the mute -	AccountID string   `bun:"type:CHAR(26),notnull"` -	Account   *Account `bun:"rel:belongs-to"` -	// id the account owning the muted status (can be the same as accountID) -	TargetAccountID string   `bun:"type:CHAR(26),notnull"` -	TargetAccount   *Account `bun:"rel:belongs-to"` -	// database id of the status that has been muted -	StatusID string  `bun:"type:CHAR(26),notnull"` -	Status   *Status `bun:"rel:belongs-to"` +	ID              string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt       time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	AccountID       string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // id of the account that created ('did') the mute +	Account         *Account  `validate:"-" bun:"rel:belongs-to"`                                            // pointer to the account specified by accountID +	TargetAccountID string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // id the account owning the muted status (can be the same as accountID) +	TargetAccount   *Account  `validate:"-" bun:"rel:belongs-to"`                                            // pointer to the account specified by targetAccountID +	StatusID        string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // database id of the status that has been muted +	Status          *Status   `validate:"-" bun:"rel:belongs-to"`                                            // pointer to the muted status specified by statusID  } diff --git a/internal/gtsmodel/tag.go b/internal/gtsmodel/tag.go index d4be0b66c..295447c4f 100644 --- a/internal/gtsmodel/tag.go +++ b/internal/gtsmodel/tag.go @@ -20,24 +20,15 @@ package gtsmodel  import "time" -// Tag represents a hashtag for gathering public statuses together +// Tag represents a hashtag for gathering public statuses together.  type Tag struct { -	// id of this tag in the database -	ID string `bun:",unique,type:CHAR(26),pk,notnull"` -	// Href of this tag, eg https://example.org/tags/somehashtag -	URL string `bun:",nullzero"` -	// name of this tag -- the tag without the hash part -	Name string `bun:",unique,notnull"` -	// Which account ID is the first one we saw using this tag? -	FirstSeenFromAccountID string `bun:"type:CHAR(26),nullzero"` -	// when was this tag created -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// when was this tag last updated -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// can our instance users use this tag? -	Useable bool `bun:",notnull,default:true"` -	// can our instance users look up this tag? -	Listable bool `bun:",notnull,default:true"` -	// when was this tag last used? -	LastStatusAt time.Time `bun:",nullzero"` +	ID                     string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt              time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt              time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	URL                    string    `validate:"required,url" bun:",nullzero,notnull"`                              // Href/web address of this tag, eg https://example.org/tags/somehashtag +	Name                   string    `validate:"required" bun:",unique,nullzero,notnull"`                           // name of this tag -- the tag without the hash part +	FirstSeenFromAccountID string    `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                       // Which account ID is the first one we saw using this tag? +	Useable                bool      `validate:"-" bun:",notnull,default:true"`                                     // can our instance users use this tag? +	Listable               bool      `validate:"-" bun:",notnull,default:true"`                                     // can our instance users look up this tag? +	LastStatusAt           time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was this tag last used?  } diff --git a/internal/gtsmodel/token.go b/internal/gtsmodel/token.go new file mode 100644 index 000000000..5fa96e915 --- /dev/null +++ b/internal/gtsmodel/token.go @@ -0,0 +1,43 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// Token is a translation of the gotosocial token with the ExpiresIn fields replaced with ExpiresAt. +type Token struct { +	ID                  string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt           time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt           time.Time `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	ClientID            string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // ID of the client who owns this token +	UserID              string    `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"`                // ID of the user who owns this token +	RedirectURI         string    `validate:"required,uri" bun:",nullzero,notnull"`                              // Oauth redirect URI for this token +	Scope               string    `validate:"required" bun:",nullzero,notnull"`                                  // Oauth scope +	Code                string    `validate:"-" bun:",pk,nullzero,notnull,default:''"`                           // Code, if present +	CodeChallenge       string    `validate:"-" bun:",nullzero"`                                                 // Code challenge, if code present +	CodeChallengeMethod string    `validate:"-" bun:",nullzero"`                                                 // Code challenge method, if code present +	CodeCreateAt        time.Time `validate:"required_with=Code" bun:"type:timestamp,nullzero"`                  // Code created time, if code present +	CodeExpiresAt       time.Time `validate:"-" bun:"type:timestamp,nullzero"`                                   // Code expires at -- null means the code never expires +	Access              string    `validate:"-" bun:",pk,nullzero,notnull,default:''"`                           // User level access token, if present +	AccessCreateAt      time.Time `validate:"required_with=Access" bun:"type:timestamp,nullzero"`                // User level access token created time, if access present +	AccessExpiresAt     time.Time `validate:"-" bun:"type:timestamp,nullzero"`                                   // User level access token expires at -- null means the token never expires +	Refresh             string    `validate:"-" bun:",pk,nullzero,notnull,default:''"`                           // Refresh token, if present +	RefreshCreateAt     time.Time `validate:"required_with=Refresh" bun:"type:timestamp,nullzero"`               // Refresh created at, if refresh present +	RefreshExpiresAt    time.Time `validate:"-" bun:"type:timestamp,nullzero"`                                   // Refresh expires at -- null means the refresh token never expires +} diff --git a/internal/gtsmodel/user.go b/internal/gtsmodel/user.go index c36d75c8c..28156cfdd 100644 --- a/internal/gtsmodel/user.go +++ b/internal/gtsmodel/user.go @@ -26,97 +26,34 @@ import (  // User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account.  // To cross reference this local user with their account (which can be local or remote), use the AccountID field.  type User struct { -	/* -		BASIC INFO -	*/ - -	// id of this user in the local database; the end-user will never need to know this, it's strictly internal -	ID string `bun:"type:CHAR(26),pk,notnull,unique"` -	// confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported -	Email string `bun:"default:null,unique,nullzero"` -	// The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet) -	AccountID string   `bun:"type:CHAR(26),unique,nullzero"` -	Account   *Account `bun:"rel:belongs-to"` -	// The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables -	EncryptedPassword string `bun:",notnull"` - -	/* -		USER METADATA -	*/ - -	// When was this user created? -	CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// From what IP was this user created? -	SignUpIP net.IP `bun:",nullzero"` -	// When was this user updated (eg., password changed, email address changed)? -	UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` -	// When did this user sign in for their current session? -	CurrentSignInAt time.Time `bun:",nullzero"` -	// What's the most recent IP of this user -	CurrentSignInIP net.IP `bun:",nullzero"` -	// When did this user last sign in? -	LastSignInAt time.Time `bun:",nullzero"` -	// What's the previous IP of this user? -	LastSignInIP net.IP `bun:",nullzero"` -	// How many times has this user signed in? -	SignInCount int -	// id of the user who invited this user (who let this guy in?) -	InviteID string `bun:"type:CHAR(26),nullzero"` -	// What languages does this user want to see? -	ChosenLanguages []string -	// What languages does this user not want to see? -	FilteredLanguages []string -	// In what timezone/locale is this user located? -	Locale string `bun:",nullzero"` -	// Which application id created this user? See gtsmodel.Application -	CreatedByApplicationID string       `bun:"type:CHAR(26),nullzero"` -	CreatedByApplication   *Application `bun:"rel:belongs-to"` -	// When did we last contact this user -	LastEmailedAt time.Time `bun:",nullzero"` - -	/* -		USER CONFIRMATION -	*/ - -	// What confirmation token did we send this user/what are we expecting back? -	ConfirmationToken string `bun:",nullzero"` -	// When did the user confirm their email address -	ConfirmedAt time.Time `bun:",nullzero"` -	// When did we send email confirmation to this user? -	ConfirmationSentAt time.Time `bun:",nullzero"` -	// Email address that hasn't yet been confirmed -	UnconfirmedEmail string `bun:",nullzero"` - -	/* -		ACL FLAGS -	*/ - -	// Is this user a moderator? -	Moderator bool -	// Is this user an admin? -	Admin bool -	// Is this user disabled from posting? -	Disabled bool -	// Has this user been approved by a moderator? -	Approved bool - -	/* -		USER SECURITY -	*/ - -	// The generated token that the user can use to reset their password -	ResetPasswordToken string `bun:",nullzero"` -	// When did we email the user their reset-password email? -	ResetPasswordSentAt time.Time `bun:",nullzero"` - -	EncryptedOTPSecret     string `bun:",nullzero"` -	EncryptedOTPSecretIv   string `bun:",nullzero"` -	EncryptedOTPSecretSalt string `bun:",nullzero"` -	OTPRequiredForLogin    bool -	OTPBackupCodes         []string -	ConsumedTimestamp      int -	RememberToken          string    `bun:",nullzero"` -	SignInToken            string    `bun:",nullzero"` -	SignInTokenSentAt      time.Time `bun:",nullzero"` -	WebauthnID             string    `bun:",nullzero"` +	ID                     string       `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`      // id of this item in the database +	CreatedAt              time.Time    `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt              time.Time    `validate:"-" bun:"type:timestamp,nullzero,notnull,default:current_timestamp"` // when was item last updated +	Email                  string       `validate:"required_with=ConfirmedAt" bun:",nullzero,unique"`                  // confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported +	AccountID              string       `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull,unique"`         // The id of the local gtsmodel.Account entry for this user. +	Account                *Account     `validate:"-" bun:"rel:belongs-to"`                                            // Pointer to the account of this user that corresponds to AccountID. +	EncryptedPassword      string       `validate:"required" bun:",nullzero,notnull"`                                  // The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables. +	SignUpIP               net.IP       `validate:"-" bun:",nullzero"`                                                 // From what IP was this user created? +	CurrentSignInAt        time.Time    `validate:"-" bun:"type:timestamp,nullzero"`                                   // When did the user sign in with their current session. +	CurrentSignInIP        net.IP       `validate:"-" bun:",nullzero"`                                                 // What's the most recent IP of this user +	LastSignInAt           time.Time    `validate:"-" bun:"type:timestamp,nullzero"`                                   // When did this user last sign in? +	LastSignInIP           net.IP       `validate:"-" bun:",nullzero"`                                                 // What's the previous IP of this user? +	SignInCount            int          `validate:"min=0" bun:",nullzero,notnull,default:0"`                           // How many times has this user signed in? +	InviteID               string       `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                       // id of the user who invited this user (who let this joker in?) +	ChosenLanguages        []string     `validate:"-" bun:",nullzero"`                                                 // What languages does this user want to see? +	FilteredLanguages      []string     `validate:"-" bun:",nullzero"`                                                 // What languages does this user not want to see? +	Locale                 string       `validate:"-" bun:",nullzero"`                                                 // In what timezone/locale is this user located? +	CreatedByApplicationID string       `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                       // Which application id created this user? See gtsmodel.Application +	CreatedByApplication   *Application `validate:"-" bun:"rel:belongs-to"`                                            // Pointer to the application corresponding to createdbyapplicationID. +	LastEmailedAt          time.Time    `validate:"-" bun:"type:timestamp,nullzero"`                                   // When was this user last contacted by email. +	ConfirmationToken      string       `validate:"required_with=ConfirmationSentAt" bun:",nullzero"`                  // What confirmation token did we send this user/what are we expecting back? +	ConfirmationSentAt     time.Time    `validate:"required_with=ConfirmationToken" bun:"type:timestamp,nullzero"`     // When did we send email confirmation to this user? +	ConfirmedAt            time.Time    `validate:"required_with=Email" bun:"type:timestamp,nullzero"`                 // When did the user confirm their email address +	UnconfirmedEmail       string       `validate:"required_without=Email" bun:",nullzero"`                            // Email address that hasn't yet been confirmed +	Moderator              bool         `validate:"-" bun:",notnull,default:false"`                                    // Is this user a moderator? +	Admin                  bool         `validate:"-" bun:",notnull,default:false"`                                    // Is this user an admin? +	Disabled               bool         `validate:"-" bun:",notnull,default:false"`                                    // Is this user disabled from posting? +	Approved               bool         `validate:"-" bun:",notnull,default:false"`                                    // Has this user been approved by a moderator? +	ResetPasswordToken     string       `validate:"required_with=ResetPasswordSentAt" bun:",nullzero"`                 // The generated token that the user can use to reset their password +	ResetPasswordSentAt    time.Time    `validate:"required_with=ResetPasswordToken" bun:"type:timestamp,nullzero"`    // When did we email the user their reset-password email?  } diff --git a/internal/id/ulid.go b/internal/id/ulid.go index b488ddfc4..1b0c2e537 100644 --- a/internal/id/ulid.go +++ b/internal/id/ulid.go @@ -10,6 +10,9 @@ import (  const randomRange = 631152381 // ~20 years in seconds +// ULID represents a Universally Unique Lexicographically Sortable Identifier of 26 characters. See https://github.com/oklog/ulid +type ULID string +  // NewULID returns a new ULID string using the current time, or an error if something goes wrong.  func NewULID() (string, error) {  	newUlid, err := ulid.New(ulid.Timestamp(time.Now()), rand.Reader) diff --git a/internal/gtsmodel/messages.go b/internal/messages/messages.go index 62beb0adc..6cd2f466c 100644 --- a/internal/gtsmodel/messages.go +++ b/internal/messages/messages.go @@ -16,21 +16,23 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package gtsmodel +package messages -// FromClientAPI wraps a message that travels from client API into the processor +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// FromClientAPI wraps a message that travels from the client API into the processor.  type FromClientAPI struct {  	APObjectType   string  	APActivityType string  	GTSModel       interface{} -	OriginAccount  *Account -	TargetAccount  *Account +	OriginAccount  *gtsmodel.Account +	TargetAccount  *gtsmodel.Account  } -// FromFederator wraps a message that travels from the federator into the processor +// FromFederator wraps a message that travels from the federator into the processor.  type FromFederator struct {  	APObjectType     string  	APActivityType   string  	GTSModel         interface{} -	ReceivingAccount *Account +	ReceivingAccount *gtsmodel.Account  } diff --git a/internal/oauth/clientstore.go b/internal/oauth/clientstore.go index a642f6cfa..b8fb143e0 100644 --- a/internal/oauth/clientstore.go +++ b/internal/oauth/clientstore.go @@ -22,6 +22,7 @@ import (  	"context"  	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/oauth2/v4"  	"github.com/superseriousbusiness/oauth2/v4/models"  ) @@ -39,7 +40,7 @@ func NewClientStore(db db.Basic) oauth2.ClientStore {  }  func (cs *clientStore) GetByID(ctx context.Context, clientID string) (oauth2.ClientInfo, error) { -	poc := &Client{} +	poc := >smodel.Client{}  	if err := cs.db.GetByID(ctx, clientID, poc); err != nil {  		return nil, err  	} @@ -47,7 +48,7 @@ func (cs *clientStore) GetByID(ctx context.Context, clientID string) (oauth2.Cli  }  func (cs *clientStore) Set(ctx context.Context, id string, cli oauth2.ClientInfo) error { -	poc := &Client{ +	poc := >smodel.Client{  		ID:     cli.GetID(),  		Secret: cli.GetSecret(),  		Domain: cli.GetDomain(), @@ -57,16 +58,8 @@ func (cs *clientStore) Set(ctx context.Context, id string, cli oauth2.ClientInfo  }  func (cs *clientStore) Delete(ctx context.Context, id string) error { -	poc := &Client{ +	poc := >smodel.Client{  		ID: id,  	}  	return cs.db.DeleteByID(ctx, id, poc)  } - -// Client is a handy little wrapper for typical oauth client details -type Client struct { -	ID     string `bun:"type:CHAR(26),pk,notnull"` -	Secret string -	Domain string -	UserID string -} diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index fd3452405..b0a36487b 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -42,9 +42,9 @@ const ()  // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout  func (suite *PgClientStoreTestSuite) SetupSuite() {  	suite.testClientID = "01FCVB74EW6YBYAEY7QG9CQQF6" -	suite.testClientSecret = "test-client-secret" +	suite.testClientSecret = "4cc87402-259b-4a35-9485-2c8bf54f3763"  	suite.testClientDomain = "https://example.org" -	suite.testClientUserID = "test-client-user-id" +	suite.testClientUserID = "01FEGYXKVCDB731QF9MVFXA4F5"  }  // SetupTest creates a postgres connection and creates the oauth_clients table before each test diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go index 264678ff5..94578dbaa 100644 --- a/internal/oauth/tokenstore.go +++ b/internal/oauth/tokenstore.go @@ -26,6 +26,7 @@ import (  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id"  	"github.com/superseriousbusiness/oauth2/v4"  	"github.com/superseriousbusiness/oauth2/v4/models" @@ -71,7 +72,7 @@ func newTokenStore(ctx context.Context, db db.Basic, log *logrus.Logger) oauth2.  func (ts *tokenStore) sweep(ctx context.Context) error {  	// select *all* tokens from the db  	// todo: if this becomes expensive (ie., there are fucking LOADS of tokens) then figure out a better way. -	tokens := new([]*Token) +	tokens := new([]*gtsmodel.Token)  	if err := ts.db.GetAll(ctx, tokens); err != nil {  		return err  	} @@ -117,17 +118,17 @@ func (ts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error {  // RemoveByCode deletes a token from the DB based on the Code field  func (ts *tokenStore) RemoveByCode(ctx context.Context, code string) error { -	return ts.db.DeleteWhere(ctx, []db.Where{{Key: "code", Value: code}}, &Token{}) +	return ts.db.DeleteWhere(ctx, []db.Where{{Key: "code", Value: code}}, >smodel.Token{})  }  // RemoveByAccess deletes a token from the DB based on the Access field  func (ts *tokenStore) RemoveByAccess(ctx context.Context, access string) error { -	return ts.db.DeleteWhere(ctx, []db.Where{{Key: "access", Value: access}}, &Token{}) +	return ts.db.DeleteWhere(ctx, []db.Where{{Key: "access", Value: access}}, >smodel.Token{})  }  // RemoveByRefresh deletes a token from the DB based on the Refresh field  func (ts *tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { -	return ts.db.DeleteWhere(ctx, []db.Where{{Key: "refresh", Value: refresh}}, &Token{}) +	return ts.db.DeleteWhere(ctx, []db.Where{{Key: "refresh", Value: refresh}}, >smodel.Token{})  }  // GetByCode selects a token from the DB based on the Code field @@ -135,7 +136,7 @@ func (ts *tokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenI  	if code == "" {  		return nil, nil  	} -	dbt := &Token{ +	dbt := >smodel.Token{  		Code: code,  	}  	if err := ts.db.GetWhere(ctx, []db.Where{{Key: "code", Value: code}}, dbt); err != nil { @@ -149,7 +150,7 @@ func (ts *tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.To  	if access == "" {  		return nil, nil  	} -	dbt := &Token{ +	dbt := >smodel.Token{  		Access: access,  	}  	if err := ts.db.GetWhere(ctx, []db.Where{{Key: "access", Value: access}}, dbt); err != nil { @@ -163,7 +164,7 @@ func (ts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.  	if refresh == "" {  		return nil, nil  	} -	dbt := &Token{ +	dbt := >smodel.Token{  		Refresh: refresh,  	}  	if err := ts.db.GetWhere(ctx, []db.Where{{Key: "refresh", Value: refresh}}, dbt); err != nil { @@ -176,37 +177,8 @@ func (ts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.  	The following models are basically helpers for the token store implementation, they should only be used internally.  */ -// Token is a translation of the gotosocial token with the ExpiresIn fields replaced with ExpiresAt. -// -// Explanation for this: gotosocial assumes an in-memory or file database of some kind, where a time-to-live parameter (TTL) can be defined, -// and tokens with expired TTLs are automatically removed. Since some databases don't have that feature, it's easier to set an expiry time and -// then periodically sweep out tokens when that time has passed. -// -// Note that this struct does *not* satisfy the token interface shown here: https://github.com/superseriousbusiness/oauth2/blob/master/model.go#L22 -// and implemented here: https://github.com/superseriousbusiness/oauth2/blob/master/models/token.go. -// As such, manual translation is always required between Token and the gotosocial *model.Token. The helper functions oauthTokenToPGToken -// and pgTokenToOauthToken can be used for that. -type Token struct { -	ID                  string `bun:"type:CHAR(26),pk,notnull"` -	ClientID            string -	UserID              string -	RedirectURI         string -	Scope               string -	Code                string `bun:"default:'',pk"` -	CodeChallenge       string -	CodeChallengeMethod string -	CodeCreateAt        time.Time `bun:",nullzero"` -	CodeExpiresAt       time.Time `bun:",nullzero"` -	Access              string    `bun:"default:'',pk"` -	AccessCreateAt      time.Time `bun:",nullzero"` -	AccessExpiresAt     time.Time `bun:",nullzero"` -	Refresh             string    `bun:"default:'',pk"` -	RefreshCreateAt     time.Time `bun:",nullzero"` -	RefreshExpiresAt    time.Time `bun:",nullzero"` -} -  // TokenToDBToken is a lil util function that takes a gotosocial token and gives back a token for inserting into a database. -func TokenToDBToken(tkn *models.Token) *Token { +func TokenToDBToken(tkn *models.Token) *gtsmodel.Token {  	now := time.Now()  	// For the following, we want to make sure we're not adding a time.Now() to an *empty* ExpiresIn, otherwise that's @@ -228,7 +200,7 @@ func TokenToDBToken(tkn *models.Token) *Token {  		rea = now.Add(tkn.RefreshExpiresIn)  	} -	return &Token{ +	return >smodel.Token{  		ClientID:            tkn.ClientID,  		UserID:              tkn.UserID,  		RedirectURI:         tkn.RedirectURI, @@ -248,7 +220,7 @@ func TokenToDBToken(tkn *models.Token) *Token {  }  // DBTokenToToken is a lil util function that takes a database token and gives back a gotosocial token -func DBTokenToToken(dbt *Token) *models.Token { +func DBTokenToToken(dbt *gtsmodel.Token) *models.Token {  	now := time.Now()  	var codeExpiresIn time.Duration diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index 81701fd7c..71b876d3b 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -30,6 +30,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/media" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/oauth"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  	"github.com/superseriousbusiness/gotosocial/internal/visibility" @@ -79,7 +80,7 @@ type processor struct {  	tc            typeutils.TypeConverter  	config        *config.Config  	mediaHandler  media.Handler -	fromClientAPI chan gtsmodel.FromClientAPI +	fromClientAPI chan messages.FromClientAPI  	oauthServer   oauth.Server  	filter        visibility.Filter  	db            db.DB @@ -88,7 +89,7 @@ type processor struct {  }  // New returns a new account processor. -func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, oauthServer oauth.Server, fromClientAPI chan gtsmodel.FromClientAPI, federator federation.Federator, config *config.Config, log *logrus.Logger) Processor { +func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, oauthServer oauth.Server, fromClientAPI chan messages.FromClientAPI, federator federation.Federator, config *config.Config, log *logrus.Logger) Processor {  	return &processor{  		tc:            tc,  		config:        config, diff --git a/internal/processing/account/createblock.go b/internal/processing/account/createblock.go index 06f82b37d..347f19bee 100644 --- a/internal/processing/account/createblock.go +++ b/internal/processing/account/createblock.go @@ -22,11 +22,13 @@ import (  	"context"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -111,9 +113,9 @@ func (p *processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel  	// follow request status changed so send the UNDO activity to the channel for async processing  	if frChanged { -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsFollow, -			APActivityType: gtsmodel.ActivityStreamsUndo, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityFollow, +			APActivityType: ap.ActivityUndo,  			GTSModel: >smodel.Follow{  				AccountID:       requestingAccount.ID,  				TargetAccountID: targetAccountID, @@ -126,9 +128,9 @@ func (p *processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel  	// follow status changed so send the UNDO activity to the channel for async processing  	if fChanged { -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsFollow, -			APActivityType: gtsmodel.ActivityStreamsUndo, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityFollow, +			APActivityType: ap.ActivityUndo,  			GTSModel: >smodel.Follow{  				AccountID:       requestingAccount.ID,  				TargetAccountID: targetAccountID, @@ -140,9 +142,9 @@ func (p *processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel  	}  	// handle the rest of the block process asynchronously -	p.fromClientAPI <- gtsmodel.FromClientAPI{ -		APObjectType:   gtsmodel.ActivityStreamsBlock, -		APActivityType: gtsmodel.ActivityStreamsCreate, +	p.fromClientAPI <- messages.FromClientAPI{ +		APObjectType:   ap.ActivityBlock, +		APActivityType: ap.ActivityCreate,  		GTSModel:       block,  		OriginAccount:  requestingAccount,  		TargetAccount:  targetAccount, diff --git a/internal/processing/account/createfollow.go b/internal/processing/account/createfollow.go index a7767afea..d3ca386ed 100644 --- a/internal/processing/account/createfollow.go +++ b/internal/processing/account/createfollow.go @@ -22,11 +22,13 @@ import (  	"context"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -99,9 +101,9 @@ func (p *processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode  	}  	// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously -	p.fromClientAPI <- gtsmodel.FromClientAPI{ -		APObjectType:   gtsmodel.ActivityStreamsFollow, -		APActivityType: gtsmodel.ActivityStreamsCreate, +	p.fromClientAPI <- messages.FromClientAPI{ +		APObjectType:   ap.ActivityFollow, +		APActivityType: ap.ActivityCreate,  		GTSModel:       fr,  		OriginAccount:  requestingAccount,  		TargetAccount:  targetAcct, diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index d97af4d2e..5de706045 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -23,9 +23,10 @@ import (  	"time"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  )  // Delete handles the complete deletion of an account. @@ -64,12 +65,12 @@ func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origi  		u := >smodel.User{}  		if err := p.db.GetWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, u); err == nil {  			// we got one! select all tokens with the user's ID -			tokens := []*oauth.Token{} +			tokens := []*gtsmodel.Token{}  			if err := p.db.GetWhere(ctx, []db.Where{{Key: "user_id", Value: u.ID}}, &tokens); err == nil {  				// we have some tokens to delete  				for _, t := range tokens {  					// delete client(s) associated with this token -					if err := p.db.DeleteByID(ctx, t.ClientID, &oauth.Client{}); err != nil { +					if err := p.db.DeleteByID(ctx, t.ClientID, >smodel.Client{}); err != nil {  						l.Errorf("error deleting oauth client: %s", err)  					}  					// delete application(s) associated with this token @@ -150,9 +151,9 @@ selectStatusesLoop:  			// pass the status delete through the client api channel for processing  			s.Account = account  			l.Debug("putting status in the client api channel") -			p.fromClientAPI <- gtsmodel.FromClientAPI{ -				APObjectType:   gtsmodel.ActivityStreamsNote, -				APActivityType: gtsmodel.ActivityStreamsDelete, +			p.fromClientAPI <- messages.FromClientAPI{ +				APObjectType:   ap.ObjectNote, +				APActivityType: ap.ActivityDelete,  				GTSModel:       s,  				OriginAccount:  account,  				TargetAccount:  account, @@ -186,9 +187,9 @@ selectStatusesLoop:  				}  				l.Debug("putting boost undo in the client api channel") -				p.fromClientAPI <- gtsmodel.FromClientAPI{ -					APObjectType:   gtsmodel.ActivityStreamsAnnounce, -					APActivityType: gtsmodel.ActivityStreamsUndo, +				p.fromClientAPI <- messages.FromClientAPI{ +					APObjectType:   ap.ActivityAnnounce, +					APActivityType: ap.ActivityUndo,  					GTSModel:       s,  					OriginAccount:  b.Account,  					TargetAccount:  account, diff --git a/internal/processing/account/removeblock.go b/internal/processing/account/removeblock.go index 7e3d78076..06bafb3a4 100644 --- a/internal/processing/account/removeblock.go +++ b/internal/processing/account/removeblock.go @@ -22,10 +22,12 @@ import (  	"context"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  )  func (p *processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { @@ -52,9 +54,9 @@ func (p *processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel  	// block status changed so send the UNDO activity to the channel for async processing  	if blockChanged { -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsBlock, -			APActivityType: gtsmodel.ActivityStreamsUndo, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityBlock, +			APActivityType: ap.ActivityUndo,  			GTSModel:       block,  			OriginAccount:  requestingAccount,  			TargetAccount:  targetAccount, diff --git a/internal/processing/account/removefollow.go b/internal/processing/account/removefollow.go index 6186c550f..9791f2e54 100644 --- a/internal/processing/account/removefollow.go +++ b/internal/processing/account/removefollow.go @@ -22,10 +22,12 @@ import (  	"context"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  )  func (p *processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { @@ -78,9 +80,9 @@ func (p *processor) FollowRemove(ctx context.Context, requestingAccount *gtsmode  	// follow request status changed so send the UNDO activity to the channel for async processing  	if frChanged { -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsFollow, -			APActivityType: gtsmodel.ActivityStreamsUndo, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityFollow, +			APActivityType: ap.ActivityUndo,  			GTSModel: >smodel.Follow{  				AccountID:       requestingAccount.ID,  				TargetAccountID: targetAccountID, @@ -93,9 +95,9 @@ func (p *processor) FollowRemove(ctx context.Context, requestingAccount *gtsmode  	// follow status changed so send the UNDO activity to the channel for async processing  	if fChanged { -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsFollow, -			APActivityType: gtsmodel.ActivityStreamsUndo, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityFollow, +			APActivityType: ap.ActivityUndo,  			GTSModel: >smodel.Follow{  				AccountID:       requestingAccount.ID,  				TargetAccountID: targetAccountID, diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 99ccbf5a0..c0fee8e25 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -26,11 +26,13 @@ import (  	"io"  	"mime/multipart" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/media" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/text" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) { @@ -49,7 +51,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form  	}  	if form.DisplayName != nil { -		if err := util.ValidateDisplayName(*form.DisplayName); err != nil { +		if err := validate.DisplayName(*form.DisplayName); err != nil {  			return nil, err  		}  		displayName := text.RemoveHTML(*form.DisplayName) // no html allowed in display name @@ -59,7 +61,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form  	}  	if form.Note != nil { -		if err := util.ValidateNote(*form.Note); err != nil { +		if err := validate.Note(*form.Note); err != nil {  			return nil, err  		}  		note := text.SanitizeHTML(*form.Note) // html OK in note but sanitize it @@ -92,7 +94,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form  	if form.Source != nil {  		if form.Source.Language != nil { -			if err := util.ValidateLanguage(*form.Source.Language); err != nil { +			if err := validate.Language(*form.Source.Language); err != nil {  				return nil, err  			}  			if err := p.db.UpdateOneByID(ctx, account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil { @@ -107,7 +109,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form  		}  		if form.Source.Privacy != nil { -			if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil { +			if err := validate.Privacy(*form.Source.Privacy); err != nil {  				return nil, err  			}  			if err := p.db.UpdateOneByID(ctx, account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil { @@ -122,9 +124,9 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form  		return nil, fmt.Errorf("could not fetch updated account %s: %s", account.ID, err)  	} -	p.fromClientAPI <- gtsmodel.FromClientAPI{ -		APObjectType:   gtsmodel.ActivityStreamsProfile, -		APActivityType: gtsmodel.ActivityStreamsUpdate, +	p.fromClientAPI <- messages.FromClientAPI{ +		APObjectType:   ap.ObjectProfile, +		APActivityType: ap.ActivityUpdate,  		GTSModel:       updatedAccount,  		OriginAccount:  updatedAccount,  	} diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go index de288811b..92f69f06b 100644 --- a/internal/processing/admin/admin.go +++ b/internal/processing/admin/admin.go @@ -29,6 +29,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/media" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  ) @@ -46,13 +47,13 @@ type processor struct {  	tc            typeutils.TypeConverter  	config        *config.Config  	mediaHandler  media.Handler -	fromClientAPI chan gtsmodel.FromClientAPI +	fromClientAPI chan messages.FromClientAPI  	db            db.DB  	log           *logrus.Logger  }  // New returns a new admin processor. -func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, fromClientAPI chan gtsmodel.FromClientAPI, config *config.Config, log *logrus.Logger) Processor { +func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, fromClientAPI chan messages.FromClientAPI, config *config.Config, log *logrus.Logger) Processor {  	return &processor{  		tc:            tc,  		config:        config, diff --git a/internal/processing/admin/createdomainblock.go b/internal/processing/admin/createdomainblock.go index a34c03a44..9c4ff780f 100644 --- a/internal/processing/admin/createdomainblock.go +++ b/internal/processing/admin/createdomainblock.go @@ -24,11 +24,13 @@ import (  	"time"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/text"  ) @@ -140,9 +142,9 @@ selectAccountsLoop:  			l.Debugf("putting delete for account %s in the clientAPI channel", a.Username)  			// pass the account delete through the client api channel for processing -			p.fromClientAPI <- gtsmodel.FromClientAPI{ -				APObjectType:   gtsmodel.ActivityStreamsPerson, -				APActivityType: gtsmodel.ActivityStreamsDelete, +			p.fromClientAPI <- messages.FromClientAPI{ +				APObjectType:   ap.ActorPerson, +				APActivityType: ap.ActivityDelete,  				GTSModel:       block,  				OriginAccount:  account,  				TargetAccount:  a, diff --git a/internal/processing/app.go b/internal/processing/app.go index 4f805572b..d6ded6efa 100644 --- a/internal/processing/app.go +++ b/internal/processing/app.go @@ -43,7 +43,6 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api  		return nil, err  	}  	clientSecret := uuid.NewString() -	vapidKey := uuid.NewString()  	appID, err := id.NewRandomULID()  	if err != nil { @@ -59,7 +58,6 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api  		ClientID:     clientID,  		ClientSecret: clientSecret,  		Scopes:       scopes, -		VapidKey:     vapidKey,  	}  	// chuck it in the db @@ -68,7 +66,7 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api  	}  	// now we need to model an oauth client from the application that the oauth library can use -	oc := &oauth.Client{ +	oc := >smodel.Client{  		ID:     clientID,  		Secret: clientSecret,  		Domain: form.RedirectURIs, diff --git a/internal/processing/followrequest.go b/internal/processing/followrequest.go index 3dd6432e2..b313e42f8 100644 --- a/internal/processing/followrequest.go +++ b/internal/processing/followrequest.go @@ -21,10 +21,11 @@ package processing  import (  	"context" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/oauth"  ) @@ -77,9 +78,9 @@ func (p *processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a  		follow.TargetAccount = followTargetAccount  	} -	p.fromClientAPI <- gtsmodel.FromClientAPI{ -		APObjectType:   gtsmodel.ActivityStreamsFollow, -		APActivityType: gtsmodel.ActivityStreamsAccept, +	p.fromClientAPI <- messages.FromClientAPI{ +		APObjectType:   ap.ActivityFollow, +		APActivityType: ap.ActivityAccept,  		GTSModel:       follow,  		OriginAccount:  follow.Account,  		TargetAccount:  follow.TargetAccount, diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index a6ea0068b..97c6cc8b2 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -25,16 +25,18 @@ import (  	"net/url"  	"github.com/go-fed/activity/streams" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  ) -func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel.FromClientAPI) error { +func (p *processor) processFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {  	switch clientMsg.APActivityType { -	case gtsmodel.ActivityStreamsCreate: +	case ap.ActivityCreate:  		// CREATE  		switch clientMsg.APObjectType { -		case gtsmodel.ActivityStreamsNote: +		case ap.ObjectNote:  			// CREATE NOTE  			status, ok := clientMsg.GTSModel.(*gtsmodel.Status)  			if !ok { @@ -49,10 +51,10 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  				return err  			} -			if status.VisibilityAdvanced != nil && status.VisibilityAdvanced.Federated { +			if status.VisibilityAdvanced.Federated {  				return p.federateStatus(ctx, status)  			} -		case gtsmodel.ActivityStreamsFollow: +		case ap.ActivityFollow:  			// CREATE FOLLOW REQUEST  			followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)  			if !ok { @@ -64,7 +66,7 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			}  			return p.federateFollow(ctx, followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount) -		case gtsmodel.ActivityStreamsLike: +		case ap.ActivityLike:  			// CREATE LIKE/FAVE  			fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)  			if !ok { @@ -76,7 +78,7 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			}  			return p.federateFave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) -		case gtsmodel.ActivityStreamsAnnounce: +		case ap.ActivityAnnounce:  			// CREATE BOOST/ANNOUNCE  			boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)  			if !ok { @@ -92,7 +94,7 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			}  			return p.federateAnnounce(ctx, boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount) -		case gtsmodel.ActivityStreamsBlock: +		case ap.ActivityBlock:  			// CREATE BLOCK  			block, ok := clientMsg.GTSModel.(*gtsmodel.Block)  			if !ok { @@ -112,10 +114,10 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			return p.federateBlock(ctx, block)  		} -	case gtsmodel.ActivityStreamsUpdate: +	case ap.ActivityUpdate:  		// UPDATE  		switch clientMsg.APObjectType { -		case gtsmodel.ActivityStreamsProfile, gtsmodel.ActivityStreamsPerson: +		case ap.ObjectProfile, ap.ActorPerson:  			// UPDATE ACCOUNT/PROFILE  			account, ok := clientMsg.GTSModel.(*gtsmodel.Account)  			if !ok { @@ -124,10 +126,10 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount)  		} -	case gtsmodel.ActivityStreamsAccept: +	case ap.ActivityAccept:  		// ACCEPT  		switch clientMsg.APObjectType { -		case gtsmodel.ActivityStreamsFollow: +		case ap.ActivityFollow:  			// ACCEPT FOLLOW  			follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)  			if !ok { @@ -140,31 +142,31 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			return p.federateAcceptFollowRequest(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount)  		} -	case gtsmodel.ActivityStreamsUndo: +	case ap.ActivityUndo:  		// UNDO  		switch clientMsg.APObjectType { -		case gtsmodel.ActivityStreamsFollow: +		case ap.ActivityFollow:  			// UNDO FOLLOW  			follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)  			if !ok {  				return errors.New("undo was not parseable as *gtsmodel.Follow")  			}  			return p.federateUnfollow(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount) -		case gtsmodel.ActivityStreamsBlock: +		case ap.ActivityBlock:  			// UNDO BLOCK  			block, ok := clientMsg.GTSModel.(*gtsmodel.Block)  			if !ok {  				return errors.New("undo was not parseable as *gtsmodel.Block")  			}  			return p.federateUnblock(ctx, block) -		case gtsmodel.ActivityStreamsLike: +		case ap.ActivityLike:  			// UNDO LIKE/FAVE  			fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)  			if !ok {  				return errors.New("undo was not parseable as *gtsmodel.StatusFave")  			}  			return p.federateUnfave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) -		case gtsmodel.ActivityStreamsAnnounce: +		case ap.ActivityAnnounce:  			// UNDO ANNOUNCE/BOOST  			boost, ok := clientMsg.GTSModel.(*gtsmodel.Status)  			if !ok { @@ -177,10 +179,10 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			return p.federateUnannounce(ctx, boost, clientMsg.OriginAccount, clientMsg.TargetAccount)  		} -	case gtsmodel.ActivityStreamsDelete: +	case ap.ActivityDelete:  		// DELETE  		switch clientMsg.APObjectType { -		case gtsmodel.ActivityStreamsNote: +		case ap.ObjectNote:  			// DELETE STATUS/NOTE  			statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status)  			if !ok { @@ -216,7 +218,7 @@ func (p *processor) processFromClientAPI(ctx context.Context, clientMsg gtsmodel  			}  			return p.federateStatusDelete(ctx, statusToDelete) -		case gtsmodel.ActivityStreamsProfile, gtsmodel.ActivityStreamsPerson: +		case ap.ObjectProfile, ap.ActorPerson:  			// DELETE ACCOUNT/PROFILE  			// the origin of the delete could be either a domain block, or an action by another (or this) account diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index cb0999cf9..d2e949cef 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -25,12 +25,14 @@ import (  	"net/url"  	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  ) -func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmodel.FromFederator) error { +func (p *processor) processFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {  	l := p.log.WithFields(logrus.Fields{  		"func":         "processFromFederator",  		"federatorMsg": fmt.Sprintf("%+v", federatorMsg), @@ -39,10 +41,10 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  	l.Trace("entering function PROCESS FROM FEDERATOR")  	switch federatorMsg.APActivityType { -	case gtsmodel.ActivityStreamsCreate: +	case ap.ActivityCreate:  		// CREATE  		switch federatorMsg.APObjectType { -		case gtsmodel.ActivityStreamsNote: +		case ap.ObjectNote:  			// CREATE A STATUS  			incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status)  			if !ok { @@ -61,10 +63,10 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  			if err := p.notifyStatus(ctx, status); err != nil {  				return err  			} -		case gtsmodel.ActivityStreamsProfile: +		case ap.ObjectProfile:  			// CREATE AN ACCOUNT  			// nothing to do here -		case gtsmodel.ActivityStreamsLike: +		case ap.ActivityLike:  			// CREATE A FAVE  			incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave)  			if !ok { @@ -74,7 +76,7 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  			if err := p.notifyFave(ctx, incomingFave, federatorMsg.ReceivingAccount); err != nil {  				return err  			} -		case gtsmodel.ActivityStreamsFollow: +		case ap.ActivityFollow:  			// CREATE A FOLLOW REQUEST  			incomingFollowRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest)  			if !ok { @@ -84,7 +86,7 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  			if err := p.notifyFollowRequest(ctx, incomingFollowRequest, federatorMsg.ReceivingAccount); err != nil {  				return err  			} -		case gtsmodel.ActivityStreamsAnnounce: +		case ap.ActivityAnnounce:  			// CREATE AN ANNOUNCE  			incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status)  			if !ok { @@ -114,7 +116,7 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  			if err := p.notifyAnnounce(ctx, incomingAnnounce); err != nil {  				return err  			} -		case gtsmodel.ActivityStreamsBlock: +		case ap.ActivityBlock:  			// CREATE A BLOCK  			block, ok := federatorMsg.GTSModel.(*gtsmodel.Block)  			if !ok { @@ -131,10 +133,10 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  			// TODO: same with notifications  			// TODO: same with bookmarks  		} -	case gtsmodel.ActivityStreamsUpdate: +	case ap.ActivityUpdate:  		// UPDATE  		switch federatorMsg.APObjectType { -		case gtsmodel.ActivityStreamsProfile: +		case ap.ObjectProfile:  			// UPDATE AN ACCOUNT  			incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)  			if !ok { @@ -150,10 +152,10 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  				return fmt.Errorf("error dereferencing account from federator: %s", err)  			}  		} -	case gtsmodel.ActivityStreamsDelete: +	case ap.ActivityDelete:  		// DELETE  		switch federatorMsg.APObjectType { -		case gtsmodel.ActivityStreamsNote: +		case ap.ObjectNote:  			// DELETE A STATUS  			// TODO: handle side effects of status deletion here:  			// 1. delete all media associated with status @@ -185,14 +187,14 @@ func (p *processor) processFromFederator(ctx context.Context, federatorMsg gtsmo  			// remove this status from any and all timelines  			return p.deleteStatusFromTimelines(ctx, statusToDelete) -		case gtsmodel.ActivityStreamsProfile: +		case ap.ObjectProfile:  			// DELETE A PROFILE/ACCOUNT  			// TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account  		} -	case gtsmodel.ActivityStreamsAccept: +	case ap.ActivityAccept:  		// ACCEPT  		switch federatorMsg.APObjectType { -		case gtsmodel.ActivityStreamsFollow: +		case ap.ActivityFollow:  			// ACCEPT A FOLLOW  			follow, ok := federatorMsg.GTSModel.(*gtsmodel.Follow)  			if !ok { diff --git a/internal/processing/instance.go b/internal/processing/instance.go index ced798c2e..e74d3077a 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -27,7 +27,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/text" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  func (p *processor) InstanceGet(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) { @@ -59,7 +59,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe  	// validate & update site title if it's set on the form  	if form.Title != nil { -		if err := util.ValidateSiteTitle(*form.Title); err != nil { +		if err := validate.SiteTitle(*form.Title); err != nil {  			return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("site title invalid: %s", err))  		}  		i.Title = text.RemoveHTML(*form.Title) // don't allow html in site title @@ -101,7 +101,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe  	// validate & update site contact email if it's set on the form  	if form.ContactEmail != nil { -		if err := util.ValidateEmail(*form.ContactEmail); err != nil { +		if err := validate.Email(*form.ContactEmail); err != nil {  			return nil, gtserror.NewErrorBadRequest(err, err.Error())  		}  		i.ContactEmail = *form.ContactEmail @@ -109,7 +109,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe  	// validate & update site short description if it's set on the form  	if form.ShortDescription != nil { -		if err := util.ValidateSiteShortDescription(*form.ShortDescription); err != nil { +		if err := validate.SiteShortDescription(*form.ShortDescription); err != nil {  			return nil, gtserror.NewErrorBadRequest(err, err.Error())  		}  		i.ShortDescription = text.SanitizeHTML(*form.ShortDescription) // html is OK in site description, but we should sanitize it @@ -117,7 +117,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe  	// validate & update site description if it's set on the form  	if form.Description != nil { -		if err := util.ValidateSiteDescription(*form.Description); err != nil { +		if err := validate.SiteDescription(*form.Description); err != nil {  			return nil, gtserror.NewErrorBadRequest(err, err.Error())  		}  		i.Description = text.SanitizeHTML(*form.Description) // html is OK in site description, but we should sanitize it @@ -125,7 +125,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe  	// validate & update site terms if it's set on the form  	if form.Terms != nil { -		if err := util.ValidateSiteTerms(*form.Terms); err != nil { +		if err := validate.SiteTerms(*form.Terms); err != nil {  			return nil, gtserror.NewErrorBadRequest(err, err.Error())  		}  		i.Terms = text.SanitizeHTML(*form.Terms) // html is OK in site terms, but we should sanitize it diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 8df464ce0..38076123f 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -32,12 +32,14 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/media" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/oauth"  	"github.com/superseriousbusiness/gotosocial/internal/processing/account"  	"github.com/superseriousbusiness/gotosocial/internal/processing/admin"  	mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media"  	"github.com/superseriousbusiness/gotosocial/internal/processing/status"  	"github.com/superseriousbusiness/gotosocial/internal/processing/streaming" +	"github.com/superseriousbusiness/gotosocial/internal/stream"  	"github.com/superseriousbusiness/gotosocial/internal/timeline"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  	"github.com/superseriousbusiness/gotosocial/internal/visibility" @@ -165,7 +167,7 @@ type Processor interface {  	// AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid.  	AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error)  	// OpenStreamForAccount opens a new stream for the given account, with the given stream type. -	OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode) +	OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode)  	/*  		FEDERATION API-FACING PROCESSING FUNCTIONS @@ -219,8 +221,8 @@ type Processor interface {  // processor just implements the Processor interface  type processor struct { -	fromClientAPI   chan gtsmodel.FromClientAPI -	fromFederator   chan gtsmodel.FromFederator +	fromClientAPI   chan messages.FromClientAPI +	fromFederator   chan messages.FromFederator  	federator       federation.Federator  	stop            chan interface{}  	log             *logrus.Logger @@ -247,8 +249,8 @@ type processor struct {  // NewProcessor returns a new Processor that uses the given federator and logger  func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage blob.Storage, timelineManager timeline.Manager, db db.DB, log *logrus.Logger) Processor { -	fromClientAPI := make(chan gtsmodel.FromClientAPI, 1000) -	fromFederator := make(chan gtsmodel.FromFederator, 1000) +	fromClientAPI := make(chan messages.FromClientAPI, 1000) +	fromFederator := make(chan messages.FromFederator, 1000)  	statusProcessor := status.New(db, tc, config, fromClientAPI, log)  	streamingProcessor := streaming.New(db, tc, oauthServer, config, log) diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go index 948d57a48..d6c4ada41 100644 --- a/internal/processing/status/boost.go +++ b/internal/processing/status/boost.go @@ -23,9 +23,11 @@ import (  	"errors"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  )  func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { @@ -44,10 +46,8 @@ func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Accou  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} -	if targetStatus.VisibilityAdvanced != nil { -		if !targetStatus.VisibilityAdvanced.Boostable { -			return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable")) -		} +	if !targetStatus.VisibilityAdvanced.Boostable { +		return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable"))  	}  	// it's visible! it's boostable! so let's boost the FUCK out of it @@ -65,9 +65,9 @@ func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Accou  	}  	// send it back to the processor for async processing -	p.fromClientAPI <- gtsmodel.FromClientAPI{ -		APObjectType:   gtsmodel.ActivityStreamsAnnounce, -		APActivityType: gtsmodel.ActivityStreamsCreate, +	p.fromClientAPI <- messages.FromClientAPI{ +		APObjectType:   ap.ActivityAnnounce, +		APActivityType: ap.ActivityCreate,  		GTSModel:       boostWrapperStatus,  		OriginAccount:  requestingAccount,  		TargetAccount:  targetStatus.Account, diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index 2e0b30ad8..a87dbc7fe 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -23,10 +23,12 @@ import (  	"fmt"  	"time" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/text"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -50,7 +52,7 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli  		AccountID:                account.ID,  		AccountURI:               account.URI,  		ContentWarning:           text.RemoveHTML(form.SpoilerText), -		ActivityStreamsType:      gtsmodel.ActivityStreamsNote, +		ActivityStreamsType:      ap.ObjectNote,  		Sensitive:                form.Sensitive,  		Language:                 form.Language,  		CreatedWithApplicationID: application.ID, @@ -95,9 +97,9 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli  	}  	// send it back to the processor for async processing -	p.fromClientAPI <- gtsmodel.FromClientAPI{ -		APObjectType:   gtsmodel.ActivityStreamsNote, -		APActivityType: gtsmodel.ActivityStreamsCreate, +	p.fromClientAPI <- messages.FromClientAPI{ +		APObjectType:   ap.ObjectNote, +		APActivityType: ap.ActivityCreate,  		GTSModel:       newStatus,  		OriginAccount:  account,  	} diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go index daa7a934f..dfb2c3626 100644 --- a/internal/processing/status/delete.go +++ b/internal/processing/status/delete.go @@ -23,9 +23,11 @@ import (  	"errors"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  )  func (p *processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { @@ -51,9 +53,9 @@ func (p *processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco  	}  	// send it back to the processor for async processing -	p.fromClientAPI <- gtsmodel.FromClientAPI{ -		APObjectType:   gtsmodel.ActivityStreamsNote, -		APActivityType: gtsmodel.ActivityStreamsDelete, +	p.fromClientAPI <- messages.FromClientAPI{ +		APObjectType:   ap.ObjectNote, +		APActivityType: ap.ActivityDelete,  		GTSModel:       targetStatus,  		OriginAccount:  requestingAccount,  		TargetAccount:  requestingAccount, diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go index 2badf83b3..195bfa56a 100644 --- a/internal/processing/status/fave.go +++ b/internal/processing/status/fave.go @@ -23,11 +23,13 @@ import (  	"errors"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -47,10 +49,8 @@ func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Accoun  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} -	if targetStatus.VisibilityAdvanced != nil { -		if !targetStatus.VisibilityAdvanced.Likeable { -			return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable")) -		} +	if !targetStatus.VisibilityAdvanced.Likeable { +		return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable"))  	}  	// first check if the status is already faved, if so we don't need to do anything @@ -84,9 +84,9 @@ func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Accoun  		}  		// send it back to the processor for async processing -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsLike, -			APActivityType: gtsmodel.ActivityStreamsCreate, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityLike, +			APActivityType: ap.ActivityCreate,  			GTSModel:       gtsFave,  			OriginAccount:  requestingAccount,  			TargetAccount:  targetStatus.Account, diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go index 37790d062..10faa5696 100644 --- a/internal/processing/status/status.go +++ b/internal/processing/status/status.go @@ -27,6 +27,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/text"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  	"github.com/superseriousbusiness/gotosocial/internal/visibility" @@ -75,12 +76,12 @@ type processor struct {  	db            db.DB  	filter        visibility.Filter  	formatter     text.Formatter -	fromClientAPI chan gtsmodel.FromClientAPI +	fromClientAPI chan messages.FromClientAPI  	log           *logrus.Logger  }  // New returns a new status processor. -func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClientAPI chan gtsmodel.FromClientAPI, log *logrus.Logger) Processor { +func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClientAPI chan messages.FromClientAPI, log *logrus.Logger) Processor {  	return &processor{  		tc:            tc,  		config:        config, diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go index ba95a96a8..707a4843b 100644 --- a/internal/processing/status/status_test.go +++ b/internal/processing/status/status_test.go @@ -24,7 +24,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/processing/status"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  ) @@ -36,11 +36,11 @@ type StatusStandardTestSuite struct {  	db                db.DB  	log               *logrus.Logger  	typeConverter     typeutils.TypeConverter -	fromClientAPIChan chan gtsmodel.FromClientAPI +	fromClientAPIChan chan messages.FromClientAPI  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/processing/status/unboost.go b/internal/processing/status/unboost.go index c3c667a71..13c24d638 100644 --- a/internal/processing/status/unboost.go +++ b/internal/processing/status/unboost.go @@ -23,10 +23,12 @@ import (  	"errors"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  )  func (p *processor) Unboost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { @@ -89,9 +91,9 @@ func (p *processor) Unboost(ctx context.Context, requestingAccount *gtsmodel.Acc  		gtsBoost.BoostOf.Account = targetStatus.Account  		// send it back to the processor for async processing -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsAnnounce, -			APActivityType: gtsmodel.ActivityStreamsUndo, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityAnnounce, +			APActivityType: ap.ActivityUndo,  			GTSModel:       gtsBoost,  			OriginAccount:  requestingAccount,  			TargetAccount:  targetStatus.Account, diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go index 3d079e2ff..27ce9b156 100644 --- a/internal/processing/status/unfave.go +++ b/internal/processing/status/unfave.go @@ -23,10 +23,12 @@ import (  	"errors"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  )  func (p *processor) Unfave(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { @@ -71,9 +73,9 @@ func (p *processor) Unfave(ctx context.Context, requestingAccount *gtsmodel.Acco  		}  		// send it back to the processor for async processing -		p.fromClientAPI <- gtsmodel.FromClientAPI{ -			APObjectType:   gtsmodel.ActivityStreamsLike, -			APActivityType: gtsmodel.ActivityStreamsUndo, +		p.fromClientAPI <- messages.FromClientAPI{ +			APObjectType:   ap.ActivityLike, +			APActivityType: ap.ActivityUndo,  			GTSModel:       gtsFave,  			OriginAccount:  requestingAccount,  			TargetAccount:  targetStatus.Account, diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index 26ee5d4f7..8861a532b 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -33,7 +33,7 @@ import (  func (p *processor) ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {  	// by default all flags are set to true -	gtsAdvancedVis := >smodel.VisibilityAdvanced{ +	gtsAdvancedVis := gtsmodel.VisibilityAdvanced{  		Federated: true,  		Boostable: true,  		Replyable: true, @@ -123,11 +123,8 @@ func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.Advance  		}  		return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)  	} - -	if repliedStatus.VisibilityAdvanced != nil { -		if !repliedStatus.VisibilityAdvanced.Replyable { -			return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) -		} +	if !repliedStatus.VisibilityAdvanced.Replyable { +		return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)  	}  	// check replied account is known to us diff --git a/internal/processing/status/util_test.go b/internal/processing/status/util_test.go index 1ec2076b1..f80cf9342 100644 --- a/internal/processing/status/util_test.go +++ b/internal/processing/status/util_test.go @@ -27,6 +27,7 @@ import (  	"github.com/stretchr/testify/suite"  	"github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/processing/status"  	"github.com/superseriousbusiness/gotosocial/testrig"  ) @@ -68,7 +69,7 @@ func (suite *UtilTestSuite) SetupTest() {  	suite.db = testrig.NewTestDB()  	suite.log = testrig.NewTestLog()  	suite.typeConverter = testrig.NewTestTypeConverter(suite.db) -	suite.fromClientAPIChan = make(chan gtsmodel.FromClientAPI, 100) +	suite.fromClientAPIChan = make(chan messages.FromClientAPI, 100)  	suite.status = status.New(suite.db, suite.typeConverter, suite.config, suite.fromClientAPIChan, suite.log)  	testrig.StandardDBSetup(suite.db, nil) diff --git a/internal/processing/streaming.go b/internal/processing/streaming.go index e1c134d00..fd5113b0d 100644 --- a/internal/processing/streaming.go +++ b/internal/processing/streaming.go @@ -23,12 +23,13 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/stream"  )  func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) {  	return p.streamingProcessor.AuthorizeStreamingRequest(ctx, accessToken)  } -func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode) { +func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode) {  	return p.streamingProcessor.OpenStreamForAccount(ctx, account, streamType)  } diff --git a/internal/processing/streaming/openstream.go b/internal/processing/streaming/openstream.go index dfad5398e..d4e4eef9f 100644 --- a/internal/processing/streaming/openstream.go +++ b/internal/processing/streaming/openstream.go @@ -9,9 +9,10 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/stream"  ) -func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode) { +func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode) {  	l := p.log.WithFields(logrus.Fields{  		"func":       "OpenStreamForAccount",  		"account":    account.ID, @@ -25,10 +26,10 @@ func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error generating stream id: %s", err))  	} -	thisStream := >smodel.Stream{ +	thisStream := &stream.Stream{  		ID:        streamID,  		Type:      streamType, -		Messages:  make(chan *gtsmodel.Message, 100), +		Messages:  make(chan *stream.Message, 100),  		Hangup:    make(chan interface{}, 1),  		Connected: true,  	} @@ -37,8 +38,8 @@ func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.  	v, ok := p.streamMap.Load(account.ID)  	if !ok || v == nil {  		// there is no entry in the streamMap for this account yet, so make one and store it -		streamsForAccount := >smodel.StreamsForAccount{ -			Streams: []*gtsmodel.Stream{ +		streamsForAccount := &stream.StreamsForAccount{ +			Streams: []*stream.Stream{  				thisStream,  			},  		} @@ -46,7 +47,7 @@ func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.  	} else {  		// there is an entry in the streamMap for this account  		// parse the interface as a streamsForAccount -		streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount) +		streamsForAccount, ok := v.(*stream.StreamsForAccount)  		if !ok {  			return nil, gtserror.NewErrorInternalError(errors.New("stream map error"))  		} @@ -63,7 +64,7 @@ func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.  // waitToCloseStream waits until the hangup channel is closed for the given stream.  // It then iterates through the map of streams stored by the processor, removes the stream from it,  // and then closes the messages channel of the stream to indicate that the channel should no longer be read from. -func (p *processor) waitToCloseStream(account *gtsmodel.Account, thisStream *gtsmodel.Stream) { +func (p *processor) waitToCloseStream(account *gtsmodel.Account, thisStream *stream.Stream) {  	<-thisStream.Hangup // wait for a hangup message  	// lock the stream to prevent more messages being put in it while we work @@ -78,7 +79,7 @@ func (p *processor) waitToCloseStream(account *gtsmodel.Account, thisStream *gts  	if !ok || v == nil {  		return  	} -	streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount) +	streamsForAccount, ok := v.(*stream.StreamsForAccount)  	if !ok {  		return  	} @@ -88,7 +89,7 @@ func (p *processor) waitToCloseStream(account *gtsmodel.Account, thisStream *gts  	defer streamsForAccount.Unlock()  	// put everything into modified streams *except* the stream we're removing -	modifiedStreams := []*gtsmodel.Stream{} +	modifiedStreams := []*stream.Stream{}  	for _, s := range streamsForAccount.Streams {  		if s.ID != thisStream.ID {  			modifiedStreams = append(modifiedStreams, s) diff --git a/internal/processing/streaming/streamdelete.go b/internal/processing/streaming/streamdelete.go index 2282c29ae..cd541bc57 100644 --- a/internal/processing/streaming/streamdelete.go +++ b/internal/processing/streaming/streamdelete.go @@ -4,7 +4,7 @@ import (  	"fmt"  	"strings" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/stream"  )  func (p *processor) StreamDelete(statusID string) error { @@ -20,7 +20,7 @@ func (p *processor) StreamDelete(statusID string) error {  		}  		// the value of the map should be a buncha streams -		streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount) +		streamsForAccount, ok := v.(*stream.StreamsForAccount)  		if !ok {  			errs = append(errs, fmt.Sprintf("stream map error for account stream %s", accountID))  		} @@ -28,13 +28,13 @@ func (p *processor) StreamDelete(statusID string) error {  		// lock the streams while we work on them  		streamsForAccount.Lock()  		defer streamsForAccount.Unlock() -		for _, stream := range streamsForAccount.Streams { +		for _, s := range streamsForAccount.Streams {  			// lock each individual stream as we work on it -			stream.Lock() -			defer stream.Unlock() -			if stream.Connected { -				stream.Messages <- >smodel.Message{ -					Stream:  []string{stream.Type}, +			s.Lock() +			defer s.Unlock() +			if s.Connected { +				s.Messages <- &stream.Message{ +					Stream:  []string{s.Type},  					Event:   "delete",  					Payload: statusID,  				} diff --git a/internal/processing/streaming/streaming.go b/internal/processing/streaming/streaming.go index f349a655a..610d4a9d2 100644 --- a/internal/processing/streaming/streaming.go +++ b/internal/processing/streaming/streaming.go @@ -11,6 +11,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/oauth" +	"github.com/superseriousbusiness/gotosocial/internal/stream"  	"github.com/superseriousbusiness/gotosocial/internal/typeutils"  	"github.com/superseriousbusiness/gotosocial/internal/visibility"  ) @@ -20,7 +21,7 @@ type Processor interface {  	// AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API  	AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error)  	// OpenStreamForAccount returns a new Stream for the given account, which will contain a channel for passing messages back to the caller. -	OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode) +	OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode)  	// StreamStatusToAccount streams the given status to any open, appropriate streams belonging to the given account.  	StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error  	// StreamNotificationToAccount streams the given notification to any open, appropriate streams belonging to the given account. diff --git a/internal/processing/streaming/streamnotification.go b/internal/processing/streaming/streamnotification.go index 24c8342ee..d8460874f 100644 --- a/internal/processing/streaming/streamnotification.go +++ b/internal/processing/streaming/streamnotification.go @@ -8,6 +8,7 @@ import (  	"github.com/sirupsen/logrus"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/stream"  )  func (p *processor) StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error { @@ -21,7 +22,7 @@ func (p *processor) StreamNotificationToAccount(n *apimodel.Notification, accoun  		return nil  	} -	streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount) +	streamsForAccount, ok := v.(*stream.StreamsForAccount)  	if !ok {  		return errors.New("stream map error")  	} @@ -33,13 +34,13 @@ func (p *processor) StreamNotificationToAccount(n *apimodel.Notification, accoun  	streamsForAccount.Lock()  	defer streamsForAccount.Unlock() -	for _, stream := range streamsForAccount.Streams { -		stream.Lock() -		defer stream.Unlock() -		if stream.Connected { -			l.Debugf("streaming notification to stream id %s", stream.ID) -			stream.Messages <- >smodel.Message{ -				Stream:  []string{stream.Type}, +	for _, s := range streamsForAccount.Streams { +		s.Lock() +		defer s.Unlock() +		if s.Connected { +			l.Debugf("streaming notification to stream id %s", s.ID) +			s.Messages <- &stream.Message{ +				Stream:  []string{s.Type},  				Event:   "notification",  				Payload: string(notificationBytes),  			} diff --git a/internal/processing/streaming/streamstatus.go b/internal/processing/streaming/streamstatus.go index 8d026252d..f4d6b2629 100644 --- a/internal/processing/streaming/streamstatus.go +++ b/internal/processing/streaming/streamstatus.go @@ -8,6 +8,7 @@ import (  	"github.com/sirupsen/logrus"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/stream"  )  func (p *processor) StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error { @@ -21,7 +22,7 @@ func (p *processor) StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.  		return nil  	} -	streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount) +	streamsForAccount, ok := v.(*stream.StreamsForAccount)  	if !ok {  		return errors.New("stream map error")  	} @@ -33,13 +34,13 @@ func (p *processor) StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.  	streamsForAccount.Lock()  	defer streamsForAccount.Unlock() -	for _, stream := range streamsForAccount.Streams { -		stream.Lock() -		defer stream.Unlock() -		if stream.Connected { -			l.Debugf("streaming status to stream id %s", stream.ID) -			stream.Messages <- >smodel.Message{ -				Stream:  []string{stream.Type}, +	for _, s := range streamsForAccount.Streams { +		s.Lock() +		defer s.Unlock() +		if s.Connected { +			l.Debugf("streaming status to stream id %s", s.ID) +			s.Messages <- &stream.Message{ +				Stream:  []string{s.Type},  				Event:   "update",  				Payload: string(statusBytes),  			} diff --git a/internal/regexes/regexes.go b/internal/regexes/regexes.go new file mode 100644 index 000000000..8ac31ef62 --- /dev/null +++ b/internal/regexes/regexes.go @@ -0,0 +1,136 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package regexes + +import ( +	"fmt" +	"regexp" +) + +const ( +	users     = "users" +	actors    = "actors" +	statuses  = "statuses" +	inbox     = "inbox" +	outbox    = "outbox" +	followers = "followers" +	following = "following" +	liked     = "liked" +	// collections = "collections" +	// featured    = "featured" +	publicKey = "main-key" +	follow    = "follow" +	// update      = "updates" +	blocks = "blocks" +) + +const ( +	maximumUsernameLength       = 64 +	maximumEmojiShortcodeLength = 30 +	maximumHashtagLength        = 30 +) + +var ( +	mentionName = `^@(\w+)(?:@([a-zA-Z0-9_\-\.:]+)?)$` +	// MentionName captures the username and domain part from a mention string +	// such as @whatever_user@example.org, returning whatever_user and example.org (without the @ symbols) +	MentionName = regexp.MustCompile(mentionName) + +	// mention regex can be played around with here: https://regex101.com/r/qwM9D3/1 +	mentionFinder = `(?:\B)(@\w+(?:@[a-zA-Z0-9_\-\.]+)?)(?:\B)?` +	// MentionFinder extracts mentions from a piece of text. +	MentionFinder = regexp.MustCompile(mentionFinder) + +	// hashtag regex can be played with here: https://regex101.com/r/bPxeca/1 +	hashtagFinder = fmt.Sprintf(`(?:^|\n|\s)(#[a-zA-Z0-9]{1,%d})(?:\b)`, maximumHashtagLength) +	// HashtagFinder finds possible hashtags in a string. +	// It returns just the string part of the hashtag, not the # symbol. +	HashtagFinder = regexp.MustCompile(hashtagFinder) + +	emojiShortcode = fmt.Sprintf(`\w{2,%d}`, maximumEmojiShortcodeLength) +	// EmojiShortcode validates an emoji name. +	EmojiShortcode = regexp.MustCompile(fmt.Sprintf("^%s$", emojiShortcode)) + +	// emoji regex can be played with here: https://regex101.com/r/478XGM/1 +	emojiFinderString = fmt.Sprintf(`(?:\B)?:(%s):(?:\B)?`, emojiShortcode) +	// EmojiFinder extracts emoji strings from a piece of text. +	EmojiFinder = regexp.MustCompile(emojiFinderString) + +	// usernameString defines an acceptable username on this instance +	usernameString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumUsernameLength) +	// Username can be used to validate usernames of new signups +	Username = regexp.MustCompile(fmt.Sprintf(`^%s$`, usernameString)) + +	userPathString = fmt.Sprintf(`^?/%s/(%s)$`, users, usernameString) +	// UserPath parses a path that validates and captures the username part from eg /users/example_username +	UserPath = regexp.MustCompile(userPathString) + +	publicKeyPath = fmt.Sprintf(`^?/%s/(%s)/%s`, users, usernameString, publicKey) +	// PublicKeyPath parses a path that validates and captures the username part from eg /users/example_username/main-key +	PublicKeyPath = regexp.MustCompile(publicKeyPath) + +	inboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, inbox) +	// InboxPath parses a path that validates and captures the username part from eg /users/example_username/inbox +	InboxPath = regexp.MustCompile(inboxPath) + +	outboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, outbox) +	// OutboxPath parses a path that validates and captures the username part from eg /users/example_username/outbox +	OutboxPath = regexp.MustCompile(outboxPath) + +	actorPath = fmt.Sprintf(`^?/%s/(%s)$`, actors, usernameString) +	// ActorPath parses a path that validates and captures the username part from eg /actors/example_username +	ActorPath = regexp.MustCompile(actorPath) + +	followersPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, followers) +	// FollowersPath parses a path that validates and captures the username part from eg /users/example_username/followers +	FollowersPath = regexp.MustCompile(followersPath) + +	followingPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, following) +	// FollowingPath parses a path that validates and captures the username part from eg /users/example_username/following +	FollowingPath = regexp.MustCompile(followingPath) + +	followPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, follow, ulid) +	// FollowPath parses a path that validates and captures the username part and the ulid part +	// from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH +	FollowPath = regexp.MustCompile(followPath) + +	ulid = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}` +	// ULID parses and validate a ULID. +	ULID = regexp.MustCompile(fmt.Sprintf(`^%s$`, ulid)) + +	likedPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameString, liked) +	// LikedPath parses a path that validates and captures the username part from eg /users/example_username/liked +	LikedPath = regexp.MustCompile(likedPath) + +	likePath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, liked, ulid) +	// LikePath parses a path that validates and captures the username part and the ulid part +	// from eg /users/example_username/like/01F7XT5JZW1WMVSW1KADS8PVDH +	LikePath = regexp.MustCompile(likePath) + +	statusesPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, statuses, ulid) +	// StatusesPath parses a path that validates and captures the username part and the ulid part +	// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH +	// The regex can be played with here: https://regex101.com/r/G9zuxQ/1 +	StatusesPath = regexp.MustCompile(statusesPath) + +	blockPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameString, blocks, ulid) +	// BlockPath parses a path that validates and captures the username part and the ulid part +	// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH +	BlockPath = regexp.MustCompile(blockPath) +) diff --git a/internal/gtsmodel/stream.go b/internal/stream/stream.go index 4a1571de5..9d1d27d72 100644 --- a/internal/gtsmodel/stream.go +++ b/internal/stream/stream.go @@ -1,4 +1,4 @@ -package gtsmodel +package stream  import "sync" diff --git a/internal/text/common.go b/internal/text/common.go index a8d585a09..a3ec15e46 100644 --- a/internal/text/common.go +++ b/internal/text/common.go @@ -25,7 +25,7 @@ import (  	"strings"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/regexes"  )  // preformat contains some common logic for making a string ready for formatting, which should be used for all user-input text. @@ -61,7 +61,7 @@ func postformat(in string) string {  }  func (f *formatter) ReplaceTags(ctx context.Context, in string, tags []*gtsmodel.Tag) string { -	return util.HashtagFinderRegex.ReplaceAllStringFunc(in, func(match string) string { +	return regexes.HashtagFinder.ReplaceAllStringFunc(in, func(match string) string {  		// we have a match  		matchTrimmed := strings.TrimSpace(match)  		tagAsEntered := strings.Split(matchTrimmed, "#")[1] diff --git a/internal/text/formatter_test.go b/internal/text/formatter_test.go index 803088794..228da4ec7 100644 --- a/internal/text/formatter_test.go +++ b/internal/text/formatter_test.go @@ -24,7 +24,6 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth"  	"github.com/superseriousbusiness/gotosocial/internal/text"  ) @@ -37,8 +36,8 @@ type TextStandardTestSuite struct {  	log    *logrus.Logger  	// standard suite models -	testTokens       map[string]*oauth.Token -	testClients      map[string]*oauth.Client +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client  	testApplications map[string]*gtsmodel.Application  	testUsers        map[string]*gtsmodel.User  	testAccounts     map[string]*gtsmodel.Account diff --git a/internal/transport/derefinstance.go b/internal/transport/derefinstance.go index 3d72d7581..673881b05 100644 --- a/internal/transport/derefinstance.go +++ b/internal/transport/derefinstance.go @@ -32,6 +32,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id"  	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  func (t *transport) DereferenceInstance(ctx context.Context, iri *url.URL) (*gtsmodel.Instance, error) { @@ -199,7 +200,7 @@ func dereferenceByNodeInfo(c context.Context, t *transport, iri *url.URL) (*gtsm  		if v, ok := i.(map[string]string); ok {  			// see if there's an email in the map  			if email, present := v["email"]; present { -				if err := util.ValidateEmail(email); err == nil { +				if err := validate.Email(email); err == nil {  					// valid email address  					contactEmail = email  				} diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 4ba0df383..580f999bc 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -94,15 +94,15 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a  	// check for bot and actor type  	switch accountable.GetTypeName() { -	case gtsmodel.ActivityStreamsPerson, gtsmodel.ActivityStreamsGroup, gtsmodel.ActivityStreamsOrganization: +	case ap.ActorPerson, ap.ActorGroup, ap.ActorOrganization:  		// people, groups, and organizations aren't bots  		acct.Bot = false  		// apps and services are -	case gtsmodel.ActivityStreamsApplication, gtsmodel.ActivityStreamsService: +	case ap.ActorApplication, ap.ActorService:  		acct.Bot = true  	default:  		// we don't know what this is! -		return nil, fmt.Errorf("type name %s not recognised or not convertible to gtsmodel.ActivityStreamsActor", accountable.GetTypeName()) +		return nil, fmt.Errorf("type name %s not recognised or not convertible to ap.ActivityStreamsActor", accountable.GetTypeName())  	}  	acct.ActorType = accountable.GetTypeName() diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 03aa0c77b..7924e2185 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -227,7 +227,6 @@ func (c *converter) AppToMastoSensitive(ctx context.Context, a *gtsmodel.Applica  		RedirectURI:  a.RedirectURI,  		ClientID:     a.ClientID,  		ClientSecret: a.ClientSecret, -		VapidKey:     a.VapidKey,  	}, nil  } diff --git a/internal/util/regexes.go b/internal/util/regexes.go deleted file mode 100644 index 88212fc43..000000000 --- a/internal/util/regexes.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package util - -import ( -	"fmt" -	"regexp" -) - -const ( -	maximumUsernameLength       = 64 -	maximumEmojiShortcodeLength = 30 -	maximumHashtagLength        = 30 -) - -var ( -	mentionNameRegexString = `^@(\w+)(?:@([a-zA-Z0-9_\-\.:]+)?)$` -	// mention name regex captures the username and domain part from a mention string -	// such as @whatever_user@example.org, returning whatever_user and example.org (without the @ symbols) -	mentionNameRegex = regexp.MustCompile(mentionNameRegexString) - -	// mention regex can be played around with here: https://regex101.com/r/qwM9D3/1 -	mentionFinderRegexString = `(?:\B)(@\w+(?:@[a-zA-Z0-9_\-\.]+)?)(?:\B)?` -	mentionFinderRegex       = regexp.MustCompile(mentionFinderRegexString) - -	// hashtag regex can be played with here: https://regex101.com/r/bPxeca/1 -	hashtagFinderRegexString = fmt.Sprintf(`(?:^|\n|\s)(#[a-zA-Z0-9]{1,%d})(?:\b)`, maximumHashtagLength) -	// HashtagFinderRegex finds possible hashtags in a string. -	// It returns just the string part of the hashtag, not the # symbol. -	HashtagFinderRegex = regexp.MustCompile(hashtagFinderRegexString) - -	emojiShortcodeRegexString     = fmt.Sprintf(`\w{2,%d}`, maximumEmojiShortcodeLength) -	emojiShortcodeValidationRegex = regexp.MustCompile(fmt.Sprintf("^%s$", emojiShortcodeRegexString)) - -	// emoji regex can be played with here: https://regex101.com/r/478XGM/1 -	emojiFinderRegexString = fmt.Sprintf(`(?:\B)?:(%s):(?:\B)?`, emojiShortcodeRegexString) -	emojiFinderRegex       = regexp.MustCompile(emojiFinderRegexString) - -	// usernameRegexString defines an acceptable username on this instance -	usernameRegexString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumUsernameLength) -	// usernameValidationRegex can be used to validate usernames of new signups -	usernameValidationRegex = regexp.MustCompile(fmt.Sprintf(`^%s$`, usernameRegexString)) - -	userPathRegexString = fmt.Sprintf(`^?/%s/(%s)$`, UsersPath, usernameRegexString) -	// userPathRegex parses a path that validates and captures the username part from eg /users/example_username -	userPathRegex = regexp.MustCompile(userPathRegexString) - -	userPublicKeyPathRegexString = fmt.Sprintf(`^?/%s/(%s)/%s`, UsersPath, usernameRegexString, PublicKeyPath) -	userPublicKeyPathRegex       = regexp.MustCompile(userPublicKeyPathRegexString) - -	inboxPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, InboxPath) -	// inboxPathRegex parses a path that validates and captures the username part from eg /users/example_username/inbox -	inboxPathRegex = regexp.MustCompile(inboxPathRegexString) - -	outboxPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, OutboxPath) -	// outboxPathRegex parses a path that validates and captures the username part from eg /users/example_username/outbox -	outboxPathRegex = regexp.MustCompile(outboxPathRegexString) - -	actorPathRegexString = fmt.Sprintf(`^?/%s/(%s)$`, ActorsPath, usernameRegexString) -	// actorPathRegex parses a path that validates and captures the username part from eg /actors/example_username -	actorPathRegex = regexp.MustCompile(actorPathRegexString) - -	followersPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, FollowersPath) -	// followersPathRegex parses a path that validates and captures the username part from eg /users/example_username/followers -	followersPathRegex = regexp.MustCompile(followersPathRegexString) - -	followingPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, FollowingPath) -	// followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/following -	followingPathRegex = regexp.MustCompile(followingPathRegexString) - -	followPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, FollowPath, ulidRegexString) -	// followPathRegex parses a path that validates and captures the username part and the ulid part -	// from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH -	followPathRegex = regexp.MustCompile(followPathRegexString) - -	ulidRegexString = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}` - -	likedPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, LikedPath) -	// likedPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked -	likedPathRegex = regexp.MustCompile(likedPathRegexString) - -	likePathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, LikedPath, ulidRegexString) -	// likePathRegex parses a path that validates and captures the username part and the ulid part -	// from eg /users/example_username/like/01F7XT5JZW1WMVSW1KADS8PVDH -	likePathRegex = regexp.MustCompile(likePathRegexString) - -	statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, ulidRegexString) -	// statusesPathRegex parses a path that validates and captures the username part and the ulid part -	// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH -	// The regex can be played with here: https://regex101.com/r/G9zuxQ/1 -	statusesPathRegex = regexp.MustCompile(statusesPathRegexString) - -	blockPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, BlocksPath, ulidRegexString) -	// blockPathRegex parses a path that validates and captures the username part and the ulid part -	// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH -	blockPathRegex = regexp.MustCompile(blockPathRegexString) -) diff --git a/internal/util/statustools.go b/internal/util/statustools.go index 4a89e60f6..ca18577b0 100644 --- a/internal/util/statustools.go +++ b/internal/util/statustools.go @@ -21,6 +21,8 @@ package util  import (  	"fmt"  	"strings" + +	"github.com/superseriousbusiness/gotosocial/internal/regexes"  )  // DeriveMentionsFromStatus takes a plaintext (ie., not html-formatted) status, @@ -31,7 +33,7 @@ import (  // or the form "@username" for local users.  func DeriveMentionsFromStatus(status string) []string {  	mentionedAccounts := []string{} -	for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) { +	for _, m := range regexes.MentionFinder.FindAllStringSubmatch(status, -1) {  		mentionedAccounts = append(mentionedAccounts, m[1])  	}  	return UniqueStrings(mentionedAccounts) @@ -43,7 +45,7 @@ func DeriveMentionsFromStatus(status string) []string {  // tags will be lowered, for consistency.  func DeriveHashtagsFromStatus(status string) []string {  	tags := []string{} -	for _, m := range HashtagFinderRegex.FindAllStringSubmatch(status, -1) { +	for _, m := range regexes.HashtagFinder.FindAllStringSubmatch(status, -1) {  		tags = append(tags, strings.TrimPrefix(m[1], "#"))  	}  	return UniqueStrings(tags) @@ -54,7 +56,7 @@ func DeriveHashtagsFromStatus(status string) []string {  // used in that status, without the surround ::.  func DeriveEmojisFromStatus(status string) []string {  	emojis := []string{} -	for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) { +	for _, m := range regexes.EmojiFinder.FindAllStringSubmatch(status, -1) {  		emojis = append(emojis, m[1])  	}  	return UniqueStrings(emojis) @@ -65,7 +67,7 @@ func DeriveEmojisFromStatus(status string) []string {  //  // If nothing is matched, it will return an error.  func ExtractMentionParts(mention string) (username, domain string, err error) { -	matches := mentionNameRegex.FindStringSubmatch(mention) +	matches := regexes.MentionName.FindStringSubmatch(mention)  	if matches == nil || len(matches) != 3 {  		err = fmt.Errorf("could't match mention %s", mention)  		return @@ -77,5 +79,5 @@ func ExtractMentionParts(mention string) (username, domain string, err error) {  // IsMention returns true if the passed string looks like @whatever@example.org  func IsMention(mention string) bool { -	return mentionNameRegex.MatchString(strings.ToLower(mention)) +	return regexes.MentionName.MatchString(strings.ToLower(mention))  } diff --git a/internal/util/uri.go b/internal/util/uri.go index 370b2fa6f..91f523a4d 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -21,6 +21,8 @@ package util  import (  	"fmt"  	"net/url" + +	"github.com/superseriousbusiness/gotosocial/internal/regexes"  )  const ( @@ -169,67 +171,67 @@ func GenerateURIsForAccount(username string, protocol string, host string) *User  // IsUserPath returns true if the given URL path corresponds to eg /users/example_username  func IsUserPath(id *url.URL) bool { -	return userPathRegex.MatchString(id.Path) +	return regexes.UserPath.MatchString(id.Path)  }  // IsInboxPath returns true if the given URL path corresponds to eg /users/example_username/inbox  func IsInboxPath(id *url.URL) bool { -	return inboxPathRegex.MatchString(id.Path) +	return regexes.InboxPath.MatchString(id.Path)  }  // IsOutboxPath returns true if the given URL path corresponds to eg /users/example_username/outbox  func IsOutboxPath(id *url.URL) bool { -	return outboxPathRegex.MatchString(id.Path) +	return regexes.OutboxPath.MatchString(id.Path)  }  // IsInstanceActorPath returns true if the given URL path corresponds to eg /actors/example_username  func IsInstanceActorPath(id *url.URL) bool { -	return actorPathRegex.MatchString(id.Path) +	return regexes.ActorPath.MatchString(id.Path)  }  // IsFollowersPath returns true if the given URL path corresponds to eg /users/example_username/followers  func IsFollowersPath(id *url.URL) bool { -	return followersPathRegex.MatchString(id.Path) +	return regexes.FollowersPath.MatchString(id.Path)  }  // IsFollowingPath returns true if the given URL path corresponds to eg /users/example_username/following  func IsFollowingPath(id *url.URL) bool { -	return followingPathRegex.MatchString(id.Path) +	return regexes.FollowingPath.MatchString(id.Path)  }  // IsFollowPath returns true if the given URL path corresponds to eg /users/example_username/follow/SOME_ULID_OF_A_FOLLOW  func IsFollowPath(id *url.URL) bool { -	return followPathRegex.MatchString(id.Path) +	return regexes.FollowPath.MatchString(id.Path)  }  // IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked  func IsLikedPath(id *url.URL) bool { -	return likedPathRegex.MatchString(id.Path) +	return regexes.LikedPath.MatchString(id.Path)  }  // IsLikePath returns true if the given URL path corresponds to eg /users/example_username/liked/SOME_ULID_OF_A_STATUS  func IsLikePath(id *url.URL) bool { -	return likePathRegex.MatchString(id.Path) +	return regexes.LikePath.MatchString(id.Path)  }  // IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_ULID_OF_A_STATUS  func IsStatusesPath(id *url.URL) bool { -	return statusesPathRegex.MatchString(id.Path) +	return regexes.StatusesPath.MatchString(id.Path)  }  // IsPublicKeyPath returns true if the given URL path corresponds to eg /users/example_username/main-key  func IsPublicKeyPath(id *url.URL) bool { -	return userPublicKeyPathRegex.MatchString(id.Path) +	return regexes.PublicKeyPath.MatchString(id.Path)  }  // IsBlockPath returns true if the given URL path corresponds to eg /users/example_username/blocks/SOME_ULID_OF_A_BLOCK  func IsBlockPath(id *url.URL) bool { -	return blockPathRegex.MatchString(id.Path) +	return regexes.BlockPath.MatchString(id.Path)  }  // ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS  func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) { -	matches := statusesPathRegex.FindStringSubmatch(id.Path) +	matches := regexes.StatusesPath.FindStringSubmatch(id.Path)  	if len(matches) != 3 {  		err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))  		return @@ -241,7 +243,7 @@ func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {  // ParseUserPath returns the username from a path such as /users/example_username  func ParseUserPath(id *url.URL) (username string, err error) { -	matches := userPathRegex.FindStringSubmatch(id.Path) +	matches := regexes.UserPath.FindStringSubmatch(id.Path)  	if len(matches) != 2 {  		err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))  		return @@ -252,7 +254,7 @@ func ParseUserPath(id *url.URL) (username string, err error) {  // ParseInboxPath returns the username from a path such as /users/example_username/inbox  func ParseInboxPath(id *url.URL) (username string, err error) { -	matches := inboxPathRegex.FindStringSubmatch(id.Path) +	matches := regexes.InboxPath.FindStringSubmatch(id.Path)  	if len(matches) != 2 {  		err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))  		return @@ -263,7 +265,7 @@ func ParseInboxPath(id *url.URL) (username string, err error) {  // ParseOutboxPath returns the username from a path such as /users/example_username/outbox  func ParseOutboxPath(id *url.URL) (username string, err error) { -	matches := outboxPathRegex.FindStringSubmatch(id.Path) +	matches := regexes.OutboxPath.FindStringSubmatch(id.Path)  	if len(matches) != 2 {  		err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))  		return @@ -274,7 +276,7 @@ func ParseOutboxPath(id *url.URL) (username string, err error) {  // ParseFollowersPath returns the username from a path such as /users/example_username/followers  func ParseFollowersPath(id *url.URL) (username string, err error) { -	matches := followersPathRegex.FindStringSubmatch(id.Path) +	matches := regexes.FollowersPath.FindStringSubmatch(id.Path)  	if len(matches) != 2 {  		err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))  		return @@ -285,7 +287,7 @@ func ParseFollowersPath(id *url.URL) (username string, err error) {  // ParseFollowingPath returns the username from a path such as /users/example_username/following  func ParseFollowingPath(id *url.URL) (username string, err error) { -	matches := followingPathRegex.FindStringSubmatch(id.Path) +	matches := regexes.FollowingPath.FindStringSubmatch(id.Path)  	if len(matches) != 2 {  		err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))  		return @@ -296,7 +298,7 @@ func ParseFollowingPath(id *url.URL) (username string, err error) {  // ParseLikedPath returns the username and ulid from a path such as /users/example_username/liked/SOME_ULID_OF_A_STATUS  func ParseLikedPath(id *url.URL) (username string, ulid string, err error) { -	matches := likePathRegex.FindStringSubmatch(id.Path) +	matches := regexes.LikePath.FindStringSubmatch(id.Path)  	if len(matches) != 3 {  		err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))  		return @@ -308,7 +310,7 @@ func ParseLikedPath(id *url.URL) (username string, ulid string, err error) {  // ParseBlockPath returns the username and ulid from a path such as /users/example_username/blocks/SOME_ULID_OF_A_BLOCK  func ParseBlockPath(id *url.URL) (username string, ulid string, err error) { -	matches := blockPathRegex.FindStringSubmatch(id.Path) +	matches := regexes.BlockPath.FindStringSubmatch(id.Path)  	if len(matches) != 3 {  		err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))  		return diff --git a/internal/validate/account_test.go b/internal/validate/account_test.go new file mode 100644 index 000000000..8fcdcfb38 --- /dev/null +++ b/internal/validate/account_test.go @@ -0,0 +1,343 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"crypto/rand" +	"crypto/rsa" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyAccount() *gtsmodel.Account { + +	priv, err := rsa.GenerateKey(rand.Reader, 2048) +	if err != nil { +		panic(err) +	} +	pub := &priv.PublicKey + +	return >smodel.Account{ +		ID:                      "01F8MH1H7YV1Z7D2C8K2730QBF", +		CreatedAt:               time.Now().Add(-48 * time.Hour), +		UpdatedAt:               time.Now().Add(-48 * time.Hour), +		Username:                "the_mighty_zork", +		Domain:                  "", +		AvatarMediaAttachmentID: "01F8MH58A357CV5K7R7TJMSH6S", +		AvatarMediaAttachment:   nil, +		AvatarRemoteURL:         "", +		HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3Q", +		HeaderMediaAttachment:   nil, +		HeaderRemoteURL:         "", +		DisplayName:             "original zork (he/they)", +		Fields:                  []gtsmodel.Field{}, +		Note:                    "hey yo this is my profile!", +		Memorial:                false, +		AlsoKnownAs:             "", +		MovedToAccountID:        "", +		Bot:                     false, +		Reason:                  "I wanna be on this damned webbed site so bad! Please! Wow", +		Locked:                  false, +		Discoverable:            true, +		Privacy:                 gtsmodel.VisibilityPublic, +		Sensitive:               false, +		Language:                "en", +		URI:                     "http://localhost:8080/users/the_mighty_zork", +		URL:                     "http://localhost:8080/@the_mighty_zork", +		LastWebfingeredAt:       time.Time{}, +		InboxURI:                "http://localhost:8080/users/the_mighty_zork/inbox", +		OutboxURI:               "http://localhost:8080/users/the_mighty_zork/outbox", +		FollowersURI:            "http://localhost:8080/users/the_mighty_zork/followers", +		FollowingURI:            "http://localhost:8080/users/the_mighty_zork/following", +		FeaturedCollectionURI:   "http://localhost:8080/users/the_mighty_zork/collections/featured", +		ActorType:               ap.ActorPerson, +		PrivateKey:              priv, +		PublicKey:               pub, +		PublicKeyURI:            "http://localhost:8080/users/the_mighty_zork#main-key", +		SensitizedAt:            time.Time{}, +		SilencedAt:              time.Time{}, +		SuspendedAt:             time.Time{}, +		HideCollections:         false, +		SuspensionOrigin:        "", +	} +} + +type AccountValidateTestSuite struct { +	suite.Suite +} + +func (suite *AccountValidateTestSuite) TestValidateAccountHappyPath() { +	// no problem here +	a := happyAccount() +	err := validate.Struct(*a) +	suite.NoError(err) +} + +// ID must be set and be valid ULID +func (suite *AccountValidateTestSuite) TestValidateAccountBadID() { +	a := happyAccount() + +	a.ID = "" +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	a.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +// CreatedAt can be set or not -- it will be set in the database anyway +func (suite *AccountValidateTestSuite) TestValidateAccountNoCreatedAt() { +	a := happyAccount() + +	a.CreatedAt = time.Time{} +	err := validate.Struct(*a) +	suite.NoError(err) +} + +// LastWebfingeredAt must be defined if remote account +func (suite *AccountValidateTestSuite) TestValidateAccountNoWebfingeredAt() { +	a := happyAccount() + +	a.Domain = "example.org" +	a.LastWebfingeredAt = time.Time{} +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.LastWebfingeredAt' Error:Field validation for 'LastWebfingeredAt' failed on the 'required_with' tag") +} + +// Username must be set +func (suite *AccountValidateTestSuite) TestValidateAccountUsername() { +	a := happyAccount() + +	a.Username = "" +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Username' Error:Field validation for 'Username' failed on the 'required' tag") +} + +// Domain must be either empty (for local accounts) or proper fqdn (for remote accounts) +func (suite *AccountValidateTestSuite) TestValidateAccountDomain() { +	a := happyAccount() +	a.LastWebfingeredAt = time.Now() + +	a.Domain = "" +	err := validate.Struct(*a) +	suite.NoError(err) + +	a.Domain = "localhost:8080" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") + +	a.Domain = "ahhhhh" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") + +	a.Domain = "https://www.example.org" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") + +	a.Domain = "example.org:8080" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") + +	a.Domain = "example.org" +	err = validate.Struct(*a) +	suite.NoError(err) +} + +// Attachment IDs must either be not set, or must be valid ULID +func (suite *AccountValidateTestSuite) TestValidateAttachmentIDs() { +	a := happyAccount() + +	a.AvatarMediaAttachmentID = "" +	a.HeaderMediaAttachmentID = "" +	err := validate.Struct(*a) +	suite.NoError(err) + +	a.AvatarMediaAttachmentID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	a.HeaderMediaAttachmentID = "aaaa" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.AvatarMediaAttachmentID' Error:Field validation for 'AvatarMediaAttachmentID' failed on the 'ulid' tag\nKey: 'Account.HeaderMediaAttachmentID' Error:Field validation for 'HeaderMediaAttachmentID' failed on the 'ulid' tag") +} + +// Attachment remote URLs must either not be set, or be valid URLs +func (suite *AccountValidateTestSuite) TestValidateAttachmentRemoteURLs() { +	a := happyAccount() + +	a.AvatarRemoteURL = "" +	a.HeaderRemoteURL = "" +	err := validate.Struct(*a) +	suite.NoError(err) + +	a.AvatarRemoteURL = "-------------" +	a.HeaderRemoteURL = "https://valid-url.com" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.AvatarRemoteURL' Error:Field validation for 'AvatarRemoteURL' failed on the 'url' tag") + +	a.AvatarRemoteURL = "https://valid-url.com" +	a.HeaderRemoteURL = "" +	err = validate.Struct(*a) +	suite.NoError(err) +} + +// Default privacy must be set if account is local +func (suite *AccountValidateTestSuite) TestValidatePrivacy() { +	a := happyAccount() +	a.LastWebfingeredAt = time.Now() + +	a.Privacy = "" +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Privacy' Error:Field validation for 'Privacy' failed on the 'required_without' tag") + +	a.Privacy = "not valid" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Privacy' Error:Field validation for 'Privacy' failed on the 'oneof' tag") + +	a.Privacy = gtsmodel.VisibilityFollowersOnly +	err = validate.Struct(*a) +	suite.NoError(err) + +	a.Privacy = "" +	a.Domain = "example.org" +	err = validate.Struct(*a) +	suite.NoError(err) + +	a.Privacy = "invalid" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Privacy' Error:Field validation for 'Privacy' failed on the 'oneof' tag") +} + +// If set, language must be a valid language +func (suite *AccountValidateTestSuite) TestValidateLanguage() { +	a := happyAccount() + +	a.Language = "" +	err := validate.Struct(*a) +	suite.NoError(err) + +	a.Language = "not valid" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.Language' Error:Field validation for 'Language' failed on the 'bcp47_language_tag' tag") + +	a.Language = "en-uk" +	err = validate.Struct(*a) +	suite.NoError(err) +} + +// Account URI must be set and must be valid +func (suite *AccountValidateTestSuite) TestValidateAccountURI() { +	a := happyAccount() + +	a.URI = "invalid-uri" +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.URI' Error:Field validation for 'URI' failed on the 'url' tag") + +	a.URI = "" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.URI' Error:Field validation for 'URI' failed on the 'required' tag") +} + +// ActivityPub URIs must be set on account if it's local +func (suite *AccountValidateTestSuite) TestValidateAccountURIs() { +	a := happyAccount() +	a.LastWebfingeredAt = time.Now() + +	a.InboxURI = "invalid-uri" +	a.OutboxURI = "invalid-uri" +	a.FollowersURI = "invalid-uri" +	a.FollowingURI = "invalid-uri" +	a.FeaturedCollectionURI = "invalid-uri" +	a.PublicKeyURI = "invalid-uri" +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.InboxURI' Error:Field validation for 'InboxURI' failed on the 'url' tag\nKey: 'Account.OutboxURI' Error:Field validation for 'OutboxURI' failed on the 'url' tag\nKey: 'Account.FollowingURI' Error:Field validation for 'FollowingURI' failed on the 'url' tag\nKey: 'Account.FollowersURI' Error:Field validation for 'FollowersURI' failed on the 'url' tag\nKey: 'Account.FeaturedCollectionURI' Error:Field validation for 'FeaturedCollectionURI' failed on the 'url' tag\nKey: 'Account.PublicKeyURI' Error:Field validation for 'PublicKeyURI' failed on the 'url' tag") + +	a.InboxURI = "" +	a.OutboxURI = "" +	a.FollowersURI = "" +	a.FollowingURI = "" +	a.FeaturedCollectionURI = "" +	a.PublicKeyURI = "" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.InboxURI' Error:Field validation for 'InboxURI' failed on the 'required_without' tag\nKey: 'Account.OutboxURI' Error:Field validation for 'OutboxURI' failed on the 'required_without' tag\nKey: 'Account.FollowingURI' Error:Field validation for 'FollowingURI' failed on the 'required_without' tag\nKey: 'Account.FollowersURI' Error:Field validation for 'FollowersURI' failed on the 'required_without' tag\nKey: 'Account.FeaturedCollectionURI' Error:Field validation for 'FeaturedCollectionURI' failed on the 'required_without' tag\nKey: 'Account.PublicKeyURI' Error:Field validation for 'PublicKeyURI' failed on the 'required' tag") + +	a.Domain = "example.org" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.PublicKeyURI' Error:Field validation for 'PublicKeyURI' failed on the 'required' tag") + +	a.InboxURI = "invalid-uri" +	a.OutboxURI = "invalid-uri" +	a.FollowersURI = "invalid-uri" +	a.FollowingURI = "invalid-uri" +	a.FeaturedCollectionURI = "invalid-uri" +	a.PublicKeyURI = "invalid-uri" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.InboxURI' Error:Field validation for 'InboxURI' failed on the 'url' tag\nKey: 'Account.OutboxURI' Error:Field validation for 'OutboxURI' failed on the 'url' tag\nKey: 'Account.FollowingURI' Error:Field validation for 'FollowingURI' failed on the 'url' tag\nKey: 'Account.FollowersURI' Error:Field validation for 'FollowersURI' failed on the 'url' tag\nKey: 'Account.FeaturedCollectionURI' Error:Field validation for 'FeaturedCollectionURI' failed on the 'url' tag\nKey: 'Account.PublicKeyURI' Error:Field validation for 'PublicKeyURI' failed on the 'url' tag") +} + +// Actor type must be set and valid +func (suite *AccountValidateTestSuite) TestValidateActorType() { +	a := happyAccount() + +	a.ActorType = "" +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.ActorType' Error:Field validation for 'ActorType' failed on the 'oneof' tag") + +	a.ActorType = "not valid" +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.ActorType' Error:Field validation for 'ActorType' failed on the 'oneof' tag") + +	a.ActorType = ap.ActivityArrive +	err = validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.ActorType' Error:Field validation for 'ActorType' failed on the 'oneof' tag") + +	a.ActorType = ap.ActorOrganization +	err = validate.Struct(*a) +	suite.NoError(err) +} + +// Private key must be set on local accounts +func (suite *AccountValidateTestSuite) TestValidatePrivateKey() { +	a := happyAccount() +	a.LastWebfingeredAt = time.Now() + +	a.PrivateKey = nil +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.PrivateKey' Error:Field validation for 'PrivateKey' failed on the 'required_without' tag") + +	a.Domain = "example.org" +	err = validate.Struct(*a) +	suite.NoError(err) +} + +// Public key must be set +func (suite *AccountValidateTestSuite) TestValidatePublicKey() { +	a := happyAccount() + +	a.PublicKey = nil +	err := validate.Struct(*a) +	suite.EqualError(err, "Key: 'Account.PublicKey' Error:Field validation for 'PublicKey' failed on the 'required' tag") +} + +func TestAccountValidateTestSuite(t *testing.T) { +	suite.Run(t, new(AccountValidateTestSuite)) +} diff --git a/internal/validate/application_test.go b/internal/validate/application_test.go new file mode 100644 index 000000000..3e4dc4235 --- /dev/null +++ b/internal/validate/application_test.go @@ -0,0 +1,133 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyApplication() *gtsmodel.Application { +	return >smodel.Application{ +		ID:           "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:    time.Now(), +		UpdatedAt:    time.Now(), +		Name:         "Tusky", +		Website:      "https://tusky.app", +		RedirectURI:  "oauth2redirect://com.keylesspalace.tusky/", +		ClientID:     "01FEEDMF6C0QD589MRK7919Z0R", +		ClientSecret: "bd740cf1-024a-4e4d-8c39-866538f52fe6", +		Scopes:       "read write follow", +	} +} + +type ApplicationValidateTestSuite struct { +	suite.Suite +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationHappyPath() { +	// no problem here +	a := happyApplication() +	err := validate.Struct(a) +	suite.NoError(err) +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationBadID() { +	a := happyApplication() + +	a.ID = "" +	err := validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	a.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationNoCreatedAt() { +	a := happyApplication() + +	a.CreatedAt = time.Time{} +	err := validate.Struct(a) +	suite.NoError(err) +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationName() { +	a := happyApplication() + +	a.Name = "" +	err := validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.Name' Error:Field validation for 'Name' failed on the 'required' tag") +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationWebsite() { +	a := happyApplication() + +	a.Website = "invalid-website" +	err := validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.Website' Error:Field validation for 'Website' failed on the 'url' tag") + +	a.Website = "" +	err = validate.Struct(a) +	suite.NoError(err) +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationRedirectURI() { +	a := happyApplication() + +	a.RedirectURI = "invalid-uri" +	err := validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.RedirectURI' Error:Field validation for 'RedirectURI' failed on the 'uri' tag") + +	a.RedirectURI = "" +	err = validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.RedirectURI' Error:Field validation for 'RedirectURI' failed on the 'required' tag") + +	a.RedirectURI = "urn:ietf:wg:oauth:2.0:oob" +	err = validate.Struct(a) +	suite.NoError(err) +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationClientSecret() { +	a := happyApplication() + +	a.ClientSecret = "invalid-uuid" +	err := validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.ClientSecret' Error:Field validation for 'ClientSecret' failed on the 'uuid' tag") + +	a.ClientSecret = "" +	err = validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.ClientSecret' Error:Field validation for 'ClientSecret' failed on the 'required' tag") +} + +func (suite *ApplicationValidateTestSuite) TestValidateApplicationScopes() { +	a := happyApplication() + +	a.Scopes = "" +	err := validate.Struct(a) +	suite.EqualError(err, "Key: 'Application.Scopes' Error:Field validation for 'Scopes' failed on the 'required' tag") +} + +func TestApplicationValidateTestSuite(t *testing.T) { +	suite.Run(t, new(ApplicationValidateTestSuite)) +} diff --git a/internal/validate/block_test.go b/internal/validate/block_test.go new file mode 100644 index 000000000..6522e217b --- /dev/null +++ b/internal/validate/block_test.go @@ -0,0 +1,116 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyBlock() *gtsmodel.Block { +	return >smodel.Block{ +		ID:              "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:       time.Now(), +		UpdatedAt:       time.Now(), +		URI:             "https://example.org/accounts/someone/blocks/01FE91RJR88PSEEE30EV35QR8N", +		AccountID:       "01FEED79PRMVWPRMFHFQM8MJQN", +		Account:         nil, +		TargetAccountID: "01FEEDMF6C0QD589MRK7919Z0R", +		TargetAccount:   nil, +	} +} + +type BlockValidateTestSuite struct { +	suite.Suite +} + +func (suite *BlockValidateTestSuite) TestValidateBlockHappyPath() { +	// no problem here +	b := happyBlock() +	err := validate.Struct(b) +	suite.NoError(err) +} + +func (suite *BlockValidateTestSuite) TestValidateBlockBadID() { +	b := happyBlock() + +	b.ID = "" +	err := validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	b.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *BlockValidateTestSuite) TestValidateBlockNoCreatedAt() { +	b := happyBlock() + +	b.CreatedAt = time.Time{} +	err := validate.Struct(b) +	suite.NoError(err) +} + +func (suite *BlockValidateTestSuite) TestValidateBlockCreatedByAccountID() { +	b := happyBlock() + +	b.AccountID = "" +	err := validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag") + +	b.AccountID = "this-is-not-a-valid-ulid" +	err = validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'ulid' tag") +} + +func (suite *BlockValidateTestSuite) TestValidateBlockTargetAccountID() { +	b := happyBlock() + +	b.TargetAccountID = "invalid-ulid" +	err := validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'ulid' tag") + +	b.TargetAccountID = "01FEEDHX4G7EGHF5GD9E82Y51Q" +	err = validate.Struct(b) +	suite.NoError(err) + +	b.TargetAccountID = "" +	err = validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'required' tag") +} + +func (suite *BlockValidateTestSuite) TestValidateBlockURI() { +	b := happyBlock() + +	b.URI = "invalid-uri" +	err := validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.URI' Error:Field validation for 'URI' failed on the 'url' tag") + +	b.URI = "" +	err = validate.Struct(b) +	suite.EqualError(err, "Key: 'Block.URI' Error:Field validation for 'URI' failed on the 'required' tag") +} + +func TestBlockValidateTestSuite(t *testing.T) { +	suite.Run(t, new(BlockValidateTestSuite)) +} diff --git a/internal/validate/client_test.go b/internal/validate/client_test.go new file mode 100644 index 000000000..23deb3c39 --- /dev/null +++ b/internal/validate/client_test.go @@ -0,0 +1,102 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyClient() *gtsmodel.Client { +	return >smodel.Client{ +		ID:        "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt: time.Now(), +		UpdatedAt: time.Now(), +		Secret:    "bd740cf1-024a-4e4d-8c39-866538f52fe6", +		Domain:    "oauth2redirect://com.keylesspalace.tusky/", +		UserID:    "01FEEDMF6C0QD589MRK7919Z0R", +	} +} + +type ClientValidateTestSuite struct { +	suite.Suite +} + +func (suite *ClientValidateTestSuite) TestValidateClientHappyPath() { +	// no problem here +	c := happyClient() +	err := validate.Struct(c) +	suite.NoError(err) +} + +func (suite *ClientValidateTestSuite) TestValidateClientBadID() { +	c := happyClient() + +	c.ID = "" +	err := validate.Struct(c) +	suite.EqualError(err, "Key: 'Client.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	c.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(c) +	suite.EqualError(err, "Key: 'Client.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *ClientValidateTestSuite) TestValidateClientNoCreatedAt() { +	c := happyClient() + +	c.CreatedAt = time.Time{} +	err := validate.Struct(c) +	suite.NoError(err) +} + +func (suite *ClientValidateTestSuite) TestValidateClientDomain() { +	c := happyClient() + +	c.Domain = "invalid-uri" +	err := validate.Struct(c) +	suite.EqualError(err, "Key: 'Client.Domain' Error:Field validation for 'Domain' failed on the 'uri' tag") + +	c.Domain = "" +	err = validate.Struct(c) +	suite.EqualError(err, "Key: 'Client.Domain' Error:Field validation for 'Domain' failed on the 'required' tag") + +	c.Domain = "urn:ietf:wg:oauth:2.0:oob" +	err = validate.Struct(c) +	suite.NoError(err) +} + +func (suite *ClientValidateTestSuite) TestValidateSecret() { +	c := happyClient() + +	c.Secret = "invalid-uuid" +	err := validate.Struct(c) +	suite.EqualError(err, "Key: 'Client.Secret' Error:Field validation for 'Secret' failed on the 'uuid' tag") + +	c.Secret = "" +	err = validate.Struct(c) +	suite.EqualError(err, "Key: 'Client.Secret' Error:Field validation for 'Secret' failed on the 'required' tag") +} + +func TestClientValidateTestSuite(t *testing.T) { +	suite.Run(t, new(ClientValidateTestSuite)) +} diff --git a/internal/validate/domainblock_test.go b/internal/validate/domainblock_test.go new file mode 100644 index 000000000..4a0777677 --- /dev/null +++ b/internal/validate/domainblock_test.go @@ -0,0 +1,122 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyDomainBlock() *gtsmodel.DomainBlock { +	return >smodel.DomainBlock{ +		ID:                 "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:          time.Now(), +		UpdatedAt:          time.Now(), +		Domain:             "baddudes.suck", +		CreatedByAccountID: "01FEED79PRMVWPRMFHFQM8MJQN", +		PrivateComment:     "we don't like em", +		PublicComment:      "poo poo dudes", +		Obfuscate:          false, +		SubscriptionID:     "", +	} +} + +type DomainBlockValidateTestSuite struct { +	suite.Suite +} + +func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockHappyPath() { +	// no problem here +	d := happyDomainBlock() +	err := validate.Struct(d) +	suite.NoError(err) +} + +func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockBadID() { +	d := happyDomainBlock() + +	d.ID = "" +	err := validate.Struct(d) +	suite.EqualError(err, "Key: 'DomainBlock.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	d.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(d) +	suite.EqualError(err, "Key: 'DomainBlock.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockNoCreatedAt() { +	d := happyDomainBlock() + +	d.CreatedAt = time.Time{} +	err := validate.Struct(d) +	suite.NoError(err) +} + +func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockBadDomain() { +	d := happyDomainBlock() + +	d.Domain = "" +	err := validate.Struct(d) +	suite.EqualError(err, "Key: 'DomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'required' tag") + +	d.Domain = "this-is-not-a-valid-domain" +	err = validate.Struct(d) +	suite.EqualError(err, "Key: 'DomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") +} + +func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockCreatedByAccountID() { +	d := happyDomainBlock() + +	d.CreatedByAccountID = "" +	err := validate.Struct(d) +	suite.EqualError(err, "Key: 'DomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'required' tag") + +	d.CreatedByAccountID = "this-is-not-a-valid-ulid" +	err = validate.Struct(d) +	suite.EqualError(err, "Key: 'DomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'ulid' tag") +} + +func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockComments() { +	d := happyDomainBlock() + +	d.PrivateComment = "" +	d.PublicComment = "" +	err := validate.Struct(d) +	suite.NoError(err) +} + +func (suite *DomainBlockValidateTestSuite) TestValidateDomainSubscriptionID() { +	d := happyDomainBlock() + +	d.SubscriptionID = "invalid-ulid" +	err := validate.Struct(d) +	suite.EqualError(err, "Key: 'DomainBlock.SubscriptionID' Error:Field validation for 'SubscriptionID' failed on the 'ulid' tag") + +	d.SubscriptionID = "01FEEDHX4G7EGHF5GD9E82Y51Q" +	err = validate.Struct(d) +	suite.NoError(err) +} + +func TestDomainBlockValidateTestSuite(t *testing.T) { +	suite.Run(t, new(DomainBlockValidateTestSuite)) +} diff --git a/internal/validate/emaildomainblock_test.go b/internal/validate/emaildomainblock_test.go new file mode 100644 index 000000000..04e81ad86 --- /dev/null +++ b/internal/validate/emaildomainblock_test.go @@ -0,0 +1,97 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyEmailDomainBlock() *gtsmodel.EmailDomainBlock { +	return >smodel.EmailDomainBlock{ +		ID:                 "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:          time.Now(), +		UpdatedAt:          time.Now(), +		Domain:             "baddudes.suck", +		CreatedByAccountID: "01FEED79PRMVWPRMFHFQM8MJQN", +	} +} + +type EmailDomainBlockValidateTestSuite struct { +	suite.Suite +} + +func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockHappyPath() { +	// no problem here +	e := happyEmailDomainBlock() +	err := validate.Struct(e) +	suite.NoError(err) +} + +func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockBadID() { +	e := happyEmailDomainBlock() + +	e.ID = "" +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'EmailDomainBlock.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	e.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'EmailDomainBlock.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockNoCreatedAt() { +	e := happyEmailDomainBlock() + +	e.CreatedAt = time.Time{} +	err := validate.Struct(e) +	suite.NoError(err) +} + +func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockBadDomain() { +	e := happyEmailDomainBlock() + +	e.Domain = "" +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'EmailDomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'required' tag") + +	e.Domain = "this-is-not-a-valid-domain" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'EmailDomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") +} + +func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockCreatedByAccountID() { +	e := happyEmailDomainBlock() + +	e.CreatedByAccountID = "" +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'EmailDomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'required' tag") + +	e.CreatedByAccountID = "this-is-not-a-valid-ulid" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'EmailDomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'ulid' tag") +} + +func TestEmailDomainBlockValidateTestSuite(t *testing.T) { +	suite.Run(t, new(EmailDomainBlockValidateTestSuite)) +} diff --git a/internal/validate/emoji_test.go b/internal/validate/emoji_test.go new file mode 100644 index 000000000..9c42b7363 --- /dev/null +++ b/internal/validate/emoji_test.go @@ -0,0 +1,195 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"os" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyEmoji() *gtsmodel.Emoji { +	// the file validator actually runs os.Stat on given paths, so we need to just create small +	// temp files for both the main attachment file and the thumbnail + +	imageFile, err := os.CreateTemp("", "gts_test_emoji") +	if err != nil { +		panic(err) +	} +	if _, err := imageFile.WriteString("main"); err != nil { +		panic(err) +	} +	imagePath := imageFile.Name() +	if err := imageFile.Close(); err != nil { +		panic(err) +	} + +	staticFile, err := os.CreateTemp("", "gts_test_emoji_static") +	if err != nil { +		panic(err) +	} +	if _, err := staticFile.WriteString("thumbnail"); err != nil { +		panic(err) +	} +	imageStaticPath := staticFile.Name() +	if err := staticFile.Close(); err != nil { +		panic(err) +	} + +	return >smodel.Emoji{ +		ID:                     "01F8MH6NEM8D7527KZAECTCR76", +		CreatedAt:              time.Now().Add(-71 * time.Hour), +		UpdatedAt:              time.Now().Add(-71 * time.Hour), +		Shortcode:              "blob_test", +		Domain:                 "example.org", +		ImageRemoteURL:         "https://example.org/emojis/blob_test.gif", +		ImageStaticRemoteURL:   "https://example.org/emojis/blob_test.png", +		ImageURL:               "", +		ImageStaticURL:         "", +		ImagePath:              imagePath, +		ImageStaticPath:        imageStaticPath, +		ImageContentType:       "image/gif", +		ImageStaticContentType: "image/png", +		ImageFileSize:          1024, +		ImageStaticFileSize:    256, +		ImageUpdatedAt:         time.Now(), +		Disabled:               false, +		URI:                    "https://example.org/emojis/blob_test", +		VisibleInPicker:        true, +		CategoryID:             "01FEE47ZH70PWDSEAVBRFNX325", +	} +} + +type EmojiValidateTestSuite struct { +	suite.Suite +} + +func (suite *EmojiValidateTestSuite) TestValidateEmojiHappyPath() { +	// no problem here +	m := happyEmoji() +	err := validate.Struct(*m) +	suite.NoError(err) +} + +func (suite *EmojiValidateTestSuite) TestValidateEmojiBadFilePaths() { +	e := happyEmoji() + +	e.ImagePath = "/tmp/nonexistent/file/for/gotosocial/test" +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'file' tag") + +	e.ImagePath = "" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'required' tag") + +	e.ImagePath = "???????????thisnot a valid path####" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'file' tag") + +	e.ImageStaticPath = "/tmp/nonexistent/file/for/gotosocial/test" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'file' tag\nKey: 'Emoji.ImageStaticPath' Error:Field validation for 'ImageStaticPath' failed on the 'file' tag") + +	e.ImageStaticPath = "" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'file' tag\nKey: 'Emoji.ImageStaticPath' Error:Field validation for 'ImageStaticPath' failed on the 'required' tag") + +	e.ImageStaticPath = "???????????thisnot a valid path####" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'file' tag\nKey: 'Emoji.ImageStaticPath' Error:Field validation for 'ImageStaticPath' failed on the 'file' tag") +} + +func (suite *EmojiValidateTestSuite) TestValidateEmojiURI() { +	e := happyEmoji() + +	e.URI = "aaaaaaaaaa" +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.URI' Error:Field validation for 'URI' failed on the 'url' tag") + +	e.URI = "" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.URI' Error:Field validation for 'URI' failed on the 'url' tag") +} + +func (suite *EmojiValidateTestSuite) TestValidateEmojiURLCombos() { +	e := happyEmoji() + +	e.ImageRemoteURL = "" +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageRemoteURL' Error:Field validation for 'ImageRemoteURL' failed on the 'required_without' tag\nKey: 'Emoji.ImageURL' Error:Field validation for 'ImageURL' failed on the 'required_without' tag") + +	e.ImageURL = "https://whatever.org" +	err = validate.Struct(e) +	suite.NoError(err) + +	e.ImageStaticRemoteURL = "" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageStaticRemoteURL' Error:Field validation for 'ImageStaticRemoteURL' failed on the 'required_without' tag\nKey: 'Emoji.ImageStaticURL' Error:Field validation for 'ImageStaticURL' failed on the 'required_without' tag") + +	e.ImageStaticURL = "https://whatever.org" +	err = validate.Struct(e) +	suite.NoError(err) + +	e.ImageURL = "" +	e.ImageStaticURL = "" +	e.ImageRemoteURL = "" +	e.ImageStaticRemoteURL = "" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageRemoteURL' Error:Field validation for 'ImageRemoteURL' failed on the 'required_without' tag\nKey: 'Emoji.ImageStaticRemoteURL' Error:Field validation for 'ImageStaticRemoteURL' failed on the 'required_without' tag\nKey: 'Emoji.ImageURL' Error:Field validation for 'ImageURL' failed on the 'required_without' tag\nKey: 'Emoji.ImageStaticURL' Error:Field validation for 'ImageStaticURL' failed on the 'required_without' tag") +} + +func (suite *EmojiValidateTestSuite) TestValidateFileSize() { +	e := happyEmoji() + +	e.ImageFileSize = 0 +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageFileSize' Error:Field validation for 'ImageFileSize' failed on the 'required' tag") + +	e.ImageStaticFileSize = 0 +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageFileSize' Error:Field validation for 'ImageFileSize' failed on the 'required' tag\nKey: 'Emoji.ImageStaticFileSize' Error:Field validation for 'ImageStaticFileSize' failed on the 'required' tag") + +	e.ImageFileSize = -1 +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageFileSize' Error:Field validation for 'ImageFileSize' failed on the 'min' tag\nKey: 'Emoji.ImageStaticFileSize' Error:Field validation for 'ImageStaticFileSize' failed on the 'required' tag") + +	e.ImageStaticFileSize = -1 +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageFileSize' Error:Field validation for 'ImageFileSize' failed on the 'min' tag\nKey: 'Emoji.ImageStaticFileSize' Error:Field validation for 'ImageStaticFileSize' failed on the 'min' tag") +} + +func (suite *EmojiValidateTestSuite) TestValidateDomain() { +	e := happyEmoji() + +	e.Domain = "" +	err := validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.ImageURL' Error:Field validation for 'ImageURL' failed on the 'required_without' tag\nKey: 'Emoji.ImageStaticURL' Error:Field validation for 'ImageStaticURL' failed on the 'required_without' tag") + +	e.Domain = "aaaaaaaaa" +	err = validate.Struct(e) +	suite.EqualError(err, "Key: 'Emoji.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") +} + +func TestEmojiValidateTestSuite(t *testing.T) { +	suite.Run(t, new(EmojiValidateTestSuite)) +} diff --git a/internal/validate/follow_test.go b/internal/validate/follow_test.go new file mode 100644 index 000000000..840f805bf --- /dev/null +++ b/internal/validate/follow_test.go @@ -0,0 +1,88 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyFollow() *gtsmodel.Follow { +	return >smodel.Follow{ +		ID:              "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:       time.Now(), +		UpdatedAt:       time.Now(), +		AccountID:       "01FE96MAE58MXCE5C4SSMEMCEK", +		Account:         nil, +		TargetAccountID: "01FE96MXRHWZHKC0WH5FT82H1A", +		TargetAccount:   nil, +		URI:             "https://example.org/users/user1/activity/follow/01FE91RJR88PSEEE30EV35QR8N", +	} +} + +type FollowValidateTestSuite struct { +	suite.Suite +} + +func (suite *FollowValidateTestSuite) TestValidateFollowHappyPath() { +	// no problem here +	f := happyFollow() +	err := validate.Struct(f) +	suite.NoError(err) +} + +func (suite *FollowValidateTestSuite) TestValidateFollowBadID() { +	f := happyFollow() + +	f.ID = "" +	err := validate.Struct(f) +	suite.EqualError(err, "Key: 'Follow.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	f.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(f) +	suite.EqualError(err, "Key: 'Follow.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *FollowValidateTestSuite) TestValidateFollowNoCreatedAt() { +	f := happyFollow() + +	f.CreatedAt = time.Time{} +	err := validate.Struct(f) +	suite.NoError(err) +} + +func (suite *FollowValidateTestSuite) TestValidateFollowNoURI() { +	f := happyFollow() + +	f.URI = "" +	err := validate.Struct(f) +	suite.EqualError(err, "Key: 'Follow.URI' Error:Field validation for 'URI' failed on the 'required' tag") + +	f.URI = "this-is-not-a-valid-url" +	err = validate.Struct(f) +	suite.EqualError(err, "Key: 'Follow.URI' Error:Field validation for 'URI' failed on the 'url' tag") +} + +func TestFollowValidateTestSuite(t *testing.T) { +	suite.Run(t, new(FollowValidateTestSuite)) +} diff --git a/internal/validate/followrequest_test.go b/internal/validate/followrequest_test.go new file mode 100644 index 000000000..24744fb53 --- /dev/null +++ b/internal/validate/followrequest_test.go @@ -0,0 +1,88 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyFollowRequest() *gtsmodel.FollowRequest { +	return >smodel.FollowRequest{ +		ID:              "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:       time.Now(), +		UpdatedAt:       time.Now(), +		AccountID:       "01FE96MAE58MXCE5C4SSMEMCEK", +		Account:         nil, +		TargetAccountID: "01FE96MXRHWZHKC0WH5FT82H1A", +		TargetAccount:   nil, +		URI:             "https://example.org/users/user1/activity/follow/01FE91RJR88PSEEE30EV35QR8N", +	} +} + +type FollowRequestValidateTestSuite struct { +	suite.Suite +} + +func (suite *FollowRequestValidateTestSuite) TestValidateFollowRequestHappyPath() { +	// no problem here +	f := happyFollowRequest() +	err := validate.Struct(f) +	suite.NoError(err) +} + +func (suite *FollowRequestValidateTestSuite) TestValidateFollowRequestBadID() { +	f := happyFollowRequest() + +	f.ID = "" +	err := validate.Struct(f) +	suite.EqualError(err, "Key: 'FollowRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	f.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(f) +	suite.EqualError(err, "Key: 'FollowRequest.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *FollowRequestValidateTestSuite) TestValidateFollowRequestNoCreatedAt() { +	f := happyFollowRequest() + +	f.CreatedAt = time.Time{} +	err := validate.Struct(f) +	suite.NoError(err) +} + +func (suite *FollowRequestValidateTestSuite) TestValidateFollowRequestNoURI() { +	f := happyFollowRequest() + +	f.URI = "" +	err := validate.Struct(f) +	suite.EqualError(err, "Key: 'FollowRequest.URI' Error:Field validation for 'URI' failed on the 'required' tag") + +	f.URI = "this-is-not-a-valid-url" +	err = validate.Struct(f) +	suite.EqualError(err, "Key: 'FollowRequest.URI' Error:Field validation for 'URI' failed on the 'url' tag") +} + +func TestFollowRequestValidateTestSuite(t *testing.T) { +	suite.Run(t, new(FollowRequestValidateTestSuite)) +} diff --git a/internal/util/validation.go b/internal/validate/formvalidation.go index 446f7a70e..9f61578e7 100644 --- a/internal/util/validation.go +++ b/internal/validate/formvalidation.go @@ -16,13 +16,14 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package util +package validate  import (  	"errors"  	"fmt"  	"net/mail" +	"github.com/superseriousbusiness/gotosocial/internal/regexes"  	pwv "github.com/wagslane/go-password-validator"  	"golang.org/x/text/language"  ) @@ -36,10 +37,13 @@ const (  	maximumShortDescriptionLength = 500  	maximumDescriptionLength      = 5000  	maximumSiteTermsLength        = 5000 +	maximumUsernameLength         = 64 +	// maximumEmojiShortcodeLength   = 30 +	// maximumHashtagLength          = 30  ) -// ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. -func ValidateNewPassword(password string) error { +// NewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. +func NewPassword(password string) error {  	if password == "" {  		return errors.New("no password provided")  	} @@ -51,23 +55,23 @@ func ValidateNewPassword(password string) error {  	return pwv.Validate(password, minimumPasswordEntropy)  } -// ValidateUsername makes sure that a given username is valid (ie., letters, numbers, underscores, check length). +// Username makes sure that a given username is valid (ie., letters, numbers, underscores, check length).  // Returns an error if not. -func ValidateUsername(username string) error { +func Username(username string) error {  	if username == "" {  		return errors.New("no username provided")  	} -	if !usernameValidationRegex.MatchString(username) { +	if !regexes.Username.MatchString(username) {  		return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max %d characters", username, maximumUsernameLength)  	}  	return nil  } -// ValidateEmail makes sure that a given email address is a valid address. +// Email makes sure that a given email address is a valid address.  // Returns an error if not. -func ValidateEmail(email string) error { +func Email(email string) error {  	if email == "" {  		return errors.New("no email provided")  	} @@ -76,9 +80,9 @@ func ValidateEmail(email string) error {  	return err  } -// ValidateLanguage checks that the given language string is a 2- or 3-letter ISO 639 code. +// Language checks that the given language string is a 2- or 3-letter ISO 639 code.  // Returns an error if the language cannot be parsed. See: https://pkg.go.dev/golang.org/x/text/language -func ValidateLanguage(lang string) error { +func Language(lang string) error {  	if lang == "" {  		return errors.New("no language provided")  	} @@ -86,8 +90,8 @@ func ValidateLanguage(lang string) error {  	return err  } -// ValidateSignUpReason checks that a sufficient reason is given for a server signup request -func ValidateSignUpReason(reason string, reasonRequired bool) error { +// SignUpReason checks that a sufficient reason is given for a server signup request +func SignUpReason(reason string, reasonRequired bool) error {  	if !reasonRequired {  		// we don't care!  		// we're not going to do anything with this text anyway if no reason is required @@ -108,36 +112,36 @@ func ValidateSignUpReason(reason string, reasonRequired bool) error {  	return nil  } -// ValidateDisplayName checks that a requested display name is valid -func ValidateDisplayName(displayName string) error { +// DisplayName checks that a requested display name is valid +func DisplayName(displayName string) error {  	// TODO: add some validation logic here -- length, characters, etc  	return nil  } -// ValidateNote checks that a given profile/account note/bio is valid -func ValidateNote(note string) error { +// Note checks that a given profile/account note/bio is valid +func Note(note string) error {  	// TODO: add some validation logic here -- length, characters, etc  	return nil  } -// ValidatePrivacy checks that the desired privacy setting is valid -func ValidatePrivacy(privacy string) error { +// Privacy checks that the desired privacy setting is valid +func Privacy(privacy string) error {  	// TODO: add some validation logic here -- length, characters, etc  	return nil  } -// ValidateEmojiShortcode just runs the given shortcode through the regular expression +// EmojiShortcode just runs the given shortcode through the regular expression  // for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters,  // lowercase a-z, numbers, and underscores. -func ValidateEmojiShortcode(shortcode string) error { -	if !emojiShortcodeValidationRegex.MatchString(shortcode) { +func EmojiShortcode(shortcode string) error { +	if !regexes.EmojiShortcode.MatchString(shortcode) {  		return fmt.Errorf("shortcode %s did not pass validation, must be between 2 and 30 characters, lowercase letters, numbers, and underscores only", shortcode)  	}  	return nil  } -// ValidateSiteTitle ensures that the given site title is within spec. -func ValidateSiteTitle(siteTitle string) error { +// SiteTitle ensures that the given site title is within spec. +func SiteTitle(siteTitle string) error {  	if len(siteTitle) > maximumSiteTitleLength {  		return fmt.Errorf("site title should be no more than %d chars but given title was %d", maximumSiteTitleLength, len(siteTitle))  	} @@ -145,8 +149,8 @@ func ValidateSiteTitle(siteTitle string) error {  	return nil  } -// ValidateSiteShortDescription ensures that the given site short description is within spec. -func ValidateSiteShortDescription(d string) error { +// SiteShortDescription ensures that the given site short description is within spec. +func SiteShortDescription(d string) error {  	if len(d) > maximumShortDescriptionLength {  		return fmt.Errorf("short description should be no more than %d chars but given description was %d", maximumShortDescriptionLength, len(d))  	} @@ -154,8 +158,8 @@ func ValidateSiteShortDescription(d string) error {  	return nil  } -// ValidateSiteDescription ensures that the given site description is within spec. -func ValidateSiteDescription(d string) error { +// SiteDescription ensures that the given site description is within spec. +func SiteDescription(d string) error {  	if len(d) > maximumDescriptionLength {  		return fmt.Errorf("description should be no more than %d chars but given description was %d", maximumDescriptionLength, len(d))  	} @@ -163,11 +167,16 @@ func ValidateSiteDescription(d string) error {  	return nil  } -// ValidateSiteTerms ensures that the given site terms string is within spec. -func ValidateSiteTerms(t string) error { +// SiteTerms ensures that the given site terms string is within spec. +func SiteTerms(t string) error {  	if len(t) > maximumSiteTermsLength {  		return fmt.Errorf("terms should be no more than %d chars but given terms was %d", maximumSiteTermsLength, len(t))  	}  	return nil  } + +// ULID returns true if the passed string is a valid ULID. +func ULID(i string) bool { +	return regexes.ULID.MatchString(i) +} diff --git a/internal/util/validation_test.go b/internal/validate/formvalidation_test.go index 639a89bbd..03bed42d8 100644 --- a/internal/util/validation_test.go +++ b/internal/validate/formvalidation_test.go @@ -16,7 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package util_test +package validate_test  import (  	"errors" @@ -25,7 +25,7 @@ import (  	"github.com/stretchr/testify/assert"  	"github.com/stretchr/testify/suite" -	"github.com/superseriousbusiness/gotosocial/internal/util" +	"github.com/superseriousbusiness/gotosocial/internal/validate"  )  type ValidationTestSuite struct { @@ -43,42 +43,42 @@ func (suite *ValidationTestSuite) TestCheckPasswordStrength() {  	strongPassword := "3dX5@Zc%mV*W2MBNEy$@"  	var err error -	err = util.ValidateNewPassword(empty) +	err = validate.NewPassword(empty)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("no password provided"), err)  	} -	err = util.ValidateNewPassword(terriblePassword) +	err = validate.NewPassword(terriblePassword)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"), err)  	} -	err = util.ValidateNewPassword(weakPassword) +	err = validate.NewPassword(weakPassword)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("insecure password, try including more special characters, using numbers or using a longer password"), err)  	} -	err = util.ValidateNewPassword(shortPassword) +	err = validate.NewPassword(shortPassword)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("insecure password, try including more special characters or using a longer password"), err)  	} -	err = util.ValidateNewPassword(specialPassword) +	err = validate.NewPassword(specialPassword)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("insecure password, try including more special characters or using a longer password"), err)  	} -	err = util.ValidateNewPassword(longPassword) +	err = validate.NewPassword(longPassword)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateNewPassword(tooLong) +	err = validate.NewPassword(tooLong)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("password should be no more than 64 chars"), err)  	} -	err = util.ValidateNewPassword(strongPassword) +	err = validate.NewPassword(strongPassword)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} @@ -95,42 +95,42 @@ func (suite *ValidationTestSuite) TestValidateUsername() {  	goodUsername := "this_is_a_good_username"  	var err error -	err = util.ValidateUsername(empty) +	err = validate.Username(empty)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("no username provided"), err)  	} -	err = util.ValidateUsername(tooLong) +	err = validate.Username(tooLong)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", tooLong), err)  	} -	err = util.ValidateUsername(withSpaces) +	err = validate.Username(withSpaces)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", withSpaces), err)  	} -	err = util.ValidateUsername(weirdChars) +	err = validate.Username(weirdChars)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", weirdChars), err)  	} -	err = util.ValidateUsername(leadingSpace) +	err = validate.Username(leadingSpace)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", leadingSpace), err)  	} -	err = util.ValidateUsername(trailingSpace) +	err = validate.Username(trailingSpace)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", trailingSpace), err)  	} -	err = util.ValidateUsername(newlines) +	err = validate.Username(newlines)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", newlines), err)  	} -	err = util.ValidateUsername(goodUsername) +	err = validate.Username(goodUsername)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} @@ -144,27 +144,27 @@ func (suite *ValidationTestSuite) TestValidateEmail() {  	emailAddress := "thisis.actually@anemail.address"  	var err error -	err = util.ValidateEmail(empty) +	err = validate.Email(empty)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("no email provided"), err)  	} -	err = util.ValidateEmail(notAnEmailAddress) +	err = validate.Email(notAnEmailAddress)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err)  	} -	err = util.ValidateEmail(almostAnEmailAddress) +	err = validate.Email(almostAnEmailAddress)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("mail: no angle-addr"), err)  	} -	err = util.ValidateEmail(aWebsite) +	err = validate.Email(aWebsite)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err)  	} -	err = util.ValidateEmail(emailAddress) +	err = validate.Email(emailAddress)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} @@ -182,47 +182,47 @@ func (suite *ValidationTestSuite) TestValidateLanguage() {  	german := "de"  	var err error -	err = util.ValidateLanguage(empty) +	err = validate.Language(empty)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("no language provided"), err)  	} -	err = util.ValidateLanguage(notALanguage) +	err = validate.Language(notALanguage)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err)  	} -	err = util.ValidateLanguage(english) +	err = validate.Language(english)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateLanguage(capitalEnglish) +	err = validate.Language(capitalEnglish)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateLanguage(arabic3Letters) +	err = validate.Language(arabic3Letters)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateLanguage(mixedCapsEnglish) +	err = validate.Language(mixedCapsEnglish)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateLanguage(englishUS) +	err = validate.Language(englishUS)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err)  	} -	err = util.ValidateLanguage(dutch) +	err = validate.Language(dutch)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateLanguage(german) +	err = validate.Language(german)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} @@ -236,43 +236,43 @@ func (suite *ValidationTestSuite) TestValidateReason() {  	var err error  	// check with no reason required -	err = util.ValidateSignUpReason(empty, false) +	err = validate.SignUpReason(empty, false)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateSignUpReason(badReason, false) +	err = validate.SignUpReason(badReason, false)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateSignUpReason(tooLong, false) +	err = validate.SignUpReason(tooLong, false)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} -	err = util.ValidateSignUpReason(goodReason, false) +	err = validate.SignUpReason(goodReason, false)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	}  	// check with reason required -	err = util.ValidateSignUpReason(empty, true) +	err = validate.SignUpReason(empty, true)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("no reason provided"), err)  	} -	err = util.ValidateSignUpReason(badReason, true) +	err = validate.SignUpReason(badReason, true)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("reason should be at least 40 chars but 'because' was 7"), err)  	} -	err = util.ValidateSignUpReason(tooLong, true) +	err = validate.SignUpReason(tooLong, true)  	if assert.Error(suite.T(), err) {  		assert.Equal(suite.T(), errors.New("reason should be no more than 500 chars but given reason was 600"), err)  	} -	err = util.ValidateSignUpReason(goodReason, true) +	err = validate.SignUpReason(goodReason, true)  	if assert.NoError(suite.T(), err) {  		assert.Equal(suite.T(), nil, err)  	} diff --git a/internal/validate/instance_test.go b/internal/validate/instance_test.go new file mode 100644 index 000000000..e2b569363 --- /dev/null +++ b/internal/validate/instance_test.go @@ -0,0 +1,146 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyInstance() *gtsmodel.Instance { +	return >smodel.Instance{ +		ID:                     "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:              time.Now(), +		UpdatedAt:              time.Now(), +		Domain:                 "example.org", +		Title:                  "Example Instance", +		URI:                    "https://example.org", +		SuspendedAt:            time.Time{}, +		DomainBlockID:          "", +		DomainBlock:            nil, +		ShortDescription:       "This is a description for the example/testing instance.", +		Description:            "This is a way longer description for the example/testing instance!", +		Terms:                  "Don't be a knobhead.", +		ContactEmail:           "admin@example.org", +		ContactAccountUsername: "admin", +		ContactAccountID:       "01FEE20H5QWHJDEXAEE9G96PR0", +		ContactAccount:         nil, +		Reputation:             420, +		Version:                "gotosocial 0.1.0", +	} +} + +type InstanceValidateTestSuite struct { +	suite.Suite +} + +func (suite *InstanceValidateTestSuite) TestValidateInstanceHappyPath() { +	// no problem here +	m := happyInstance() +	err := validate.Struct(*m) +	suite.NoError(err) +} + +func (suite *InstanceValidateTestSuite) TestValidateInstanceBadID() { +	m := happyInstance() + +	m.ID = "" +	err := validate.Struct(*m) +	suite.EqualError(err, "Key: 'Instance.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(*m) +	suite.EqualError(err, "Key: 'Instance.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *InstanceValidateTestSuite) TestValidateInstanceAccountURI() { +	i := happyInstance() + +	i.URI = "" +	err := validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.URI' Error:Field validation for 'URI' failed on the 'required' tag") + +	i.URI = "---------------------------" +	err = validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.URI' Error:Field validation for 'URI' failed on the 'url' tag") +} + +func (suite *InstanceValidateTestSuite) TestValidateInstanceDodgyAccountID() { +	i := happyInstance() + +	i.ContactAccountID = "9HZJ76B6VXSKF" +	err := validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.ContactAccountID' Error:Field validation for 'ContactAccountID' failed on the 'ulid' tag") + +	i.ContactAccountID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" +	err = validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.ContactAccountID' Error:Field validation for 'ContactAccountID' failed on the 'ulid' tag") + +	i.ContactAccountID = "" +	err = validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.ContactAccountID' Error:Field validation for 'ContactAccountID' failed on the 'required_with' tag") + +	i.ContactAccountUsername = "" +	err = validate.Struct(i) +	suite.NoError(err) +} + +func (suite *InstanceValidateTestSuite) TestValidateInstanceDomain() { +	i := happyInstance() + +	i.Domain = "poopoo" +	err := validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") + +	i.Domain = "" +	err = validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.Domain' Error:Field validation for 'Domain' failed on the 'required' tag") + +	i.Domain = "https://aaaaaaaaaaaaah.org" +	err = validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") +} + +func (suite *InstanceValidateTestSuite) TestValidateInstanceContactEmail() { +	i := happyInstance() + +	i.ContactEmail = "poopoo" +	err := validate.Struct(i) +	suite.EqualError(err, "Key: 'Instance.ContactEmail' Error:Field validation for 'ContactEmail' failed on the 'email' tag") + +	i.ContactEmail = "" +	err = validate.Struct(i) +	suite.NoError(err) +} + +func (suite *InstanceValidateTestSuite) TestValidateInstanceNoCreatedAt() { +	i := happyInstance() + +	i.CreatedAt = time.Time{} +	err := validate.Struct(i) +	suite.NoError(err) +} + +func TestInstanceValidateTestSuite(t *testing.T) { +	suite.Run(t, new(InstanceValidateTestSuite)) +} diff --git a/internal/validate/mediaattachment_test.go b/internal/validate/mediaattachment_test.go new file mode 100644 index 000000000..58108fc42 --- /dev/null +++ b/internal/validate/mediaattachment_test.go @@ -0,0 +1,230 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"os" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyMediaAttachment() *gtsmodel.MediaAttachment { +	// the file validator actually runs os.Stat on given paths, so we need to just create small +	// temp files for both the main attachment file and the thumbnail + +	mainFile, err := os.CreateTemp("", "gts_test_mainfile") +	if err != nil { +		panic(err) +	} +	if _, err := mainFile.WriteString("main"); err != nil { +		panic(err) +	} +	mainPath := mainFile.Name() +	if err := mainFile.Close(); err != nil { +		panic(err) +	} + +	thumbnailFile, err := os.CreateTemp("", "gts_test_thumbnail") +	if err != nil { +		panic(err) +	} +	if _, err := thumbnailFile.WriteString("thumbnail"); err != nil { +		panic(err) +	} +	thumbnailPath := thumbnailFile.Name() +	if err := thumbnailFile.Close(); err != nil { +		panic(err) +	} + +	return >smodel.MediaAttachment{ +		ID:        "01F8MH6NEM8D7527KZAECTCR76", +		CreatedAt: time.Now().Add(-71 * time.Hour), +		UpdatedAt: time.Now().Add(-71 * time.Hour), +		StatusID:  "01F8MH75CBF9JFX4ZAD54N0W0R", +		URL:       "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg", +		RemoteURL: "", +		Type:      gtsmodel.FileTypeImage, +		FileMeta: gtsmodel.FileMeta{ +			Original: gtsmodel.Original{ +				Width:  1200, +				Height: 630, +				Size:   756000, +				Aspect: 1.9047619047619047, +			}, +			Small: gtsmodel.Small{ +				Width:  256, +				Height: 134, +				Size:   34304, +				Aspect: 1.9104477611940298, +			}, +		}, +		AccountID:         "01F8MH17FWEB39HZJ76B6VXSKF", +		Description:       "Black and white image of some 50's style text saying: Welcome On Board", +		ScheduledStatusID: "", +		Blurhash:          "LNJRdVM{00Rj%Mayt7j[4nWBofRj", +		Processing:        2, +		File: gtsmodel.File{ +			Path:        mainPath, +			ContentType: "image/jpeg", +			FileSize:    62529, +			UpdatedAt:   time.Now().Add(-71 * time.Hour), +		}, +		Thumbnail: gtsmodel.Thumbnail{ +			Path:        thumbnailPath, +			ContentType: "image/jpeg", +			FileSize:    6872, +			UpdatedAt:   time.Now().Add(-71 * time.Hour), +			URL:         "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpeg", +			RemoteURL:   "", +		}, +		Avatar: false, +		Header: false, +	} +} + +type MediaAttachmentValidateTestSuite struct { +	suite.Suite +} + +func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentHappyPath() { +	// no problem here +	m := happyMediaAttachment() +	err := validate.Struct(m) +	suite.NoError(err) +} + +func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentBadFilePaths() { +	m := happyMediaAttachment() + +	m.File.Path = "/tmp/nonexistent/file/for/gotosocial/test" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.File.Path' Error:Field validation for 'Path' failed on the 'file' tag") + +	m.File.Path = "" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.File.Path' Error:Field validation for 'Path' failed on the 'required' tag") + +	m.File.Path = "???????????thisnot a valid path####" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.File.Path' Error:Field validation for 'Path' failed on the 'file' tag") + +	m.Thumbnail.Path = "/tmp/nonexistent/file/for/gotosocial/test" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.File.Path' Error:Field validation for 'Path' failed on the 'file' tag\nKey: 'MediaAttachment.Thumbnail.Path' Error:Field validation for 'Path' failed on the 'file' tag") + +	m.Thumbnail.Path = "" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.File.Path' Error:Field validation for 'Path' failed on the 'file' tag\nKey: 'MediaAttachment.Thumbnail.Path' Error:Field validation for 'Path' failed on the 'required' tag") + +	m.Thumbnail.Path = "???????????thisnot a valid path####" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.File.Path' Error:Field validation for 'Path' failed on the 'file' tag\nKey: 'MediaAttachment.Thumbnail.Path' Error:Field validation for 'Path' failed on the 'file' tag") +} + +func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentBadType() { +	m := happyMediaAttachment() + +	m.Type = "" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.Type' Error:Field validation for 'Type' failed on the 'oneof' tag") + +	m.Type = "Not Supported" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.Type' Error:Field validation for 'Type' failed on the 'oneof' tag") +} + +func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentBadFileMeta() { +	m := happyMediaAttachment() + +	m.FileMeta.Original.Aspect = 0 +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.FileMeta.Original.Aspect' Error:Field validation for 'Aspect' failed on the 'required_with' tag") + +	m.FileMeta.Original.Height = 0 +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.FileMeta.Original.Height' Error:Field validation for 'Height' failed on the 'required_with' tag\nKey: 'MediaAttachment.FileMeta.Original.Aspect' Error:Field validation for 'Aspect' failed on the 'required_with' tag") + +	m.FileMeta.Original = gtsmodel.Original{} +	err = validate.Struct(m) +	suite.NoError(err) + +	m.FileMeta.Focus.X = 3.6 +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.FileMeta.Focus.X' Error:Field validation for 'X' failed on the 'max' tag") + +	m.FileMeta.Focus.Y = -50 +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.FileMeta.Focus.X' Error:Field validation for 'X' failed on the 'max' tag\nKey: 'MediaAttachment.FileMeta.Focus.Y' Error:Field validation for 'Y' failed on the 'min' tag") +} + +func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentBadURLCombos() { +	m := happyMediaAttachment() + +	m.URL = "aaaaaaaaaa" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.URL' Error:Field validation for 'URL' failed on the 'url' tag") + +	m.URL = "" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.URL' Error:Field validation for 'URL' failed on the 'required_without' tag\nKey: 'MediaAttachment.RemoteURL' Error:Field validation for 'RemoteURL' failed on the 'required_without' tag") + +	m.RemoteURL = "oooooooooo" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.RemoteURL' Error:Field validation for 'RemoteURL' failed on the 'url' tag") + +	m.RemoteURL = "https://a-valid-url.gay" +	err = validate.Struct(m) +	suite.NoError(err) +} + +func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentBlurhash() { +	m := happyMediaAttachment() + +	m.Blurhash = "" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.Blurhash' Error:Field validation for 'Blurhash' failed on the 'required_if' tag") + +	m.Type = gtsmodel.FileTypeAudio +	err = validate.Struct(m) +	suite.NoError(err) + +	m.Blurhash = "some_blurhash" +	err = validate.Struct(m) +	suite.NoError(err) +} + +func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentProcessing() { +	m := happyMediaAttachment() + +	m.Processing = 420 +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.Processing' Error:Field validation for 'Processing' failed on the 'oneof' tag") + +	m.Processing = -5 +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'MediaAttachment.Processing' Error:Field validation for 'Processing' failed on the 'oneof' tag") +} + +func TestMediaAttachmentValidateTestSuite(t *testing.T) { +	suite.Run(t, new(MediaAttachmentValidateTestSuite)) +} diff --git a/internal/validate/mention_test.go b/internal/validate/mention_test.go new file mode 100644 index 000000000..1b460803c --- /dev/null +++ b/internal/validate/mention_test.go @@ -0,0 +1,102 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyMention() *gtsmodel.Mention { +	return >smodel.Mention{ +		ID:               "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:        time.Now(), +		UpdatedAt:        time.Now(), +		OriginAccountID:  "01FE96MAE58MXCE5C4SSMEMCEK", +		OriginAccountURI: "https://some-instance/accounts/bleepbloop", +		OriginAccount:    nil, +		TargetAccountID:  "01FE96MXRHWZHKC0WH5FT82H1A", +		TargetAccount:    nil, +		StatusID:         "01FE96NBPNJNY26730FT6GZTFE", +		Status:           nil, +	} +} + +type MentionValidateTestSuite struct { +	suite.Suite +} + +func (suite *MentionValidateTestSuite) TestValidateMentionHappyPath() { +	// no problem here +	m := happyMention() +	err := validate.Struct(m) +	suite.NoError(err) +} + +func (suite *MentionValidateTestSuite) TestValidateMentionBadID() { +	m := happyMention() + +	m.ID = "" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'Mention.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'Mention.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *MentionValidateTestSuite) TestValidateMentionAccountURI() { +	m := happyMention() + +	m.OriginAccountURI = "" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'Mention.OriginAccountURI' Error:Field validation for 'OriginAccountURI' failed on the 'url' tag") + +	m.OriginAccountURI = "---------------------------" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'Mention.OriginAccountURI' Error:Field validation for 'OriginAccountURI' failed on the 'url' tag") +} + +func (suite *MentionValidateTestSuite) TestValidateMentionDodgyStatusID() { +	m := happyMention() + +	m.StatusID = "9HZJ76B6VXSKF" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'Mention.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + +	m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'Mention.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") +} + +func (suite *MentionValidateTestSuite) TestValidateMentionNoCreatedAt() { +	m := happyMention() + +	m.CreatedAt = time.Time{} +	err := validate.Struct(m) +	suite.NoError(err) +} + +func TestMentionValidateTestSuite(t *testing.T) { +	suite.Run(t, new(MentionValidateTestSuite)) +} diff --git a/internal/validate/notification_test.go b/internal/validate/notification_test.go new file mode 100644 index 000000000..bca1ac9fe --- /dev/null +++ b/internal/validate/notification_test.go @@ -0,0 +1,98 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyNotification() *gtsmodel.Notification { +	return >smodel.Notification{ +		ID:               "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:        time.Now(), +		NotificationType: gtsmodel.NotificationFave, +		OriginAccountID:  "01FE96MAE58MXCE5C4SSMEMCEK", +		OriginAccount:    nil, +		TargetAccountID:  "01FE96MXRHWZHKC0WH5FT82H1A", +		TargetAccount:    nil, +		StatusID:         "01FE96NBPNJNY26730FT6GZTFE", +		Status:           nil, +	} +} + +type NotificationValidateTestSuite struct { +	suite.Suite +} + +func (suite *NotificationValidateTestSuite) TestValidateNotificationHappyPath() { +	// no problem here +	n := happyNotification() +	err := validate.Struct(n) +	suite.NoError(err) +} + +func (suite *NotificationValidateTestSuite) TestValidateNotificationBadID() { +	n := happyNotification() + +	n.ID = "" +	err := validate.Struct(n) +	suite.EqualError(err, "Key: 'Notification.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	n.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(n) +	suite.EqualError(err, "Key: 'Notification.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *NotificationValidateTestSuite) TestValidateNotificationStatusID() { +	n := happyNotification() + +	n.StatusID = "" +	err := validate.Struct(n) +	suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'required_if' tag") + +	n.StatusID = "9HZJ76B6VXSKF" +	err = validate.Struct(n) +	suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + +	n.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" +	err = validate.Struct(n) +	suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + +	n.StatusID = "" +	n.NotificationType = gtsmodel.NotificationFollowRequest +	err = validate.Struct(n) +	suite.NoError(err) +} + +func (suite *NotificationValidateTestSuite) TestValidateNotificationNoCreatedAt() { +	n := happyNotification() + +	n.CreatedAt = time.Time{} +	err := validate.Struct(n) +	suite.NoError(err) +} + +func TestNotificationValidateTestSuite(t *testing.T) { +	suite.Run(t, new(NotificationValidateTestSuite)) +} diff --git a/internal/validate/routersession_test.go b/internal/validate/routersession_test.go new file mode 100644 index 000000000..8f4e112a5 --- /dev/null +++ b/internal/validate/routersession_test.go @@ -0,0 +1,88 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyRouterSession() *gtsmodel.RouterSession { +	return >smodel.RouterSession{ +		ID:    "01FE91RJR88PSEEE30EV35QR8N", +		Auth:  []byte("12345678901234567890123456789012"), +		Crypt: []byte("12345678901234567890123456789012"), +	} +} + +type RouterSessionValidateTestSuite struct { +	suite.Suite +} + +func (suite *RouterSessionValidateTestSuite) TestValidateRouterSessionHappyPath() { +	// no problem here +	r := happyRouterSession() +	err := validate.Struct(r) +	suite.NoError(err) +} + +func (suite *RouterSessionValidateTestSuite) TestValidateRouterSessionAuth() { +	r := happyRouterSession() + +	// remove auth struct +	r.Auth = nil +	err := validate.Struct(r) +	suite.EqualError(err, "Key: 'RouterSession.Auth' Error:Field validation for 'Auth' failed on the 'required' tag") + +	// auth bytes too long +	r.Auth = []byte("1234567890123456789012345678901234567890") +	err = validate.Struct(r) +	suite.EqualError(err, "Key: 'RouterSession.Auth' Error:Field validation for 'Auth' failed on the 'len' tag") + +	// auth bytes too short +	r.Auth = []byte("12345678901") +	err = validate.Struct(r) +	suite.EqualError(err, "Key: 'RouterSession.Auth' Error:Field validation for 'Auth' failed on the 'len' tag") +} + +func (suite *RouterSessionValidateTestSuite) TestValidateRouterSessionCrypt() { +	r := happyRouterSession() + +	// remove crypt struct +	r.Crypt = nil +	err := validate.Struct(r) +	suite.EqualError(err, "Key: 'RouterSession.Crypt' Error:Field validation for 'Crypt' failed on the 'required' tag") + +	// crypt bytes too long +	r.Crypt = []byte("1234567890123456789012345678901234567890") +	err = validate.Struct(r) +	suite.EqualError(err, "Key: 'RouterSession.Crypt' Error:Field validation for 'Crypt' failed on the 'len' tag") + +	// crypt bytes too short +	r.Crypt = []byte("12345678901") +	err = validate.Struct(r) +	suite.EqualError(err, "Key: 'RouterSession.Crypt' Error:Field validation for 'Crypt' failed on the 'len' tag") +} + +func TestRouterSessionValidateTestSuite(t *testing.T) { +	suite.Run(t, new(RouterSessionValidateTestSuite)) +} diff --git a/internal/validate/status_test.go b/internal/validate/status_test.go new file mode 100644 index 000000000..7c85414b1 --- /dev/null +++ b/internal/validate/status_test.go @@ -0,0 +1,163 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyStatus() *gtsmodel.Status { +	return >smodel.Status{ +		ID:                       "01FEBBH6NYDG87NK6A6EC543ED", +		CreatedAt:                time.Now(), +		UpdatedAt:                time.Now(), +		URI:                      "https://example.org/users/test_user/statuses/01FEBBH6NYDG87NK6A6EC543ED", +		URL:                      "https://example.org/@test_user/01FEBBH6NYDG87NK6A6EC543ED", +		Content:                  "<p>Test status! #hello</p>", +		AttachmentIDs:            []string{"01FEBBKZBY9H5FEP3PHVVAAGN1", "01FEBBM7S2R4WT6WWW22KN1PWE"}, +		Attachments:              nil, +		TagIDs:                   []string{"01FEBBNBMBSN1FESMZ1TCXNWYP"}, +		Tags:                     nil, +		MentionIDs:               nil, +		Mentions:                 nil, +		EmojiIDs:                 nil, +		Emojis:                   nil, +		Local:                    true, +		AccountID:                "01FEBBQ4KEP3824WW61MF52638", +		Account:                  nil, +		AccountURI:               "https://example.org/users/test_user", +		InReplyToID:              "", +		InReplyToURI:             "", +		InReplyToAccountID:       "", +		InReplyTo:                nil, +		InReplyToAccount:         nil, +		BoostOfID:                "", +		BoostOfAccountID:         "", +		BoostOf:                  nil, +		BoostOfAccount:           nil, +		ContentWarning:           "hello world test post", +		Visibility:               gtsmodel.VisibilityPublic, +		Sensitive:                false, +		Language:                 "en", +		CreatedWithApplicationID: "01FEBBZHF4GFVRXSJVXD0JTZZ2", +		CreatedWithApplication:   nil, +		VisibilityAdvanced: gtsmodel.VisibilityAdvanced{ +			Federated: true, +			Boostable: true, +			Replyable: true, +			Likeable:  true, +		}, +		ActivityStreamsType: ap.ObjectNote, +		Text:                "Test status! #hello", +		Pinned:              false, +	} +} + +type StatusValidateTestSuite struct { +	suite.Suite +} + +func (suite *StatusValidateTestSuite) TestValidateStatusHappyPath() { +	// no problem here +	s := happyStatus() +	err := validate.Struct(s) +	suite.NoError(err) +} + +func (suite *StatusValidateTestSuite) TestValidateStatusBadID() { +	s := happyStatus() + +	s.ID = "" +	err := validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	s.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *StatusValidateTestSuite) TestValidateStatusAttachmentIDs() { +	s := happyStatus() + +	s.AttachmentIDs[0] = "" +	err := validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.AttachmentIDs[0]' Error:Field validation for 'AttachmentIDs[0]' failed on the 'ulid' tag") + +	s.AttachmentIDs[0] = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.AttachmentIDs[0]' Error:Field validation for 'AttachmentIDs[0]' failed on the 'ulid' tag") + +	s.AttachmentIDs[1] = "" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.AttachmentIDs[0]' Error:Field validation for 'AttachmentIDs[0]' failed on the 'ulid' tag\nKey: 'Status.AttachmentIDs[1]' Error:Field validation for 'AttachmentIDs[1]' failed on the 'ulid' tag") + +	s.AttachmentIDs = []string{} +	err = validate.Struct(s) +	suite.NoError(err) + +	s.AttachmentIDs = nil +	err = validate.Struct(s) +	suite.NoError(err) +} + +func (suite *StatusValidateTestSuite) TestStatusApplicationID() { +	s := happyStatus() + +	s.CreatedWithApplicationID = "" +	err := validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.CreatedWithApplicationID' Error:Field validation for 'CreatedWithApplicationID' failed on the 'required_if' tag") + +	s.Local = false +	err = validate.Struct(s) +	suite.NoError(err) +} + +func (suite *StatusValidateTestSuite) TestValidateStatusReplyFields() { +	s := happyStatus() + +	s.InReplyToAccountID = "01FEBCTP6DN7961PN81C3DVM4N                         " +	err := validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.InReplyToID' Error:Field validation for 'InReplyToID' failed on the 'required_with' tag\nKey: 'Status.InReplyToURI' Error:Field validation for 'InReplyToURI' failed on the 'required_with' tag\nKey: 'Status.InReplyToAccountID' Error:Field validation for 'InReplyToAccountID' failed on the 'ulid' tag") + +	s.InReplyToAccountID = "01FEBCTP6DN7961PN81C3DVM4N" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.InReplyToID' Error:Field validation for 'InReplyToID' failed on the 'required_with' tag\nKey: 'Status.InReplyToURI' Error:Field validation for 'InReplyToURI' failed on the 'required_with' tag") + +	s.InReplyToURI = "https://example.org/users/mmbop/statuses/aaaaaaaa" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.InReplyToID' Error:Field validation for 'InReplyToID' failed on the 'required_with' tag") + +	s.InReplyToID = "not a valid ulid" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'Status.InReplyToID' Error:Field validation for 'InReplyToID' failed on the 'ulid' tag") + +	s.InReplyToID = "01FEBD07E72DEY6YB9K10ZA6ST" +	err = validate.Struct(s) +	suite.NoError(err) +} + +func TestStatusValidateTestSuite(t *testing.T) { +	suite.Run(t, new(StatusValidateTestSuite)) +} diff --git a/internal/validate/statusbookmark_test.go b/internal/validate/statusbookmark_test.go new file mode 100644 index 000000000..a52d0e26a --- /dev/null +++ b/internal/validate/statusbookmark_test.go @@ -0,0 +1,88 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyStatusBookmark() *gtsmodel.StatusBookmark { +	return >smodel.StatusBookmark{ +		ID:              "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:       time.Now(), +		AccountID:       "01FE96MAE58MXCE5C4SSMEMCEK", +		Account:         nil, +		TargetAccountID: "01FE96MXRHWZHKC0WH5FT82H1A", +		TargetAccount:   nil, +		StatusID:        "01FE96NBPNJNY26730FT6GZTFE", +		Status:          nil, +	} +} + +type StatusBookmarkValidateTestSuite struct { +	suite.Suite +} + +func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkHappyPath() { +	// no problem here +	s := happyStatusBookmark() +	err := validate.Struct(s) +	suite.NoError(err) +} + +func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkBadID() { +	s := happyStatusBookmark() + +	s.ID = "" +	err := validate.Struct(s) +	suite.EqualError(err, "Key: 'StatusBookmark.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	s.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'StatusBookmark.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkDodgyStatusID() { +	s := happyStatusBookmark() + +	s.StatusID = "9HZJ76B6VXSKF" +	err := validate.Struct(s) +	suite.EqualError(err, "Key: 'StatusBookmark.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + +	s.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" +	err = validate.Struct(s) +	suite.EqualError(err, "Key: 'StatusBookmark.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") +} + +func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkNoCreatedAt() { +	s := happyStatusBookmark() + +	s.CreatedAt = time.Time{} +	err := validate.Struct(s) +	suite.NoError(err) +} + +func TestStatusBookmarkValidateTestSuite(t *testing.T) { +	suite.Run(t, new(StatusBookmarkValidateTestSuite)) +} diff --git a/internal/validate/statusfave_test.go b/internal/validate/statusfave_test.go new file mode 100644 index 000000000..18808f51e --- /dev/null +++ b/internal/validate/statusfave_test.go @@ -0,0 +1,101 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyStatusFave() *gtsmodel.StatusFave { +	return >smodel.StatusFave{ +		ID:              "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:       time.Now(), +		AccountID:       "01FE96MAE58MXCE5C4SSMEMCEK", +		Account:         nil, +		TargetAccountID: "01FE96MXRHWZHKC0WH5FT82H1A", +		TargetAccount:   nil, +		StatusID:        "01FE96NBPNJNY26730FT6GZTFE", +		Status:          nil, +		URI:             "https://example.org/users/user1/activity/faves/01FE91RJR88PSEEE30EV35QR8N", +	} +} + +type StatusFaveValidateTestSuite struct { +	suite.Suite +} + +func (suite *StatusFaveValidateTestSuite) TestValidateStatusFaveHappyPath() { +	// no problem here +	f := happyStatusFave() +	err := validate.Struct(f) +	suite.NoError(err) +} + +func (suite *StatusFaveValidateTestSuite) TestValidateStatusFaveBadID() { +	f := happyStatusFave() + +	f.ID = "" +	err := validate.Struct(f) +	suite.EqualError(err, "Key: 'StatusFave.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	f.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(f) +	suite.EqualError(err, "Key: 'StatusFave.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *StatusFaveValidateTestSuite) TestValidateStatusFaveDodgyStatusID() { +	f := happyStatusFave() + +	f.StatusID = "9HZJ76B6VXSKF" +	err := validate.Struct(f) +	suite.EqualError(err, "Key: 'StatusFave.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + +	f.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" +	err = validate.Struct(f) +	suite.EqualError(err, "Key: 'StatusFave.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") +} + +func (suite *StatusFaveValidateTestSuite) TestValidateStatusFaveNoCreatedAt() { +	f := happyStatusFave() + +	f.CreatedAt = time.Time{} +	err := validate.Struct(f) +	suite.NoError(err) +} + +func (suite *StatusFaveValidateTestSuite) TestValidateStatusFaveNoURI() { +	f := happyStatusFave() + +	f.URI = "" +	err := validate.Struct(f) +	suite.EqualError(err, "Key: 'StatusFave.URI' Error:Field validation for 'URI' failed on the 'required' tag") + +	f.URI = "this-is-not-a-valid-url" +	err = validate.Struct(f) +	suite.EqualError(err, "Key: 'StatusFave.URI' Error:Field validation for 'URI' failed on the 'url' tag") +} + +func TestStatusFaveValidateTestSuite(t *testing.T) { +	suite.Run(t, new(StatusFaveValidateTestSuite)) +} diff --git a/internal/validate/statusmute_test.go b/internal/validate/statusmute_test.go new file mode 100644 index 000000000..c3fe5129d --- /dev/null +++ b/internal/validate/statusmute_test.go @@ -0,0 +1,88 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyStatusMute() *gtsmodel.StatusMute { +	return >smodel.StatusMute{ +		ID:              "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:       time.Now(), +		AccountID:       "01FE96MAE58MXCE5C4SSMEMCEK", +		Account:         nil, +		TargetAccountID: "01FE96MXRHWZHKC0WH5FT82H1A", +		TargetAccount:   nil, +		StatusID:        "01FE96NBPNJNY26730FT6GZTFE", +		Status:          nil, +	} +} + +type StatusMuteValidateTestSuite struct { +	suite.Suite +} + +func (suite *StatusMuteValidateTestSuite) TestValidateStatusMuteHappyPath() { +	// no problem here +	m := happyStatusMute() +	err := validate.Struct(m) +	suite.NoError(err) +} + +func (suite *StatusMuteValidateTestSuite) TestValidateStatusMuteBadID() { +	m := happyStatusMute() + +	m.ID = "" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'StatusMute.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'StatusMute.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *StatusMuteValidateTestSuite) TestValidateStatusMuteDodgyStatusID() { +	m := happyStatusMute() + +	m.StatusID = "9HZJ76B6VXSKF" +	err := validate.Struct(m) +	suite.EqualError(err, "Key: 'StatusMute.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + +	m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" +	err = validate.Struct(m) +	suite.EqualError(err, "Key: 'StatusMute.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") +} + +func (suite *StatusMuteValidateTestSuite) TestValidateStatusMuteNoCreatedAt() { +	m := happyStatusMute() + +	m.CreatedAt = time.Time{} +	err := validate.Struct(m) +	suite.NoError(err) +} + +func TestStatusMuteValidateTestSuite(t *testing.T) { +	suite.Run(t, new(StatusMuteValidateTestSuite)) +} diff --git a/internal/validate/structvalidation.go b/internal/validate/structvalidation.go new file mode 100644 index 000000000..ff72e253d --- /dev/null +++ b/internal/validate/structvalidation.go @@ -0,0 +1,63 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate + +import ( +	"reflect" + +	"github.com/go-playground/validator/v10" +	"github.com/superseriousbusiness/gotosocial/internal/regexes" +) + +var v *validator.Validate + +func ulidValidator(fl validator.FieldLevel) bool { +	field := fl.Field() + +	switch field.Kind() { +	case reflect.String: +		return regexes.ULID.MatchString(field.String()) +	default: +		return false +	} +} + +func init() { +	v = validator.New() +	if err := v.RegisterValidation("ulid", ulidValidator); err != nil { +		panic(err) +	} +} + +// Struct validates the passed struct, returning validator.ValidationErrors if invalid, or nil if OK. +func Struct(s interface{}) error { +	return processValidationError(v.Struct(s)) +} + +func processValidationError(err error) error { +	if err == nil { +		return nil +	} + +	if ive, ok := err.(*validator.InvalidValidationError); ok { +		panic(ive) +	} + +	return err.(validator.ValidationErrors) +} diff --git a/internal/validate/structvalidation_test.go b/internal/validate/structvalidation_test.go new file mode 100644 index 000000000..1942a44f8 --- /dev/null +++ b/internal/validate/structvalidation_test.go @@ -0,0 +1,71 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +type ValidateTestSuite struct { +	suite.Suite +} + +func (suite *ValidateTestSuite) TestValidateNilPointer() { +	var nilUser *gtsmodel.User +	suite.Panics(func() { +		validate.Struct(nilUser) +	}) +} + +func (suite *ValidateTestSuite) TestValidatePointer() { +	user := >smodel.User{} +	err := validate.Struct(user) +	suite.EqualError(err, "Key: 'User.ID' Error:Field validation for 'ID' failed on the 'required' tag\nKey: 'User.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\nKey: 'User.EncryptedPassword' Error:Field validation for 'EncryptedPassword' failed on the 'required' tag\nKey: 'User.UnconfirmedEmail' Error:Field validation for 'UnconfirmedEmail' failed on the 'required_without' tag") +} + +func (suite *ValidateTestSuite) TestValidateNil() { +	suite.Panics(func() { +		validate.Struct(nil) +	}) +} + +func (suite *ValidateTestSuite) TestValidateWeirdULID() { +	type a struct { +		ID bool `validate:"required,ulid"` +	} + +	err := validate.Struct(a{ID: true}) +	suite.Error(err) +} + +func (suite *ValidateTestSuite) TestValidateNotStruct() { +	type aaaaaaa string +	aaaaaa := aaaaaaa("aaaa") +	suite.Panics(func() { +		validate.Struct(aaaaaa) +	}) +} + +func TestValidateTestSuite(t *testing.T) { +	suite.Run(t, new(ValidateTestSuite)) +} diff --git a/internal/validate/tag_test.go b/internal/validate/tag_test.go new file mode 100644 index 000000000..14032319e --- /dev/null +++ b/internal/validate/tag_test.go @@ -0,0 +1,93 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyTag() *gtsmodel.Tag { +	return >smodel.Tag{ +		ID:                     "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:              time.Now(), +		UpdatedAt:              time.Now(), +		URL:                    "https://example.org/tags/some_tag", +		Name:                   "some_tag", +		FirstSeenFromAccountID: "01FE91SR5P2GW06K3AJ98P72MT", +		Useable:                true, +		Listable:               true, +		LastStatusAt:           time.Now(), +	} +} + +type TagValidateTestSuite struct { +	suite.Suite +} + +func (suite *TagValidateTestSuite) TestValidateTagHappyPath() { +	// no problem here +	t := happyTag() +	err := validate.Struct(t) +	suite.NoError(err) +} + +func (suite *TagValidateTestSuite) TestValidateTagNoName() { +	t := happyTag() +	t.Name = "" + +	err := validate.Struct(t) +	suite.EqualError(err, "Key: 'Tag.Name' Error:Field validation for 'Name' failed on the 'required' tag") +} + +func (suite *TagValidateTestSuite) TestValidateTagBadURL() { +	t := happyTag() + +	t.URL = "" +	err := validate.Struct(t) +	suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'required' tag") + +	t.URL = "no-schema.com" +	err = validate.Struct(t) +	suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'url' tag") + +	t.URL = "justastring" +	err = validate.Struct(t) +	suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'url' tag") + +	t.URL = "https://aaa\n\n\naaaaaaaa" +	err = validate.Struct(t) +	suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'url' tag") +} + +func (suite *TagValidateTestSuite) TestValidateTagNoFirstSeenFromAccountID() { +	t := happyTag() +	t.FirstSeenFromAccountID = "" + +	err := validate.Struct(t) +	suite.NoError(err) +} + +func TestTagValidateTestSuite(t *testing.T) { +	suite.Run(t, new(TagValidateTestSuite)) +} diff --git a/internal/validate/token_test.go b/internal/validate/token_test.go new file mode 100644 index 000000000..cf4c8a6ca --- /dev/null +++ b/internal/validate/token_test.go @@ -0,0 +1,99 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyToken() *gtsmodel.Token { +	return >smodel.Token{ +		ID:          "01FE91RJR88PSEEE30EV35QR8N", +		CreatedAt:   time.Now(), +		UpdatedAt:   time.Now(), +		ClientID:    "01FEEDMF6C0QD589MRK7919Z0R", +		UserID:      "01FEK0BFJKYXB4Y51RBQ7P5P79", +		RedirectURI: "oauth2redirect://com.keylesspalace.tusky/", +		Scope:       "read write follow", +	} +} + +type TokenValidateTestSuite struct { +	suite.Suite +} + +func (suite *TokenValidateTestSuite) TestValidateTokenHappyPath() { +	// no problem here +	t := happyToken() +	err := validate.Struct(t) +	suite.NoError(err) +} + +func (suite *TokenValidateTestSuite) TestValidateTokenBadID() { +	t := happyToken() + +	t.ID = "" +	err := validate.Struct(t) +	suite.EqualError(err, "Key: 'Token.ID' Error:Field validation for 'ID' failed on the 'required' tag") + +	t.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" +	err = validate.Struct(t) +	suite.EqualError(err, "Key: 'Token.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *TokenValidateTestSuite) TestValidateTokenNoCreatedAt() { +	t := happyToken() + +	t.CreatedAt = time.Time{} +	err := validate.Struct(t) +	suite.NoError(err) +} + +func (suite *TokenValidateTestSuite) TestValidateTokenRedirectURI() { +	t := happyToken() + +	t.RedirectURI = "invalid-uri" +	err := validate.Struct(t) +	suite.EqualError(err, "Key: 'Token.RedirectURI' Error:Field validation for 'RedirectURI' failed on the 'uri' tag") + +	t.RedirectURI = "" +	err = validate.Struct(t) +	suite.EqualError(err, "Key: 'Token.RedirectURI' Error:Field validation for 'RedirectURI' failed on the 'required' tag") + +	t.RedirectURI = "urn:ietf:wg:oauth:2.0:oob" +	err = validate.Struct(t) +	suite.NoError(err) +} + +func (suite *TokenValidateTestSuite) TestValidateTokenScope() { +	t := happyToken() + +	t.Scope = "" +	err := validate.Struct(t) +	suite.EqualError(err, "Key: 'Token.Scope' Error:Field validation for 'Scope' failed on the 'required' tag") +} + +func TestTokenValidateTestSuite(t *testing.T) { +	suite.Run(t, new(TokenValidateTestSuite)) +} diff --git a/internal/validate/user_test.go b/internal/validate/user_test.go new file mode 100644 index 000000000..f747b7c08 --- /dev/null +++ b/internal/validate/user_test.go @@ -0,0 +1,134 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package validate_test + +import ( +	"net" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/validate" +) + +func happyUser() *gtsmodel.User { +	return >smodel.User{ +		ID:                     "01FE8TTK9F34BR0KG7639AJQTX", +		Email:                  "whatever@example.org", +		AccountID:              "01FE8TWA7CN8J7237K5DFS1RY5", +		Account:                nil, +		EncryptedPassword:      "$2y$10$tkRapNGW.RWkEuCMWdgArunABFvsPGRvFQY3OibfSJo0RDL3z8WfC", +		CreatedAt:              time.Now(), +		UpdatedAt:              time.Now(), +		SignUpIP:               net.ParseIP("128.64.32.16"), +		CurrentSignInAt:        time.Now(), +		CurrentSignInIP:        net.ParseIP("128.64.32.16"), +		LastSignInAt:           time.Now(), +		LastSignInIP:           net.ParseIP("128.64.32.16"), +		SignInCount:            0, +		InviteID:               "", +		ChosenLanguages:        []string{}, +		FilteredLanguages:      []string{}, +		Locale:                 "en", +		CreatedByApplicationID: "01FE8Y5EHMWCA1MHMTNHRVZ1X4", +		CreatedByApplication:   nil, +		LastEmailedAt:          time.Now(), +		ConfirmationToken:      "", +		ConfirmedAt:            time.Now(), +		ConfirmationSentAt:     time.Time{}, +		UnconfirmedEmail:       "", +		Moderator:              false, +		Admin:                  false, +		Disabled:               false, +		Approved:               true, +	} +} + +type UserValidateTestSuite struct { +	suite.Suite +} + +func (suite *UserValidateTestSuite) TestValidateUserHappyPath() { +	// no problem here +	u := happyUser() +	err := validate.Struct(u) +	suite.NoError(err) +} + +func (suite *UserValidateTestSuite) TestValidateUserNoID() { +	// user has no id set +	u := happyUser() +	u.ID = "" + +	err := validate.Struct(u) +	suite.EqualError(err, "Key: 'User.ID' Error:Field validation for 'ID' failed on the 'required' tag") +} + +func (suite *UserValidateTestSuite) TestValidateUserNoEmail() { +	// user has no email or unconfirmed email set +	u := happyUser() +	u.Email = "" + +	err := validate.Struct(u) +	suite.EqualError(err, "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required_with' tag\nKey: 'User.UnconfirmedEmail' Error:Field validation for 'UnconfirmedEmail' failed on the 'required_without' tag") +} + +func (suite *UserValidateTestSuite) TestValidateUserOnlyUnconfirmedEmail() { +	// user has only UnconfirmedEmail but ConfirmedAt is set +	u := happyUser() +	u.Email = "" +	u.UnconfirmedEmail = "whatever@example.org" + +	err := validate.Struct(u) +	suite.EqualError(err, "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required_with' tag") +} + +func (suite *UserValidateTestSuite) TestValidateUserOnlyUnconfirmedEmailOK() { +	// user has only UnconfirmedEmail and ConfirmedAt is not set +	u := happyUser() +	u.Email = "" +	u.UnconfirmedEmail = "whatever@example.org" +	u.ConfirmedAt = time.Time{} + +	err := validate.Struct(u) +	suite.NoError(err) +} + +func (suite *UserValidateTestSuite) TestValidateUserNoConfirmedAt() { +	// user has Email but no ConfirmedAt +	u := happyUser() +	u.ConfirmedAt = time.Time{} + +	err := validate.Struct(u) +	suite.EqualError(err, "Key: 'User.ConfirmedAt' Error:Field validation for 'ConfirmedAt' failed on the 'required_with' tag") +} + +func (suite *UserValidateTestSuite) TestValidateUserUnlikelySignInCount() { +	// user has Email but no ConfirmedAt +	u := happyUser() +	u.SignInCount = -69 + +	err := validate.Struct(u) +	suite.EqualError(err, "Key: 'User.SignInCount' Error:Field validation for 'SignInCount' failed on the 'min' tag") +} + +func TestUserValidateTestSuite(t *testing.T) { +	suite.Run(t, new(UserValidateTestSuite)) +} @@ -1,3 +1,3 @@  #!/bin/bash -golangci-lint run
\ No newline at end of file +golangci-lint run --test=false diff --git a/testrig/db.go b/testrig/db.go index 52a1af6e9..7cb4f7645 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -26,7 +26,6 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/db/bundb"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth"  )  var testModels []interface{} = []interface{}{ @@ -51,8 +50,8 @@ var testModels []interface{} = []interface{}{  	>smodel.Instance{},  	>smodel.Notification{},  	>smodel.RouterSession{}, -	&oauth.Token{}, -	&oauth.Client{}, +	>smodel.Token{}, +	>smodel.Client{},  }  // NewTestDB returns a new initialized, empty database for testing. diff --git a/testrig/testmodels.go b/testrig/testmodels.go index c3ff25e57..a38e72329 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -36,13 +36,13 @@ import (  	"github.com/go-fed/activity/pub"  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/oauth"  )  // NewTestTokens returns a map of tokens keyed according to which account the token belongs to. -func NewTestTokens() map[string]*oauth.Token { -	tokens := map[string]*oauth.Token{ +func NewTestTokens() map[string]*gtsmodel.Token { +	tokens := map[string]*gtsmodel.Token{  		"local_account_1": {  			ID:              "01F8MGTQW4DKTDF8SW5CT9HYGA",  			ClientID:        "01F8MGV8AC3NGSJW0FE8W1BV70", @@ -68,8 +68,8 @@ func NewTestTokens() map[string]*oauth.Token {  }  // NewTestClients returns a map of Clients keyed according to which account they are used by. -func NewTestClients() map[string]*oauth.Client { -	clients := map[string]*oauth.Client{ +func NewTestClients() map[string]*gtsmodel.Client { +	clients := map[string]*gtsmodel.Client{  		"admin_account": {  			ID:     "01F8MGWSJCND9BWBD4WGJXBM93",  			Secret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a", @@ -103,7 +103,6 @@ func NewTestApplications() map[string]*gtsmodel.Application {  			ClientID:     "01F8MGWSJCND9BWBD4WGJXBM93",           // admin client  			ClientSecret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a", // admin client  			Scopes:       "read write follow push", -			VapidKey:     "76ae0095-8a10-438f-9f49-522d1985b190",  		},  		"application_1": {  			ID:           "01F8MGY43H3N2C8EWPR2FPYEXG", @@ -113,7 +112,6 @@ func NewTestApplications() map[string]*gtsmodel.Application {  			ClientID:     "01F8MGV8AC3NGSJW0FE8W1BV70",           // client_1  			ClientSecret: "c3724c74-dc3b-41b2-a108-0ea3d8399830", // client_1  			Scopes:       "read write follow push", -			VapidKey:     "4738dfd7-ca73-4aa6-9aa9-80e946b7db36",  		},  		"application_2": {  			ID:           "01F8MGYG9E893WRHW0TAEXR8GJ", @@ -123,7 +121,6 @@ func NewTestApplications() map[string]*gtsmodel.Application {  			ClientID:     "01F8MGW47HN8ZXNHNZ7E47CDMQ",           // client_2  			ClientSecret: "8f5603a5-c721-46cd-8f1b-2e368f51379f", // client_2  			Scopes:       "read write follow push", -			VapidKey:     "c040a5fc-e1e2-4859-bbea-0a3efbca1c4b",  		},  	}  	return apps @@ -149,7 +146,7 @@ func NewTestUsers() map[string]*gtsmodel.User {  			ChosenLanguages:        []string{},  			FilteredLanguages:      []string{},  			Locale:                 "en", -			CreatedByApplicationID: "", +			CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",  			LastEmailedAt:          time.Time{},  			ConfirmationToken:      "a5a280bd-34be-44a3-8330-a57eaf61b8dd",  			ConfirmedAt:            time.Time{}, @@ -179,7 +176,7 @@ func NewTestUsers() map[string]*gtsmodel.User {  			ChosenLanguages:        []string{"en"},  			FilteredLanguages:      []string{},  			Locale:                 "en", -			CreatedByApplicationID: "", +			CreatedByApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",  			LastEmailedAt:          time.Now().Add(-30 * time.Minute),  			ConfirmationToken:      "",  			ConfirmedAt:            time.Now().Add(-72 * time.Hour), @@ -239,7 +236,7 @@ func NewTestUsers() map[string]*gtsmodel.User {  			ChosenLanguages:        []string{"en"},  			FilteredLanguages:      []string{},  			Locale:                 "en", -			CreatedByApplicationID: "", +			CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",  			LastEmailedAt:          time.Now().Add(-55 * time.Minute),  			ConfirmationToken:      "",  			ConfirmedAt:            time.Now().Add(-34 * time.Hour), @@ -260,10 +257,6 @@ func NewTestUsers() map[string]*gtsmodel.User {  // NewTestAccounts returns a map of accounts keyed by what type of account they are.  func NewTestAccounts() map[string]*gtsmodel.Account {  	accounts := map[string]*gtsmodel.Account{ -		"instance_account": { -			ID:       "01F8MH261H1KSV3GW3016GZRY3", -			Username: "localhost:8080", -		},  		"unconfirmed_account": {  			ID:                      "01F8MH0BBE4FHXPH513MBVFHB0",  			Username:                "weed_lord420", @@ -291,7 +284,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {  			FollowersURI:            "http://localhost:8080/users/weed_lord420/followers",  			FollowingURI:            "http://localhost:8080/users/weed_lord420/following",  			FeaturedCollectionURI:   "http://localhost:8080/users/weed_lord420/collections/featured", -			ActorType:               gtsmodel.ActivityStreamsPerson, +			ActorType:               ap.ActorPerson,  			AlsoKnownAs:             "",  			PrivateKey:              &rsa.PrivateKey{},  			PublicKey:               &rsa.PublicKey{}, @@ -330,7 +323,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {  			FollowersURI:            "http://localhost:8080/users/admin/followers",  			FollowingURI:            "http://localhost:8080/users/admin/following",  			FeaturedCollectionURI:   "http://localhost:8080/users/admin/collections/featured", -			ActorType:               gtsmodel.ActivityStreamsPerson, +			ActorType:               ap.ActorPerson,  			AlsoKnownAs:             "",  			PrivateKey:              &rsa.PrivateKey{},  			PublicKey:               &rsa.PublicKey{}, @@ -367,7 +360,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {  			FollowersURI:            "http://localhost:8080/users/the_mighty_zork/followers",  			FollowingURI:            "http://localhost:8080/users/the_mighty_zork/following",  			FeaturedCollectionURI:   "http://localhost:8080/users/the_mighty_zork/collections/featured", -			ActorType:               gtsmodel.ActivityStreamsPerson, +			ActorType:               ap.ActorPerson,  			AlsoKnownAs:             "",  			PrivateKey:              &rsa.PrivateKey{},  			PublicKey:               &rsa.PublicKey{}, @@ -405,7 +398,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {  			FollowersURI:            "http://localhost:8080/users/1happyturtle/followers",  			FollowingURI:            "http://localhost:8080/users/1happyturtle/following",  			FeaturedCollectionURI:   "http://localhost:8080/users/1happyturtle/collections/featured", -			ActorType:               gtsmodel.ActivityStreamsPerson, +			ActorType:               ap.ActorPerson,  			AlsoKnownAs:             "",  			PrivateKey:              &rsa.PrivateKey{},  			PublicKey:               &rsa.PublicKey{}, @@ -440,7 +433,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {  			FollowersURI:          "http://fossbros-anonymous.io/users/foss_satan/followers",  			FollowingURI:          "http://fossbros-anonymous.io/users/foss_satan/following",  			FeaturedCollectionURI: "http://fossbros-anonymous.io/users/foss_satan/collections/featured", -			ActorType:             gtsmodel.ActivityStreamsPerson, +			ActorType:             ap.ActorPerson,  			AlsoKnownAs:           "",  			PrivateKey:            &rsa.PrivateKey{},  			PublicKey:             &rsa.PublicKey{}, @@ -799,6 +792,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-71 * time.Hour),  			UpdatedAt:                time.Now().Add(-71 * time.Hour),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/admin",  			AccountID:                "01F8MH17FWEB39HZJ76B6VXSKF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -807,13 +801,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                false,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"admin_account_status_2": {  			ID:                       "01F8MHAAY43M6RJ473VQFCVH37", @@ -823,6 +817,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-70 * time.Hour),  			UpdatedAt:                time.Now().Add(-70 * time.Hour),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/admin",  			AccountID:                "01F8MH17FWEB39HZJ76B6VXSKF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -831,13 +826,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                true,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_1_status_1": {  			ID:                       "01F8MHAMCHF6Y650WCRSCP4WMY", @@ -847,6 +842,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-47 * time.Hour),  			UpdatedAt:                time.Now().Add(-47 * time.Hour),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/the_mighty_zork",  			AccountID:                "01F8MH1H7YV1Z7D2C8K2730QBF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -855,13 +851,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                true,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_1_status_2": {  			ID:                       "01F8MHAYFKS4KMXF8K5Y1C0KRN", @@ -871,6 +867,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-46 * time.Hour),  			UpdatedAt:                time.Now().Add(-46 * time.Hour),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/the_mighty_zork",  			AccountID:                "01F8MH1H7YV1Z7D2C8K2730QBF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -879,13 +876,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                false,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: false,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_1_status_3": {  			ID:                       "01F8MHBBN8120SYH7D5S050MGK", @@ -895,6 +892,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-45 * time.Hour),  			UpdatedAt:                time.Now().Add(-45 * time.Hour),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/the_mighty_zork",  			AccountID:                "01F8MH1H7YV1Z7D2C8K2730QBF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -903,13 +901,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                false,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: false,  				Replyable: false,  				Likeable:  false,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_1_status_4": {  			ID:                       "01F8MH82FYRXD2RC6108DAJ5HB", @@ -920,6 +918,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-1 * time.Hour),  			UpdatedAt:                time.Now().Add(-1 * time.Hour),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/the_mighty_zork",  			AccountID:                "01F8MH1H7YV1Z7D2C8K2730QBF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -928,13 +927,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                false,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_1_status_5": {  			ID:                       "01FCTA44PW9H1TB328S9AQXKDS", @@ -945,6 +944,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-1 * time.Minute),  			UpdatedAt:                time.Now().Add(-1 * time.Minute),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/the_mighty_zork",  			AccountID:                "01F8MH1H7YV1Z7D2C8K2730QBF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -953,13 +953,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                false,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_2_status_1": {  			ID:                       "01F8MHBQCBTDKN6X5VHGMMN4MA", @@ -969,6 +969,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-189 * time.Hour),  			UpdatedAt:                time.Now().Add(-189 * time.Hour),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/1happyturtle",  			AccountID:                "01F8MH5NBDF2MV7CTC4Q5128HF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -977,13 +978,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                true,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_2_status_2": {  			ID:                       "01F8MHC0H0A7XHTVH5F596ZKBM", @@ -993,6 +994,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-1 * time.Minute),  			UpdatedAt:                time.Now().Add(-1 * time.Minute),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/1happyturtle",  			AccountID:                "01F8MH5NBDF2MV7CTC4Q5128HF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -1001,13 +1003,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                true,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: false,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_2_status_3": {  			ID:                       "01F8MHC8VWDRBQR0N1BATDDEM5", @@ -1017,6 +1019,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-2 * time.Minute),  			UpdatedAt:                time.Now().Add(-2 * time.Minute),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/1happyturtle",  			AccountID:                "01F8MH5NBDF2MV7CTC4Q5128HF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -1025,13 +1028,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                true,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: false,  				Likeable:  false,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_2_status_4": {  			ID:                       "01F8MHCP5P2NWYQ416SBA0XSEV", @@ -1041,6 +1044,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-1 * time.Minute),  			UpdatedAt:                time.Now().Add(-1 * time.Minute),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/1happyturtle",  			AccountID:                "01F8MH5NBDF2MV7CTC4Q5128HF",  			InReplyToID:              "",  			BoostOfID:                "", @@ -1049,13 +1053,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                true,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: false,  				Boostable: false,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  		"local_account_2_status_5": {  			ID:                       "01FCQSQ667XHJ9AV9T27SJJSX5", @@ -1065,6 +1069,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			CreatedAt:                time.Now().Add(-1 * time.Minute),  			UpdatedAt:                time.Now().Add(-1 * time.Minute),  			Local:                    true, +			AccountURI:               "http://localhost:8080/users/1happyturtle",  			MentionIDs:               []string{"01FDF2HM2NF6FSRZCDEDV451CN"},  			AccountID:                "01F8MH5NBDF2MV7CTC4Q5128HF",  			InReplyToID:              "01F8MHAMCHF6Y650WCRSCP4WMY", @@ -1076,13 +1081,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status {  			Sensitive:                false,  			Language:                 "en",  			CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", -			VisibilityAdvanced: >smodel.VisibilityAdvanced{ +			VisibilityAdvanced: gtsmodel.VisibilityAdvanced{  				Federated: true,  				Boostable: true,  				Replyable: true,  				Likeable:  true,  			}, -			ActivityStreamsType: gtsmodel.ActivityStreamsNote, +			ActivityStreamsType: ap.ObjectNote,  		},  	}  } diff --git a/vendor/github.com/uptrace/bun/migrate/migration.go b/vendor/github.com/uptrace/bun/migrate/migration.go new file mode 100644 index 000000000..79f13f972 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/migration.go @@ -0,0 +1,272 @@ +package migrate + +import ( +	"bufio" +	"bytes" +	"context" +	"fmt" +	"io/fs" +	"sort" +	"strings" +	"time" + +	"github.com/uptrace/bun" +) + +type Migration struct { +	bun.BaseModel + +	ID         int64 +	Name       string +	GroupID    int64 +	MigratedAt time.Time `bun:",notnull,nullzero,default:current_timestamp"` + +	Up   MigrationFunc `bun:"-"` +	Down MigrationFunc `bun:"-"` +} + +func (m *Migration) String() string { +	return m.Name +} + +func (m *Migration) IsApplied() bool { +	return m.ID > 0 +} + +type MigrationFunc func(ctx context.Context, db *bun.DB) error + +func NewSQLMigrationFunc(fsys fs.FS, name string) MigrationFunc { +	return func(ctx context.Context, db *bun.DB) error { +		isTx := strings.HasSuffix(name, ".tx.up.sql") || strings.HasSuffix(name, ".tx.down.sql") + +		f, err := fsys.Open(name) +		if err != nil { +			return err +		} + +		scanner := bufio.NewScanner(f) +		var queries []string + +		var query []byte +		for scanner.Scan() { +			b := scanner.Bytes() + +			const prefix = "--bun:" +			if bytes.HasPrefix(b, []byte(prefix)) { +				b = b[len(prefix):] +				if bytes.Equal(b, []byte("split")) { +					queries = append(queries, string(query)) +					query = query[:0] +					continue +				} +				return fmt.Errorf("bun: unknown directive: %q", b) +			} + +			query = append(query, b...) +			query = append(query, '\n') +		} + +		if len(query) > 0 { +			queries = append(queries, string(query)) +		} +		if err := scanner.Err(); err != nil { +			return err +		} + +		var idb bun.IConn + +		if isTx { +			tx, err := db.BeginTx(ctx, nil) +			if err != nil { +				return err +			} +			idb = tx +		} else { +			conn, err := db.Conn(ctx) +			if err != nil { +				return err +			} +			idb = conn +		} + +		for _, q := range queries { +			_, err = idb.ExecContext(ctx, q) +			if err != nil { +				return err +			} +		} + +		if tx, ok := idb.(bun.Tx); ok { +			return tx.Commit() +		} else if conn, ok := idb.(bun.Conn); ok { +			return conn.Close() +		} + +		panic("not reached") +	} +} + +const goTemplate = `package %s + +import ( +	"context" +	"fmt" + +	"github.com/uptrace/bun" +) + +func init() { +	Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { +		fmt.Print(" [up migration] ") +		return nil +	}, func(ctx context.Context, db *bun.DB) error { +		fmt.Print(" [down migration] ") +		return nil +	}) +} +` + +const sqlTemplate = `SELECT 1 + +--bun:split + +SELECT 2 +` + +//------------------------------------------------------------------------------ + +type MigrationSlice []Migration + +func (ms MigrationSlice) String() string { +	if len(ms) == 0 { +		return "empty" +	} + +	if len(ms) > 5 { +		return fmt.Sprintf("%d migrations (%s ... %s)", len(ms), ms[0].Name, ms[len(ms)-1].Name) +	} + +	var sb strings.Builder + +	for i := range ms { +		if i > 0 { +			sb.WriteString(", ") +		} +		sb.WriteString(ms[i].Name) +	} + +	return sb.String() +} + +// Applied returns applied migrations in descending order +// (the order is important and is used in Rollback). +func (ms MigrationSlice) Applied() MigrationSlice { +	var applied MigrationSlice +	for i := range ms { +		if ms[i].IsApplied() { +			applied = append(applied, ms[i]) +		} +	} +	sortDesc(applied) +	return applied +} + +// Unapplied returns unapplied migrations in ascending order +// (the order is important and is used in Migrate). +func (ms MigrationSlice) Unapplied() MigrationSlice { +	var unapplied MigrationSlice +	for i := range ms { +		if !ms[i].IsApplied() { +			unapplied = append(unapplied, ms[i]) +		} +	} +	sortAsc(unapplied) +	return unapplied +} + +// LastGroupID returns the last applied migration group id. +// The id is 0 when there are no migration groups. +func (ms MigrationSlice) LastGroupID() int64 { +	var lastGroupID int64 +	for i := range ms { +		groupID := ms[i].GroupID +		if groupID != 0 && groupID > lastGroupID { +			lastGroupID = groupID +		} +	} +	return lastGroupID +} + +// LastGroup returns the last applied migration group. +func (ms MigrationSlice) LastGroup() *MigrationGroup { +	group := &MigrationGroup{ +		ID: ms.LastGroupID(), +	} +	if group.ID == 0 { +		return group +	} +	for i := range ms { +		if ms[i].GroupID == group.ID { +			group.Migrations = append(group.Migrations, ms[i]) +		} +	} +	return group +} + +type MigrationGroup struct { +	ID         int64 +	Migrations MigrationSlice +} + +func (g *MigrationGroup) IsZero() bool { +	return g.ID == 0 && len(g.Migrations) == 0 +} + +func (g *MigrationGroup) String() string { +	if g.IsZero() { +		return "nil" +	} +	return fmt.Sprintf("group #%d (%s)", g.ID, g.Migrations) +} + +type MigrationFile struct { +	Name    string +	Path    string +	Content string +} + +//------------------------------------------------------------------------------ + +type migrationConfig struct { +	nop bool +} + +func newMigrationConfig(opts []MigrationOption) *migrationConfig { +	cfg := new(migrationConfig) +	for _, opt := range opts { +		opt(cfg) +	} +	return cfg +} + +type MigrationOption func(cfg *migrationConfig) + +func WithNopMigration() MigrationOption { +	return func(cfg *migrationConfig) { +		cfg.nop = true +	} +} + +//------------------------------------------------------------------------------ + +func sortAsc(ms MigrationSlice) { +	sort.Slice(ms, func(i, j int) bool { +		return ms[i].Name < ms[j].Name +	}) +} + +func sortDesc(ms MigrationSlice) { +	sort.Slice(ms, func(i, j int) bool { +		return ms[i].Name > ms[j].Name +	}) +} diff --git a/vendor/github.com/uptrace/bun/migrate/migrations.go b/vendor/github.com/uptrace/bun/migrate/migrations.go new file mode 100644 index 000000000..9af861048 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/migrations.go @@ -0,0 +1,168 @@ +package migrate + +import ( +	"errors" +	"fmt" +	"io/fs" +	"os" +	"path/filepath" +	"regexp" +	"runtime" +	"strings" +) + +type MigrationsOption func(m *Migrations) + +func WithMigrationsDirectory(directory string) MigrationsOption { +	return func(m *Migrations) { +		m.explicitDirectory = directory +	} +} + +type Migrations struct { +	ms MigrationSlice + +	explicitDirectory string +	implicitDirectory string +} + +func NewMigrations(opts ...MigrationsOption) *Migrations { +	m := new(Migrations) +	for _, opt := range opts { +		opt(m) +	} +	m.implicitDirectory = filepath.Dir(migrationFile()) +	return m +} + +func (m *Migrations) Sorted() MigrationSlice { +	migrations := make(MigrationSlice, len(m.ms)) +	copy(migrations, m.ms) +	sortAsc(migrations) +	return migrations +} + +func (m *Migrations) MustRegister(up, down MigrationFunc) { +	if err := m.Register(up, down); err != nil { +		panic(err) +	} +} + +func (m *Migrations) Register(up, down MigrationFunc) error { +	fpath := migrationFile() +	name, err := extractMigrationName(fpath) +	if err != nil { +		return err +	} + +	m.Add(Migration{ +		Name: name, +		Up:   up, +		Down: down, +	}) + +	return nil +} + +func (m *Migrations) Add(migration Migration) { +	if migration.Name == "" { +		panic("migration name is required") +	} +	m.ms = append(m.ms, migration) +} + +func (m *Migrations) DiscoverCaller() error { +	dir := filepath.Dir(migrationFile()) +	return m.Discover(os.DirFS(dir)) +} + +func (m *Migrations) Discover(fsys fs.FS) error { +	return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { +		if err != nil { +			return err +		} +		if d.IsDir() { +			return nil +		} + +		if !strings.HasSuffix(path, ".up.sql") && !strings.HasSuffix(path, ".down.sql") { +			return nil +		} + +		name, err := extractMigrationName(path) +		if err != nil { +			return err +		} + +		migration := m.getOrCreateMigration(name) +		if err != nil { +			return err +		} +		migrationFunc := NewSQLMigrationFunc(fsys, path) + +		if strings.HasSuffix(path, ".up.sql") { +			migration.Up = migrationFunc +			return nil +		} +		if strings.HasSuffix(path, ".down.sql") { +			migration.Down = migrationFunc +			return nil +		} + +		return errors.New("migrate: not reached") +	}) +} + +func (m *Migrations) getOrCreateMigration(name string) *Migration { +	for i := range m.ms { +		m := &m.ms[i] +		if m.Name == name { +			return m +		} +	} + +	m.ms = append(m.ms, Migration{Name: name}) +	return &m.ms[len(m.ms)-1] +} + +func (m *Migrations) getDirectory() string { +	if m.explicitDirectory != "" { +		return m.explicitDirectory +	} +	if m.implicitDirectory != "" { +		return m.implicitDirectory +	} +	return filepath.Dir(migrationFile()) +} + +func migrationFile() string { +	const depth = 32 +	var pcs [depth]uintptr +	n := runtime.Callers(1, pcs[:]) +	frames := runtime.CallersFrames(pcs[:n]) + +	for { +		f, ok := frames.Next() +		if !ok { +			break +		} +		if !strings.Contains(f.Function, "/bun/migrate.") { +			return f.File +		} +	} + +	return "" +} + +var fnameRE = regexp.MustCompile(`^(\d{14})_[0-9a-z_\-]+\.`) + +func extractMigrationName(fpath string) (string, error) { +	fname := filepath.Base(fpath) + +	matches := fnameRE.FindStringSubmatch(fname) +	if matches == nil { +		return "", fmt.Errorf("migrate: unsupported migration name format: %q", fname) +	} + +	return matches[1], nil +} diff --git a/vendor/github.com/uptrace/bun/migrate/migrator.go b/vendor/github.com/uptrace/bun/migrate/migrator.go new file mode 100644 index 000000000..f9b4a51c2 --- /dev/null +++ b/vendor/github.com/uptrace/bun/migrate/migrator.go @@ -0,0 +1,401 @@ +package migrate + +import ( +	"context" +	"errors" +	"fmt" +	"io/ioutil" +	"log" +	"path/filepath" +	"regexp" +	"time" + +	"github.com/uptrace/bun" +) + +type MigratorOption func(m *Migrator) + +func WithTableName(table string) MigratorOption { +	return func(m *Migrator) { +		m.table = table +	} +} + +func WithLocksTableName(table string) MigratorOption { +	return func(m *Migrator) { +		m.locksTable = table +	} +} + +type Migrator struct { +	db         *bun.DB +	migrations *Migrations + +	ms MigrationSlice + +	table      string +	locksTable string +} + +func NewMigrator(db *bun.DB, migrations *Migrations, opts ...MigratorOption) *Migrator { +	m := &Migrator{ +		db:         db, +		migrations: migrations, + +		ms: migrations.ms, + +		table:      "bun_migrations", +		locksTable: "bun_migration_locks", +	} +	for _, opt := range opts { +		opt(m) +	} +	return m +} + +func (m *Migrator) DB() *bun.DB { +	return m.db +} + +// MigrationsWithStatus returns migrations with status in ascending order. +func (m *Migrator) MigrationsWithStatus(ctx context.Context) (MigrationSlice, error) { +	sorted := m.migrations.Sorted() + +	applied, err := m.selectAppliedMigrations(ctx) +	if err != nil { +		return nil, err +	} + +	appliedMap := migrationMap(applied) +	for i := range sorted { +		m1 := &sorted[i] +		if m2, ok := appliedMap[m1.Name]; ok { +			m1.ID = m2.ID +			m1.GroupID = m2.GroupID +			m1.MigratedAt = m2.MigratedAt +		} +	} + +	return sorted, nil +} + +func (m *Migrator) Init(ctx context.Context) error { +	if _, err := m.db.NewCreateTable(). +		Model((*Migration)(nil)). +		ModelTableExpr(m.table). +		IfNotExists(). +		Exec(ctx); err != nil { +		return err +	} +	if _, err := m.db.NewCreateTable(). +		Model((*migrationLock)(nil)). +		ModelTableExpr(m.locksTable). +		IfNotExists(). +		Exec(ctx); err != nil { +		return err +	} +	return nil +} + +func (m *Migrator) Reset(ctx context.Context) error { +	if _, err := m.db.NewDropTable(). +		Model((*Migration)(nil)). +		ModelTableExpr(m.table). +		IfExists(). +		Exec(ctx); err != nil { +		return err +	} +	if _, err := m.db.NewDropTable(). +		Model((*migrationLock)(nil)). +		ModelTableExpr(m.locksTable). +		IfExists(). +		Exec(ctx); err != nil { +		return err +	} +	return m.Init(ctx) +} + +func (m *Migrator) Migrate(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) { +	cfg := newMigrationConfig(opts) + +	if err := m.validate(); err != nil { +		return nil, err +	} + +	if err := m.Lock(ctx); err != nil { +		return nil, err +	} +	defer m.Unlock(ctx) //nolint:errcheck + +	migrations, err := m.MigrationsWithStatus(ctx) +	if err != nil { +		return nil, err +	} + +	group := &MigrationGroup{ +		Migrations: migrations.Unapplied(), +	} +	if len(group.Migrations) == 0 { +		return group, nil +	} +	group.ID = migrations.LastGroupID() + 1 + +	for i := range group.Migrations { +		migration := &group.Migrations[i] +		migration.GroupID = group.ID + +		if !cfg.nop && migration.Up != nil { +			if err := migration.Up(ctx, m.db); err != nil { +				return nil, err +			} +		} + +		if err := m.MarkApplied(ctx, migration); err != nil { +			return nil, err +		} +	} + +	return group, nil +} + +func (m *Migrator) Rollback(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) { +	cfg := newMigrationConfig(opts) + +	if err := m.validate(); err != nil { +		return nil, err +	} + +	if err := m.Lock(ctx); err != nil { +		return nil, err +	} +	defer m.Unlock(ctx) //nolint:errcheck + +	migrations, err := m.MigrationsWithStatus(ctx) +	if err != nil { +		return nil, err +	} + +	lastGroup := migrations.LastGroup() + +	for i := len(lastGroup.Migrations) - 1; i >= 0; i-- { +		migration := &lastGroup.Migrations[i] + +		if !cfg.nop && migration.Down != nil { +			if err := migration.Down(ctx, m.db); err != nil { +				return nil, err +			} +		} + +		if err := m.MarkUnapplied(ctx, migration); err != nil { +			return nil, err +		} +	} + +	return lastGroup, nil +} + +type MigrationStatus struct { +	Migrations    MigrationSlice +	NewMigrations MigrationSlice +	LastGroup     *MigrationGroup +} + +func (m *Migrator) Status(ctx context.Context) (*MigrationStatus, error) { +	log.Printf( +		"DEPRECATED: bun: replace Status(ctx) with " + +			"MigrationsWithStatus(ctx)") + +	migrations, err := m.MigrationsWithStatus(ctx) +	if err != nil { +		return nil, err +	} +	return &MigrationStatus{ +		Migrations:    migrations, +		NewMigrations: migrations.Unapplied(), +		LastGroup:     migrations.LastGroup(), +	}, nil +} + +func (m *Migrator) MarkCompleted(ctx context.Context) (*MigrationGroup, error) { +	log.Printf( +		"DEPRECATED: bun: replace MarkCompleted(ctx) with " + +			"Migrate(ctx, migrate.WithNopMigration())") + +	return m.Migrate(ctx, WithNopMigration()) +} + +type goMigrationConfig struct { +	packageName string +} + +type GoMigrationOption func(cfg *goMigrationConfig) + +func WithPackageName(name string) GoMigrationOption { +	return func(cfg *goMigrationConfig) { +		cfg.packageName = name +	} +} + +// CreateGoMigration creates a Go migration file. +func (m *Migrator) CreateGoMigration( +	ctx context.Context, name string, opts ...GoMigrationOption, +) (*MigrationFile, error) { +	cfg := &goMigrationConfig{ +		packageName: "migrations", +	} +	for _, opt := range opts { +		opt(cfg) +	} + +	name, err := m.genMigrationName(name) +	if err != nil { +		return nil, err +	} + +	fname := name + ".go" +	fpath := filepath.Join(m.migrations.getDirectory(), fname) +	content := fmt.Sprintf(goTemplate, cfg.packageName) + +	if err := ioutil.WriteFile(fpath, []byte(content), 0o644); err != nil { +		return nil, err +	} + +	mf := &MigrationFile{ +		Name:    fname, +		Path:    fpath, +		Content: content, +	} +	return mf, nil +} + +// CreateSQLMigrations creates an up and down SQL migration files. +func (m *Migrator) CreateSQLMigrations(ctx context.Context, name string) ([]*MigrationFile, error) { +	name, err := m.genMigrationName(name) +	if err != nil { +		return nil, err +	} + +	up, err := m.createSQL(ctx, name+".up.sql") +	if err != nil { +		return nil, err +	} + +	down, err := m.createSQL(ctx, name+".down.sql") +	if err != nil { +		return nil, err +	} + +	return []*MigrationFile{up, down}, nil +} + +func (m *Migrator) createSQL(ctx context.Context, fname string) (*MigrationFile, error) { +	fpath := filepath.Join(m.migrations.getDirectory(), fname) + +	if err := ioutil.WriteFile(fpath, []byte(sqlTemplate), 0o644); err != nil { +		return nil, err +	} + +	mf := &MigrationFile{ +		Name:    fname, +		Path:    fpath, +		Content: goTemplate, +	} +	return mf, nil +} + +var nameRE = regexp.MustCompile(`^[0-9a-z_\-]+$`) + +func (m *Migrator) genMigrationName(name string) (string, error) { +	const timeFormat = "20060102150405" + +	if name == "" { +		return "", errors.New("migrate: migration name can't be empty") +	} +	if !nameRE.MatchString(name) { +		return "", fmt.Errorf("migrate: invalid migration name: %q", name) +	} + +	version := time.Now().UTC().Format(timeFormat) +	return fmt.Sprintf("%s_%s", version, name), nil +} + +// MarkApplied marks the migration as applied (applied). +func (m *Migrator) MarkApplied(ctx context.Context, migration *Migration) error { +	_, err := m.db.NewInsert().Model(migration). +		ModelTableExpr(m.table). +		Exec(ctx) +	return err +} + +// MarkUnapplied marks the migration as unapplied (new). +func (m *Migrator) MarkUnapplied(ctx context.Context, migration *Migration) error { +	_, err := m.db.NewDelete(). +		Model(migration). +		ModelTableExpr(m.table). +		Where("id = ?", migration.ID). +		Exec(ctx) +	return err +} + +// selectAppliedMigrations selects applied (applied) migrations in descending order. +func (m *Migrator) selectAppliedMigrations(ctx context.Context) (MigrationSlice, error) { +	var ms MigrationSlice +	if err := m.db.NewSelect(). +		ColumnExpr("*"). +		Model(&ms). +		ModelTableExpr(m.table). +		Scan(ctx); err != nil { +		return nil, err +	} +	return ms, nil +} + +func (m *Migrator) formattedTableName(db *bun.DB) string { +	return db.Formatter().FormatQuery(m.table) +} + +func (m *Migrator) validate() error { +	if len(m.ms) == 0 { +		return errors.New("migrate: there are no any migrations") +	} +	return nil +} + +//------------------------------------------------------------------------------ + +type migrationLock struct { +	ID        int64 +	TableName string `bun:",unique"` +} + +func (m *Migrator) Lock(ctx context.Context) error { +	lock := &migrationLock{ +		TableName: m.formattedTableName(m.db), +	} +	if _, err := m.db.NewInsert(). +		Model(lock). +		ModelTableExpr(m.locksTable). +		Exec(ctx); err != nil { +		return fmt.Errorf("migrate: migrations table is already locked (%w)", err) +	} +	return nil +} + +func (m *Migrator) Unlock(ctx context.Context) error { +	tableName := m.formattedTableName(m.db) +	_, err := m.db.NewDelete(). +		Model((*migrationLock)(nil)). +		ModelTableExpr(m.locksTable). +		Where("? = ?", bun.Ident("table_name"), tableName). +		Exec(ctx) +	return err +} + +func migrationMap(ms MigrationSlice) map[string]*Migration { +	mp := make(map[string]*Migration) +	for i := range ms { +		m := &ms[i] +		mp[m.Name] = m +	} +	return mp +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d98ddb2cf..f8bdb32e2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -404,6 +404,7 @@ github.com/uptrace/bun/extra/bunjson  github.com/uptrace/bun/internal  github.com/uptrace/bun/internal/parser  github.com/uptrace/bun/internal/tagparser +github.com/uptrace/bun/migrate  github.com/uptrace/bun/schema  # github.com/uptrace/bun/dialect/pgdialect v0.4.3  ## explicit  | 
