gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 4696e1a7b389599fa981f334b343daa911b11f5d
parent 684bd56528c5ffa37af8340b8b141fe390dd05a4
Author: tsmethurst <tobi.smethurst@klarrio.com>
Date:   Wed,  1 Sep 2021 18:29:25 +0200

moving stuff around

Diffstat:
Minternal/api/client/account/accountcreate.go | 12++++++------
Minternal/api/client/admin/emojicreate.go | 4++--
Minternal/api/client/auth/auth_test.go | 1-
Minternal/api/client/auth/callback.go | 6+++---
Minternal/api/client/status/statuscreate.go | 4++--
Minternal/api/client/streaming/stream.go | 8++++----
Minternal/cliactions/admin/account/account.go | 20++++++++++----------
Minternal/db/bundb/bundb.go | 3+++
Dinternal/db/bundb/migrations/20210816411877_struct_validation.go | 83-------------------------------------------------------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/account.go | 78------------------------------------------------------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/application.go | 32--------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/block.go | 15---------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/domainblock.go | 35-----------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/emaildomainblock.go | 31-------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/emoji.go | 45---------------------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/follow.go | 35-----------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/followrequest.go | 35-----------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/instance.go | 25-------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/mediaattachment.go | 117-------------------------------------------------------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/mention.go | 59-----------------------------------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/notification.go | 49-------------------------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/routersession.go | 26--------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/status.go | 116-------------------------------------------------------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/statusbookmark.go | 33---------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/statusfave.go | 34----------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/statusmute.go | 33---------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/tag.go | 34----------------------------------
Dinternal/db/bundb/migrations/20210816411877_struct_validation/user.go | 70----------------------------------------------------------------------
Minternal/db/bundb/migrations/README.md | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Minternal/gtsmodel/account.go | 35++++++++++++++++++++++++++---------
Minternal/gtsmodel/application.go | 19+++++++++++--------
Minternal/gtsmodel/block.go | 34++++++++++++++++++++++++++--------
Dinternal/gtsmodel/block_test.go | 115-------------------------------------------------------------------------------
Minternal/gtsmodel/client.go | 32+++++++++++++++++++++++++++-----
Minternal/gtsmodel/domainblock.go | 20++++++++++----------
Dinternal/gtsmodel/domainblock_test.go | 121-------------------------------------------------------------------------------
Minternal/gtsmodel/emaildomainblock.go | 12++++++------
Dinternal/gtsmodel/emaildomainblock_test.go | 96-------------------------------------------------------------------------------
Minternal/gtsmodel/emoji.go | 6+++---
Dinternal/gtsmodel/emoji_test.go | 194-------------------------------------------------------------------------------
Minternal/gtsmodel/follow.go | 20++++++++++----------
Dinternal/gtsmodel/follow_test.go | 87-------------------------------------------------------------------------------
Minternal/gtsmodel/followrequest.go | 20++++++++++----------
Dinternal/gtsmodel/followrequest_test.go | 87-------------------------------------------------------------------------------
Minternal/gtsmodel/instance.go | 24+++++++++++++++++++++---
Dinternal/gtsmodel/instance_test.go | 145-------------------------------------------------------------------------------
Minternal/gtsmodel/mediaattachment.go | 24++++++++++++------------
Dinternal/gtsmodel/mediaattachment_test.go | 229-------------------------------------------------------------------------------
Minternal/gtsmodel/mention.go | 22+++++++++++-----------
Dinternal/gtsmodel/mention_test.go | 101-------------------------------------------------------------------------------
Minternal/gtsmodel/notification.go | 5+++--
Dinternal/gtsmodel/notification_test.go | 97-------------------------------------------------------------------------------
Dinternal/gtsmodel/poll.go | 19-------------------
Dinternal/gtsmodel/relationship.go | 36------------------------------------
Minternal/gtsmodel/routersession.go | 10+++++++---
Dinternal/gtsmodel/routersession_test.go | 87-------------------------------------------------------------------------------
Minternal/gtsmodel/status.go | 12++++++------
Dinternal/gtsmodel/status_test.go | 162-------------------------------------------------------------------------------
Minternal/gtsmodel/statusbookmark.go | 17+++++++++--------
Dinternal/gtsmodel/statusbookmark_test.go | 87-------------------------------------------------------------------------------
Minternal/gtsmodel/statusfave.go | 19++++++++++---------
Dinternal/gtsmodel/statusfave_test.go | 100-------------------------------------------------------------------------------
Minternal/gtsmodel/statusmute.go | 17+++++++++--------
Dinternal/gtsmodel/statusmute_test.go | 87-------------------------------------------------------------------------------
Dinternal/gtsmodel/stream.go | 38--------------------------------------
Minternal/gtsmodel/tag.go | 18+++++++++---------
Dinternal/gtsmodel/tag_test.go | 92-------------------------------------------------------------------------------
Minternal/gtsmodel/token.go | 43++++++++++++++++++-------------------------
Minternal/gtsmodel/user.go | 71++++++++++++++++++++++++++++++-----------------------------------------
Dinternal/gtsmodel/user_test.go | 124-------------------------------------------------------------------------------
Dinternal/gtsmodel/validate.go | 75---------------------------------------------------------------------------
Dinternal/gtsmodel/validate_test.go | 64----------------------------------------------------------------
Minternal/oauth/clientstore_test.go | 4++--
Minternal/processing/account/update.go | 10+++++-----
Minternal/processing/app.go | 2--
Minternal/processing/instance.go | 12++++++------
Minternal/processing/processor.go | 3++-
Minternal/processing/streaming.go | 3++-
Minternal/processing/streaming/openstream.go | 19++++++++++---------
Minternal/processing/streaming/streamdelete.go | 16++++++++--------
Minternal/processing/streaming/streaming.go | 3++-
Minternal/processing/streaming/streamnotification.go | 17+++++++++--------
Minternal/processing/streaming/streamstatus.go | 17+++++++++--------
Ainternal/regexes/regexes.go | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/stream/stream.go | 38++++++++++++++++++++++++++++++++++++++
Minternal/text/common.go | 4++--
Minternal/transport/derefinstance.go | 3++-
Minternal/typeutils/internaltofrontend.go | 1-
Dinternal/util/regexes.go | 114-------------------------------------------------------------------------------
Minternal/util/statustools.go | 12+++++++-----
Minternal/util/uri.go | 42++++++++++++++++++++++--------------------
Dinternal/util/validation.go | 178-------------------------------------------------------------------------------
Dinternal/util/validation_test.go | 283-------------------------------------------------------------------------------
Ainternal/validate/block_test.go | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/domainblock_test.go | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/emaildomainblock_test.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/emoji_test.go | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/follow_test.go | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/followrequest_test.go | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/formvalidation.go | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/formvalidation_test.go | 283+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/instance_test.go | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/mediaattachment_test.go | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/mention_test.go | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/notification_test.go | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/routersession_test.go | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/status_test.go | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/statusbookmark_test.go | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/statusfave_test.go | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/statusmute_test.go | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/structvalidation.go | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/structvalidation_test.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/tag_test.go | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/validate/user_test.go | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtestrig/testmodels.go | 3---
115 files changed, 3249 insertions(+), 4121 deletions(-)

diff --git 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 @@ -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 @@ -95,7 +95,6 @@ func (suite *AuthTestSuite) SetupSuite() { ClientID: "a-known-client-id", ClientSecret: "some-secret", Scopes: "read", - VapidKey: uuid.NewString(), } } diff --git 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/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 @@ -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/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/db/bundb/bundb.go b/internal/db/bundb/bundb.go @@ -86,6 +86,9 @@ func doMigration(ctx context.Context, db *bun.DB, log *logrus.Logger) error { group, err := migrator.Migrate(ctx) if err != nil { + if err.Error() == "migrate: there are no any migrations" { + return nil + } return err } diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation.go b/internal/db/bundb/migrations/20210816411877_struct_validation.go @@ -1,83 +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 migrations - -import ( - "context" - - gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20210816411877_struct_validation" - "github.com/uptrace/bun" -) - -func init() { - - var models []interface{} = []interface{}{ - &gtsmodel.Account{}, - &gtsmodel.Application{}, - &gtsmodel.Block{}, - &gtsmodel.DomainBlock{}, - &gtsmodel.EmailDomainBlock{}, - &gtsmodel.Follow{}, - &gtsmodel.FollowRequest{}, - &gtsmodel.MediaAttachment{}, - &gtsmodel.Mention{}, - &gtsmodel.Status{}, - &gtsmodel.StatusToEmoji{}, - &gtsmodel.StatusToTag{}, - &gtsmodel.StatusFave{}, - &gtsmodel.StatusBookmark{}, - &gtsmodel.StatusMute{}, - &gtsmodel.Tag{}, - &gtsmodel.User{}, - &gtsmodel.Emoji{}, - &gtsmodel.Instance{}, - &gtsmodel.Notification{}, - &gtsmodel.RouterSession{}, - &gtsmodel.Token{}, - &gtsmodel.Client{}, - } - - up := func(ctx context.Context, db *bun.DB) error { - return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { - for _, m := range models { - _, err := tx.NewCreateTable().Model(m).IfNotExists().Exec(ctx) - if err != nil { - return err - } - } - - return nil - }) - } - - down := func(ctx context.Context, db *bun.DB) error { - return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { - _, err := tx.NewDropTable().Model(&gtsmodel.Account{}).Exec(ctx) - if err != nil { - return err - } - - return nil - }) - } - - if err := Migrations.Register(up, down); err != nil { - panic(err) - } -} diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation/account.go b/internal/db/bundb/migrations/20210816411877_struct_validation/account.go @@ -1,78 +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 - -import ( - "crypto/rsa" - "time" -) - -// Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc). -type Account 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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:"oneof=public unlocked followers_only mutuals_only direct" bun:",nullzero,notnull,default:'public'"` // 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:"-" 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:"omitempty,url" bun:",unique,nullzero"` // Web URL for this account's profile - LastWebfingeredAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // Last time this account was refreshed/located with webfinger. - InboxURI string `validate:"omitempty,url" bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to - OutboxURI string `validate:"omitempty,url" bun:",nullzero,unique"` // Address of this account's activitypub outbox - FollowingURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the following list of this account - FollowersURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the followers list of this account - FeaturedCollectionURI string `validate:"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" bun:",nullzero,notnull"` // Web-reachable location of this account's public key - SensitizedAt time.Time `validate:"-" bun:",nullzero"` // When was this account set to have all its media shown as sensitive? - SilencedAt time.Time `validate:"-" bun:",nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)? - SuspendedAt time.Time `validate:"-" bun:",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 `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). -} diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation/application.go b/internal/db/bundb/migrations/20210816411877_struct_validation/application.go @@ -1,32 +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 - -// 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 string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull"` // id of this application in the db - 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" bun:",nullzero,notnull"` // redirect uri requested by the application for oauth2 flow - ClientID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // 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,default:'read'"` // scopes requested when this app was created - VapidKey string `validate:"-" bun:",nullzero"` // a vapid key generated for this app when it was created -} diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation/block.go b/internal/db/bundb/migrations/20210816411877_struct_validation/block.go @@ -1,15 +0,0 @@ -package gtsmodel - -import "time" - -// Block refers to the blocking of one account by another. -type Block 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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/db/bundb/migrations/20210816411877_struct_validation/domainblock.go b/internal/db/bundb/migrations/20210816411877_struct_validation/domainblock.go @@ -1,35 +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 - -import "time" - -// DomainBlock represents a federation block against a particular domain -type DomainBlock 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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/db/bundb/migrations/20210816411877_struct_validation/emaildomainblock.go b/internal/db/bundb/migrations/20210816411877_struct_validation/emaildomainblock.go @@ -1,31 +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 - -import "time" - -// EmailDomainBlock represents a domain that the server should automatically reject sign-up requests from. -type EmailDomainBlock 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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/db/bundb/migrations/20210816411877_struct_validation/emoji.go b/internal/db/bundb/migrations/20210816411877_struct_validation/emoji.go @@ -1,45 +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 - -import "time" - -// Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens. -type Emoji 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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:",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/db/bundb/migrations/20210816411877_struct_validation/follow.go b/internal/db/bundb/migrations/20210816411877_struct_validation/follow.go @@ -1,35 +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 - -import "time" - -// Follow represents one account following another, and the metadata around that follow. -type Follow 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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/db/bundb/migrations/20210816411877_struct_validation/followrequest.go b/internal/db/bundb/migrations/20210816411877_struct_validation/followrequest.go @@ -1,35 +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 - -import "time" - -// FollowRequest represents one account requesting to follow another, and the metadata around that request. -type FollowRequest 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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/db/bundb/migrations/20210816411877_struct_validation/instance.go b/internal/db/bundb/migrations/20210816411877_struct_validation/instance.go @@ -1,25 +0,0 @@ -package gtsmodel - -import "time" - -// Instance represents a federated instance, either local or remote. -type Instance 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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:",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/db/bundb/migrations/20210816411877_struct_validation/mediaattachment.go b/internal/db/bundb/migrations/20210816411877_struct_validation/mediaattachment.go @@ -1,117 +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 - -import ( - "time" -) - -// 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 string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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 { - 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:",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 { - 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:",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 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 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 `validate:"required"` - Small Small - Focus Focus -} - -// Small can be used for a thumbnail of any media type -type Small struct { - 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 `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 `validate:"omitempty,max=1,min=-1"` - Y float32 `validate:"omitempty,max=1,min=-1"` -} diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation/mention.go b/internal/db/bundb/migrations/20210816411877_struct_validation/mention.go @@ -1,59 +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 - -import "time" - -// Mention refers to the 'tagging' or 'mention' of a user within a status. -type Mention 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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 - These fields are just for convenience while passing the mention - around internally, to make fewer database calls and whatnot. They're - not meant to be put in the database! - */ - - // NameString is for putting in the namestring of the mentioned user - // before the mention is dereferenced. Should be in a form along the lines of: - // @whatever_username@example.org - // - // This will not be put in the database, it's just for convenience. - 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 `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 `validate:"-" bun:"-"` - // A pointer to the gtsmodel account of the mentioned account. -} diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation/notification.go b/internal/db/bundb/migrations/20210816411877_struct_validation/notification.go @@ -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 - -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 string `validate:"ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // 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 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/db/bundb/migrations/20210816411877_struct_validation/routersession.go b/internal/db/bundb/migrations/20210816411877_struct_validation/routersession.go @@ -1,26 +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 - -// RouterSession is used to store and retrieve settings for a router session. -type RouterSession struct { - ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull"` - 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/db/bundb/migrations/20210816411877_struct_validation/status.go b/internal/db/bundb/migrations/20210816411877_struct_validation/status.go @@ -1,116 +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 - -import ( - "time" -) - -// Status represents a user-created 'post' or 'status' in the database, either remote or local -type Status 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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,nullzero"` // 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,nullzero"` // 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,nullzero"` // 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,nullzero"` // 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 `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 `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. -type Visibility string - -const ( - // VisibilityPublic means this status will be visible to everyone on all timelines. - VisibilityPublic Visibility = "public" - // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. - VisibilityUnlocked Visibility = "unlocked" - // VisibilityFollowersOnly means this status is viewable to followers only. - VisibilityFollowersOnly Visibility = "followers_only" - // VisibilityMutualsOnly means this status is visible to mutual followers only. - VisibilityMutualsOnly Visibility = "mutuals_only" - // VisibilityDirect means this status is visible only to mentioned recipients. - VisibilityDirect Visibility = "direct" - // VisibilityDefault is used when no other setting can be found. - VisibilityDefault Visibility = VisibilityUnlocked -) - -// VisibilityAdvanced models flags for fine-tuning visibility and interactivity of a status. -// -// All flags default to true. -// -// If PUBLIC is selected, flags will all be overwritten to TRUE regardless of what is selected. -// -// If UNLOCKED is selected, any flags can be turned on or off in any combination. -// -// If FOLLOWERS-ONLY or MUTUALS-ONLY are selected, boostable will always be FALSE. Other flags can be turned on or off as desired. -// -// If DIRECT is selected, boostable will be FALSE, and all other flags will be TRUE. -type VisibilityAdvanced struct { - 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/db/bundb/migrations/20210816411877_struct_validation/statusbookmark.go b/internal/db/bundb/migrations/20210816411877_struct_validation/statusbookmark.go @@ -1,33 +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 - -import "time" - -// StatusBookmark refers to one account having a 'bookmark' of the status of another account. -type StatusBookmark 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:",nullzero,notnull,default:current_timestamp"` // when was item created - 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/db/bundb/migrations/20210816411877_struct_validation/statusfave.go b/internal/db/bundb/migrations/20210816411877_struct_validation/statusfave.go @@ -1,34 +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 - -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 string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created - 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/db/bundb/migrations/20210816411877_struct_validation/statusmute.go b/internal/db/bundb/migrations/20210816411877_struct_validation/statusmute.go @@ -1,33 +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 - -import "time" - -// StatusMute refers to one account having muted the status of another account or its own. -type StatusMute 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:",nullzero,notnull,default:current_timestamp"` // when was item created - 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/db/bundb/migrations/20210816411877_struct_validation/tag.go b/internal/db/bundb/migrations/20210816411877_struct_validation/tag.go @@ -1,34 +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 - -import "time" - -// Tag represents a hashtag for gathering public statuses together. -type Tag 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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:",nullzero,notnull,default:current_timestamp"` // when was this tag last used? -} diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation/user.go b/internal/db/bundb/migrations/20210816411877_struct_validation/user.go @@ -1,70 +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 - -import ( - "net" - "time" -) - -// 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 { - 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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:",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:",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:"-" 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:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // 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:",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:",nullzero"` // When did we send email confirmation to this user? - ConfirmedAt time.Time `validate:"required_with=Email" bun:",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:",nullzero"` // When did we email the user their reset-password email? - - EncryptedOTPSecret string `validate:"-" bun:",nullzero"` - EncryptedOTPSecretIv string `validate:"-" bun:",nullzero"` - EncryptedOTPSecretSalt string `validate:"-" bun:",nullzero"` - OTPRequiredForLogin bool `validate:"-" bun:",notnull,default:false"` - OTPBackupCodes []string `validate:"-" bun:",nullzero"` - ConsumedTimestamp int `validate:"-" bun:",nullzero"` - RememberToken string `validate:"-" bun:",nullzero"` - SignInToken string `validate:"-" bun:",nullzero"` - SignInTokenSentAt time.Time `validate:"-" bun:",nullzero"` - WebauthnID string `validate:"-" bun:",nullzero"` -} diff --git a/internal/db/bundb/migrations/README.md b/internal/db/bundb/migrations/README.md @@ -4,7 +4,55 @@ [See here](https://bun.uptrace.dev/guide/migrations.html#migration-names) -As a template, take one of the existing migration files and modify it. It will be automatically loaded and handled by Bun. +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 @@ -19,3 +67,4 @@ echo "$(date --utc +%Y%m%H%M%S%N | head -c 14)_$(git rev-parse --abbrev-ref HEAD ## 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/account.go b/internal/gtsmodel/account.go @@ -30,8 +30,8 @@ import ( // Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc). type Account 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated + 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 @@ -54,20 +54,20 @@ type Account struct { Sensitive bool `validate:"-" bun:",nullzero,default:false"` // Set posts from this account to sensitive by default? Language string `validate:"-" 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:"omitempty,url" bun:",unique,nullzero"` // Web URL for this account's profile - LastWebfingeredAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // Last time this account was refreshed/located with webfinger. + URL string `validate:"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:"omitempty,url" bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to OutboxURI string `validate:"omitempty,url" bun:",nullzero,unique"` // Address of this account's activitypub outbox FollowingURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the following list of this account FollowersURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the followers list of this account FeaturedCollectionURI string `validate:"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? + 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" bun:",nullzero,notnull"` // Web-reachable location of this account's public key - SensitizedAt time.Time `validate:"-" bun:",nullzero"` // When was this account set to have all its media shown as sensitive? - SilencedAt time.Time `validate:"-" bun:",nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)? - SuspendedAt time.Time `validate:"-" bun:",nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) + 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 } @@ -80,3 +80,20 @@ type Field struct { 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/application.go b/internal/gtsmodel/application.go @@ -18,15 +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 string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull"` // id of this application in the db - 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" bun:",nullzero,notnull"` // redirect uri requested by the application for oauth2 flow - ClientID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // 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,default:'read'"` // scopes requested when this app was created - VapidKey string `validate:"-" bun:",nullzero"` // a vapid key generated for this app when it was created + 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" bun:",nullzero,notnull"` // redirect uri requested by the application for oauth2 flow + ClientID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // 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,default:'read'"` // scopes requested when this app was created } diff --git a/internal/gtsmodel/block.go b/internal/gtsmodel/block.go @@ -1,15 +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 string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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 + 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/block_test.go b/internal/gtsmodel/block_test.go @@ -1,115 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyBlock() *gtsmodel.Block { - return &gtsmodel.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 - d := happyBlock() - err := gtsmodel.ValidateStruct(*d) - suite.NoError(err) -} - -func (suite *BlockValidateTestSuite) TestValidateBlockBadID() { - d := happyBlock() - - d.ID = "" - err := gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - d.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") -} - -func (suite *BlockValidateTestSuite) TestValidateBlockNoCreatedAt() { - d := happyBlock() - - d.CreatedAt = time.Time{} - err := gtsmodel.ValidateStruct(*d) - suite.NoError(err) -} - -func (suite *BlockValidateTestSuite) TestValidateBlockCreatedByAccountID() { - d := happyBlock() - - d.AccountID = "" - err := gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag") - - d.AccountID = "this-is-not-a-valid-ulid" - err = gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'ulid' tag") -} - -func (suite *BlockValidateTestSuite) TestValidateBlockTargetAccountID() { - d := happyBlock() - - d.TargetAccountID = "invalid-ulid" - err := gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'ulid' tag") - - d.TargetAccountID = "01FEEDHX4G7EGHF5GD9E82Y51Q" - err = gtsmodel.ValidateStruct(*d) - suite.NoError(err) - - d.TargetAccountID = "" - err = gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'required' tag") -} - -func (suite *BlockValidateTestSuite) TestValidateBlockURI() { - d := happyBlock() - - d.URI = "invalid-uri" - err := gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'Block.URI' Error:Field validation for 'URI' failed on the 'url' tag") - - d.URI = "" - err = gtsmodel.ValidateStruct(*d) - 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/gtsmodel/client.go b/internal/gtsmodel/client.go @@ -1,9 +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 -// Client is a handy little wrapper for typical oauth client details +import "time" + +// Client is a wrapper for OAuth client details. type Client struct { - ID string `bun:"type:CHAR(26),pk,notnull"` - Secret string - Domain string - UserID string + 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" 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 @@ -22,14 +22,14 @@ import "time" // DomainBlock represents a federation block against a particular domain type DomainBlock 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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? + 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/domainblock_test.go b/internal/gtsmodel/domainblock_test.go @@ -1,121 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyDomainBlock() *gtsmodel.DomainBlock { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*d) - suite.NoError(err) -} - -func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockBadID() { - d := happyDomainBlock() - - d.ID = "" - err := gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'DomainBlock.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - d.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*d) - suite.NoError(err) -} - -func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockBadDomain() { - d := happyDomainBlock() - - d.Domain = "" - err := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*d) - suite.NoError(err) -} - -func (suite *DomainBlockValidateTestSuite) TestValidateDomainSubscriptionID() { - d := happyDomainBlock() - - d.SubscriptionID = "invalid-ulid" - err := gtsmodel.ValidateStruct(*d) - suite.EqualError(err, "Key: 'DomainBlock.SubscriptionID' Error:Field validation for 'SubscriptionID' failed on the 'ulid' tag") - - d.SubscriptionID = "01FEEDHX4G7EGHF5GD9E82Y51Q" - err = gtsmodel.ValidateStruct(*d) - suite.NoError(err) -} - -func TestDomainBlockValidateTestSuite(t *testing.T) { - suite.Run(t, new(DomainBlockValidateTestSuite)) -} diff --git a/internal/gtsmodel/emaildomainblock.go b/internal/gtsmodel/emaildomainblock.go @@ -22,10 +22,10 @@ import "time" // EmailDomainBlock represents a domain that the server should automatically reject sign-up requests from. type EmailDomainBlock 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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 + 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/emaildomainblock_test.go b/internal/gtsmodel/emaildomainblock_test.go @@ -1,96 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyEmailDomainBlock() *gtsmodel.EmailDomainBlock { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*e) - suite.NoError(err) -} - -func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockBadID() { - e := happyEmailDomainBlock() - - e.ID = "" - err := gtsmodel.ValidateStruct(*e) - suite.EqualError(err, "Key: 'EmailDomainBlock.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - e.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*e) - suite.NoError(err) -} - -func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockBadDomain() { - e := happyEmailDomainBlock() - - e.Domain = "" - err := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go @@ -23,8 +23,8 @@ import "time" // Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens. type Emoji 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated + 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. @@ -37,7 +37,7 @@ type Emoji struct { 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:",nullzero,notnull,default:current_timestamp"` // When was the emoji image last updated? + 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? diff --git a/internal/gtsmodel/emoji_test.go b/internal/gtsmodel/emoji_test.go @@ -1,194 +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_test - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -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 &gtsmodel.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 := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *EmojiValidateTestSuite) TestValidateEmojiBadFilePaths() { - e := happyEmoji() - - e.ImagePath = "/tmp/nonexistent/file/for/gotosocial/test" - err := gtsmodel.ValidateStruct(*e) - suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'file' tag") - - e.ImagePath = "" - err = gtsmodel.ValidateStruct(*e) - suite.EqualError(err, "Key: 'Emoji.ImagePath' Error:Field validation for 'ImagePath' failed on the 'required' tag") - - e.ImagePath = "???????????thisnot a valid path####" - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*e) - suite.EqualError(err, "Key: 'Emoji.URI' Error:Field validation for 'URI' failed on the 'url' tag") - - e.URI = "" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*e) - suite.NoError(err) - - e.ImageStaticRemoteURL = "" - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*e) - suite.NoError(err) - - e.ImageURL = "" - e.ImageStaticURL = "" - e.ImageRemoteURL = "" - e.ImageStaticRemoteURL = "" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*e) - suite.EqualError(err, "Key: 'Emoji.ImageFileSize' Error:Field validation for 'ImageFileSize' failed on the 'required' tag") - - e.ImageStaticFileSize = 0 - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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/gtsmodel/follow.go b/internal/gtsmodel/follow.go @@ -22,14 +22,14 @@ import "time" // Follow represents one account following another, and the metadata around that follow. type Follow 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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? + 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/follow_test.go b/internal/gtsmodel/follow_test.go @@ -1,87 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyFollow() *gtsmodel.Follow { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*f) - suite.NoError(err) -} - -func (suite *FollowValidateTestSuite) TestValidateFollowBadID() { - f := happyFollow() - - f.ID = "" - err := gtsmodel.ValidateStruct(*f) - suite.EqualError(err, "Key: 'Follow.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - f.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*f) - suite.NoError(err) -} - -func (suite *FollowValidateTestSuite) TestValidateFollowNoURI() { - f := happyFollow() - - f.URI = "" - err := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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/gtsmodel/followrequest.go b/internal/gtsmodel/followrequest.go @@ -22,14 +22,14 @@ import "time" // FollowRequest represents one account requesting to follow another, and the metadata around that request. type FollowRequest 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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? + 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/followrequest_test.go b/internal/gtsmodel/followrequest_test.go @@ -1,87 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyFollowRequest() *gtsmodel.FollowRequest { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*f) - suite.NoError(err) -} - -func (suite *FollowRequestValidateTestSuite) TestValidateFollowRequestBadID() { - f := happyFollowRequest() - - f.ID = "" - err := gtsmodel.ValidateStruct(*f) - suite.EqualError(err, "Key: 'FollowRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - f.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*f) - suite.NoError(err) -} - -func (suite *FollowRequestValidateTestSuite) TestValidateFollowRequestNoURI() { - f := happyFollowRequest() - - f.URI = "" - err := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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/gtsmodel/instance.go b/internal/gtsmodel/instance.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + package gtsmodel import "time" @@ -5,12 +23,12 @@ import "time" // Instance represents a federated instance, either local or remote. type Instance 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated + 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:",nullzero"` // When was this instance suspended, if at all? + 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 diff --git a/internal/gtsmodel/instance_test.go b/internal/gtsmodel/instance_test.go @@ -1,145 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyInstance() *gtsmodel.Instance { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *InstanceValidateTestSuite) TestValidateInstanceBadID() { - m := happyInstance() - - m.ID = "" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Instance.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*i) - suite.EqualError(err, "Key: 'Instance.URI' Error:Field validation for 'URI' failed on the 'required' tag") - - i.URI = "---------------------------" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*i) - suite.EqualError(err, "Key: 'Instance.ContactAccountID' Error:Field validation for 'ContactAccountID' failed on the 'ulid' tag") - - i.ContactAccountID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" - err = gtsmodel.ValidateStruct(*i) - suite.EqualError(err, "Key: 'Instance.ContactAccountID' Error:Field validation for 'ContactAccountID' failed on the 'ulid' tag") - - i.ContactAccountID = "" - err = gtsmodel.ValidateStruct(*i) - suite.EqualError(err, "Key: 'Instance.ContactAccountID' Error:Field validation for 'ContactAccountID' failed on the 'required_with' tag") - - i.ContactAccountUsername = "" - err = gtsmodel.ValidateStruct(*i) - suite.NoError(err) -} - -func (suite *InstanceValidateTestSuite) TestValidateInstanceDomain() { - i := happyInstance() - - i.Domain = "poopoo" - err := gtsmodel.ValidateStruct(*i) - suite.EqualError(err, "Key: 'Instance.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag") - - i.Domain = "" - err = gtsmodel.ValidateStruct(*i) - suite.EqualError(err, "Key: 'Instance.Domain' Error:Field validation for 'Domain' failed on the 'required' tag") - - i.Domain = "https://aaaaaaaaaaaaah.org" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*i) - suite.EqualError(err, "Key: 'Instance.ContactEmail' Error:Field validation for 'ContactEmail' failed on the 'email' tag") - - i.ContactEmail = "" - err = gtsmodel.ValidateStruct(*i) - suite.NoError(err) -} - -func (suite *InstanceValidateTestSuite) TestValidateInstanceNoCreatedAt() { - i := happyInstance() - - i.CreatedAt = time.Time{} - err := gtsmodel.ValidateStruct(*i) - suite.NoError(err) -} - -func TestInstanceValidateTestSuite(t *testing.T) { - suite.Run(t, new(InstanceValidateTestSuite)) -} diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go @@ -26,8 +26,8 @@ import ( // somewhere in storage and that can be retrieved and served by the router. type MediaAttachment 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated + 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) @@ -47,20 +47,20 @@ type MediaAttachment struct { // File refers to the metadata for the whole file type File struct { - 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:",nullzero,notnull,default:current_timestamp"` // When was the file last updated. + 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 { - 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:",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) + 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. diff --git a/internal/gtsmodel/mediaattachment_test.go b/internal/gtsmodel/mediaattachment_test.go @@ -1,229 +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_test - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -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 &gtsmodel.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 := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentBadFilePaths() { - m := happyMediaAttachment() - - m.File.Path = "/tmp/nonexistent/file/for/gotosocial/test" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'MediaAttachment.File.Path' Error:Field validation for 'Path' failed on the 'file' tag") - - m.File.Path = "" - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'MediaAttachment.Type' Error:Field validation for 'Type' failed on the 'oneof' tag") - - m.Type = "Not Supported" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*m) - suite.NoError(err) - - m.FileMeta.Focus.X = 3.6 - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'MediaAttachment.URL' Error:Field validation for 'URL' failed on the 'url' tag") - - m.URL = "" - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentBlurhash() { - m := happyMediaAttachment() - - m.Blurhash = "" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'MediaAttachment.Blurhash' Error:Field validation for 'Blurhash' failed on the 'required_if' tag") - - m.Type = gtsmodel.FileTypeAudio - err = gtsmodel.ValidateStruct(*m) - suite.NoError(err) - - m.Blurhash = "some_blurhash" - err = gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *MediaAttachmentValidateTestSuite) TestValidateMediaAttachmentProcessing() { - m := happyMediaAttachment() - - m.Processing = 420 - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'MediaAttachment.Processing' Error:Field validation for 'Processing' failed on the 'oneof' tag") - - m.Processing = -5 - err = gtsmodel.ValidateStruct(*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/gtsmodel/mention.go b/internal/gtsmodel/mention.go @@ -22,17 +22,17 @@ import "time" // Mention refers to the 'tagging' or 'mention' of a user within a status. type Mention 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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? + 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 diff --git a/internal/gtsmodel/mention_test.go b/internal/gtsmodel/mention_test.go @@ -1,101 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyMention() *gtsmodel.Mention { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *MentionValidateTestSuite) TestValidateMentionBadID() { - m := happyMention() - - m.ID = "" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Mention.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Mention.OriginAccountURI' Error:Field validation for 'OriginAccountURI' failed on the 'url' tag") - - m.OriginAccountURI = "---------------------------" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Mention.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") - - m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func TestMentionValidateTestSuite(t *testing.T) { - suite.Run(t, new(MentionValidateTestSuite)) -} diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go @@ -22,8 +22,9 @@ 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 string `validate:"ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created + 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? diff --git a/internal/gtsmodel/notification_test.go b/internal/gtsmodel/notification_test.go @@ -1,97 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyNotification() *gtsmodel.Notification { - return &gtsmodel.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 - m := happyNotification() - err := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *NotificationValidateTestSuite) TestValidateNotificationBadID() { - m := happyNotification() - - m.ID = "" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Notification.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") - - m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Notification.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") -} - -func (suite *NotificationValidateTestSuite) TestValidateNotificationStatusID() { - m := happyNotification() - - m.StatusID = "" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'required_if' tag") - - m.StatusID = "9HZJ76B6VXSKF" - err = gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") - - m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" - err = gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") - - m.StatusID = "" - m.NotificationType = gtsmodel.NotificationFollowRequest - err = gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *NotificationValidateTestSuite) TestValidateNotificationNoCreatedAt() { - m := happyNotification() - - m.CreatedAt = time.Time{} - err := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func TestNotificationValidateTestSuite(t *testing.T) { - suite.Run(t, new(NotificationValidateTestSuite)) -} diff --git a/internal/gtsmodel/poll.go b/internal/gtsmodel/poll.go @@ -1,19 +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 diff --git a/internal/gtsmodel/relationship.go b/internal/gtsmodel/relationship.go @@ -1,36 +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 { - 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/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 `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull"` - Auth []byte `validate:"required,len=32" bun:"type:bytea,notnull,nullzero"` - Crypt []byte `validate:"required,len=32" 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/routersession_test.go b/internal/gtsmodel/routersession_test.go @@ -1,87 +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_test - -import ( - "testing" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyRouterSession() *gtsmodel.RouterSession { - return &gtsmodel.RouterSession{ - ID: "01FE91RJR88PSEEE30EV35QR8N", - Auth: []byte("12345678901234567890123456789012"), - Crypt: []byte("12345678901234567890123456789012"), - } -} - -type RouterSessionValidateTestSuite struct { - suite.Suite -} - -func (suite *RouterSessionValidateTestSuite) TestValidateRouterSessionHappyPath() { - // no problem here - r := happyRouterSession() - err := gtsmodel.ValidateStruct(*r) - suite.NoError(err) -} - -func (suite *RouterSessionValidateTestSuite) TestValidateRouterSessionAuth() { - r := happyRouterSession() - - // remove auth struct - r.Auth = nil - err := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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/gtsmodel/status.go b/internal/gtsmodel/status.go @@ -25,18 +25,18 @@ import ( // Status represents a user-created 'post' or 'status' in the database, either remote or local type Status 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated + 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,nullzero"` // Database IDs of any media attachments associated with this status + 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,nullzero"` // Database IDs of any tags used in this status + 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,nullzero"` // Database IDs of any mentions in this status + 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,nullzero"` // Database IDs of any emojis used in this status + 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? diff --git a/internal/gtsmodel/status_test.go b/internal/gtsmodel/status_test.go @@ -1,162 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyStatus() *gtsmodel.Status { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*s) - suite.NoError(err) -} - -func (suite *StatusValidateTestSuite) TestValidateStatusBadID() { - s := happyStatus() - - s.ID = "" - err := gtsmodel.ValidateStruct(*s) - suite.EqualError(err, "Key: 'Status.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - s.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*s) - suite.EqualError(err, "Key: 'Status.AttachmentIDs[0]' Error:Field validation for 'AttachmentIDs[0]' failed on the 'ulid' tag") - - s.AttachmentIDs[0] = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*s) - suite.EqualError(err, "Key: 'Status.AttachmentIDs[0]' Error:Field validation for 'AttachmentIDs[0]' failed on the 'ulid' tag") - - s.AttachmentIDs[1] = "" - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*s) - suite.NoError(err) - - s.AttachmentIDs = nil - err = gtsmodel.ValidateStruct(*s) - suite.NoError(err) -} - -func (suite *StatusValidateTestSuite) TestStatusApplicationID() { - s := happyStatus() - - s.CreatedWithApplicationID = "" - err := gtsmodel.ValidateStruct(*s) - suite.EqualError(err, "Key: 'Status.CreatedWithApplicationID' Error:Field validation for 'CreatedWithApplicationID' failed on the 'required_if' tag") - - s.Local = false - err = gtsmodel.ValidateStruct(*s) - suite.NoError(err) -} - -func (suite *StatusValidateTestSuite) TestValidateStatusReplyFields() { - s := happyStatus() - - s.InReplyToAccountID = "01FEBCTP6DN7961PN81C3DVM4N " - err := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*s) - suite.EqualError(err, "Key: 'Status.InReplyToID' Error:Field validation for 'InReplyToID' failed on the 'ulid' tag") - - s.InReplyToID = "01FEBD07E72DEY6YB9K10ZA6ST" - err = gtsmodel.ValidateStruct(*s) - suite.NoError(err) -} - -func TestStatusValidateTestSuite(t *testing.T) { - suite.Run(t, new(StatusValidateTestSuite)) -} diff --git a/internal/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go @@ -22,12 +22,13 @@ import "time" // StatusBookmark refers to one account having a 'bookmark' of the status of another account. type StatusBookmark 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:",nullzero,notnull,default:current_timestamp"` // when was item created - 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 + 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/statusbookmark_test.go b/internal/gtsmodel/statusbookmark_test.go @@ -1,87 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyStatusBookmark() *gtsmodel.StatusBookmark { - return &gtsmodel.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 - m := happyStatusBookmark() - err := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkBadID() { - m := happyStatusBookmark() - - m.ID = "" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'StatusBookmark.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'StatusBookmark.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") -} - -func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkDodgyStatusID() { - m := happyStatusBookmark() - - m.StatusID = "9HZJ76B6VXSKF" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'StatusBookmark.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") - - m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" - err = gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'StatusBookmark.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") -} - -func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkNoCreatedAt() { - m := happyStatusBookmark() - - m.CreatedAt = time.Time{} - err := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func TestStatusBookmarkValidateTestSuite(t *testing.T) { - suite.Run(t, new(StatusBookmarkValidateTestSuite)) -} diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go @@ -22,13 +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 string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created - 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 + 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/statusfave_test.go b/internal/gtsmodel/statusfave_test.go @@ -1,100 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyStatusFave() *gtsmodel.StatusFave { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*f) - suite.NoError(err) -} - -func (suite *StatusFaveValidateTestSuite) TestValidateStatusFaveBadID() { - f := happyStatusFave() - - f.ID = "" - err := gtsmodel.ValidateStruct(*f) - suite.EqualError(err, "Key: 'StatusFave.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - f.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*f) - suite.EqualError(err, "Key: 'StatusFave.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") - - f.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*f) - suite.NoError(err) -} - -func (suite *StatusFaveValidateTestSuite) TestValidateStatusFaveNoURI() { - f := happyStatusFave() - - f.URI = "" - err := gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go @@ -22,12 +22,13 @@ import "time" // StatusMute refers to one account having muted the status of another account or its own. type StatusMute 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:",nullzero,notnull,default:current_timestamp"` // when was item created - 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 + 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/statusmute_test.go b/internal/gtsmodel/statusmute_test.go @@ -1,87 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyStatusMute() *gtsmodel.StatusMute { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func (suite *StatusMuteValidateTestSuite) TestValidateStatusMuteBadID() { - m := happyStatusMute() - - m.ID = "" - err := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'StatusMute.ID' Error:Field validation for 'ID' failed on the 'required' tag") - - m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*m) - suite.EqualError(err, "Key: 'StatusMute.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") - - m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" - err = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*m) - suite.NoError(err) -} - -func TestStatusMuteValidateTestSuite(t *testing.T) { - suite.Run(t, new(StatusMuteValidateTestSuite)) -} diff --git a/internal/gtsmodel/stream.go b/internal/gtsmodel/stream.go @@ -1,38 +0,0 @@ -package gtsmodel - -import "sync" - -// StreamsForAccount is a wrapper for the multiple streams that one account can have running at the same time. -// TODO: put a limit on this -type StreamsForAccount struct { - // The currently held streams for this account - Streams []*Stream - // Mutex to lock/unlock when modifying the slice of streams. - sync.Mutex -} - -// Stream represents one open stream for a client. -type Stream struct { - // ID of this stream, generated during creation. - ID string - // Type of this stream: user/public/etc - Type string - // Channel of messages for the client to read from - Messages chan *Message - // Channel to close when the client drops away - Hangup chan interface{} - // Only put messages in the stream when Connected - Connected bool - // Mutex to lock/unlock when inserting messages, hanging up, changing the connected state etc. - sync.Mutex -} - -// Message represents one streamed message. -type Message struct { - // All the stream types this message should be delivered to. - Stream []string `json:"stream"` - // The event type of the message (update/delete/notification etc) - Event string `json:"event"` - // The actual payload of the message. In case of an update or notification, this will be a JSON string. - Payload string `json:"payload"` -} diff --git a/internal/gtsmodel/tag.go b/internal/gtsmodel/tag.go @@ -22,13 +22,13 @@ import "time" // Tag represents a hashtag for gathering public statuses together. type Tag 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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:",nullzero,notnull,default:current_timestamp"` // when was this tag last used? + 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/tag_test.go b/internal/gtsmodel/tag_test.go @@ -1,92 +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_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyTag() *gtsmodel.Tag { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*t) - suite.NoError(err) -} - -func (suite *TagValidateTestSuite) TestValidateTagNoName() { - t := happyTag() - t.Name = "" - - err := gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*t) - suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'required' tag") - - t.URL = "no-schema.com" - err = gtsmodel.ValidateStruct(*t) - suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'url' tag") - - t.URL = "justastring" - err = gtsmodel.ValidateStruct(*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 = gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*t) - suite.NoError(err) -} - -func TestTagValidateTestSuite(t *testing.T) { - suite.Run(t, new(TagValidateTestSuite)) -} diff --git a/internal/gtsmodel/token.go b/internal/gtsmodel/token.go @@ -21,30 +21,23 @@ package gtsmodel import "time" // 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 `validate:"ulid" bun:"type:CHAR(26),pk,nullzero,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"` + 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,url" bun:",nullzero,notnull"` // Oauth redirect URI for this token + Scope string `validate:"omitempty,url" bun:",nullzero,notnull,default:'read'"` // 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 @@ -26,45 +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 { - 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:",nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `validate:"-" bun:",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:",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:",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:"-" 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:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // 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:",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:",nullzero"` // When did we send email confirmation to this user? - ConfirmedAt time.Time `validate:"required_with=Email" bun:",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:",nullzero"` // When did we email the user their reset-password email? - - EncryptedOTPSecret string `validate:"-" bun:",nullzero"` - EncryptedOTPSecretIv string `validate:"-" bun:",nullzero"` - EncryptedOTPSecretSalt string `validate:"-" bun:",nullzero"` - OTPRequiredForLogin bool `validate:"-" bun:",notnull,default:false"` - OTPBackupCodes []string `validate:"-" bun:",nullzero"` - ConsumedTimestamp int `validate:"-" bun:",nullzero"` - RememberToken string `validate:"-" bun:",nullzero"` - SignInToken string `validate:"-" bun:",nullzero"` - SignInTokenSentAt time.Time `validate:"-" bun:",nullzero"` - WebauthnID string `validate:"-" 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:"-" 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/gtsmodel/user_test.go b/internal/gtsmodel/user_test.go @@ -1,124 +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_test - -import ( - "net" - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func happyUser() *gtsmodel.User { - return &gtsmodel.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 := gtsmodel.ValidateStruct(*u) - suite.NoError(err) -} - -func (suite *UserValidateTestSuite) TestValidateUserNoID() { - // user has no id set - u := happyUser() - u.ID = "" - - err := gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*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 := gtsmodel.ValidateStruct(*u) - suite.NoError(err) -} - -func (suite *UserValidateTestSuite) TestValidateUserNoConfirmedAt() { - // user has Email but no ConfirmedAt - u := happyUser() - u.ConfirmedAt = time.Time{} - - err := gtsmodel.ValidateStruct(*u) - suite.EqualError(err, "Key: 'User.ConfirmedAt' Error:Field validation for 'ConfirmedAt' failed on the 'required_with' tag") -} - -func TestUserValidateTestSuite(t *testing.T) { - suite.Run(t, new(UserValidateTestSuite)) -} diff --git a/internal/gtsmodel/validate.go b/internal/gtsmodel/validate.go @@ -1,75 +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 - -import ( - "reflect" - - "github.com/go-playground/validator/v10" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -var v *validator.Validate - -// Validation Panic messages -const ( - PointerValidationPanic = "validate function was passed pointer" - InvalidValidationPanic = "validate function was passed invalid item" -) - -func ulidValidator(fl validator.FieldLevel) bool { - field := fl.Field() - - switch field.Kind() { - case reflect.String: - return util.ValidateULID(field.String()) - default: - return false - } -} - -func init() { - v = validator.New() - v.RegisterValidation("ulid", ulidValidator) -} - -// ValidateStruct validates the passed struct, returning validator.ValidationErrors if invalid, or nil if OK. -func ValidateStruct(s interface{}) error { - switch reflect.ValueOf(s).Kind() { - case reflect.Invalid: - panic(InvalidValidationPanic) - case reflect.Ptr: - panic(PointerValidationPanic) - } - - err := v.Struct(s) - return processValidationError(err) -} - -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/gtsmodel/validate_test.go b/internal/gtsmodel/validate_test.go @@ -1,64 +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_test - -import ( - "testing" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -type ValidateTestSuite struct { - suite.Suite -} - -func (suite *ValidateTestSuite) TestValidatePointer() { - var nilUser *gtsmodel.User - suite.PanicsWithValue(gtsmodel.PointerValidationPanic, func() { - gtsmodel.ValidateStruct(nilUser) - }) -} - -func (suite *ValidateTestSuite) TestValidateNil() { - suite.PanicsWithValue(gtsmodel.InvalidValidationPanic, func() { - gtsmodel.ValidateStruct(nil) - }) -} - -func (suite *ValidateTestSuite) TestValidateWeirdULID() { - type a struct { - ID bool `validate:"required,ulid"` - } - - err := gtsmodel.ValidateStruct(a{ID: true}) - suite.Error(err) -} - -func (suite *ValidateTestSuite) TestValidateNotStruct() { - type aaaaaaa string - aaaaaa := aaaaaaa("aaaa") - suite.Panics(func() { - gtsmodel.ValidateStruct(aaaaaa) - }) -} - -func TestValidateTestSuite(t *testing.T) { - suite.Run(t, new(ValidateTestSuite)) -} diff --git 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/processing/account/update.go b/internal/processing/account/update.go @@ -32,7 +32,7 @@ import ( "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) { @@ -51,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 @@ -61,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 @@ -94,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, &gtsmodel.Account{}); err != nil { @@ -109,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, &gtsmodel.Account{}); err != nil { diff --git 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 diff --git 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 @@ -39,6 +39,7 @@ import ( 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" @@ -166,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 diff --git 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 @@ -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 := &gtsmodel.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 := &gtsmodel.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 @@ -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 <- &gtsmodel.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 @@ -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 @@ -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 <- &gtsmodel.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 @@ -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 <- &gtsmodel.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 @@ -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/stream/stream.go b/internal/stream/stream.go @@ -0,0 +1,38 @@ +package stream + +import "sync" + +// StreamsForAccount is a wrapper for the multiple streams that one account can have running at the same time. +// TODO: put a limit on this +type StreamsForAccount struct { + // The currently held streams for this account + Streams []*Stream + // Mutex to lock/unlock when modifying the slice of streams. + sync.Mutex +} + +// Stream represents one open stream for a client. +type Stream struct { + // ID of this stream, generated during creation. + ID string + // Type of this stream: user/public/etc + Type string + // Channel of messages for the client to read from + Messages chan *Message + // Channel to close when the client drops away + Hangup chan interface{} + // Only put messages in the stream when Connected + Connected bool + // Mutex to lock/unlock when inserting messages, hanging up, changing the connected state etc. + sync.Mutex +} + +// Message represents one streamed message. +type Message struct { + // All the stream types this message should be delivered to. + Stream []string `json:"stream"` + // The event type of the message (update/delete/notification etc) + Event string `json:"event"` + // The actual payload of the message. In case of an update or notification, this will be a JSON string. + Payload string `json:"payload"` +} diff --git 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/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/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 @@ -1,114 +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}` - ulidRegex = regexp.MustCompile(fmt.Sprintf(`^%s$`, ulidRegexString)) - - 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 @@ -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 @@ -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/util/validation.go b/internal/util/validation.go @@ -1,178 +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 ( - "errors" - "fmt" - "net/mail" - - pwv "github.com/wagslane/go-password-validator" - "golang.org/x/text/language" -) - -const ( - maximumPasswordLength = 64 - minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator - minimumReasonLength = 40 - maximumReasonLength = 500 - maximumSiteTitleLength = 40 - maximumShortDescriptionLength = 500 - maximumDescriptionLength = 5000 - maximumSiteTermsLength = 5000 -) - -// ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. -func ValidateNewPassword(password string) error { - if password == "" { - return errors.New("no password provided") - } - - if len(password) > maximumPasswordLength { - return fmt.Errorf("password should be no more than %d chars", maximumPasswordLength) - } - - return pwv.Validate(password, minimumPasswordEntropy) -} - -// ValidateUsername makes sure that a given username is valid (ie., letters, numbers, underscores, check length). -// Returns an error if not. -func ValidateUsername(username string) error { - if username == "" { - return errors.New("no username provided") - } - - if !usernameValidationRegex.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. -// Returns an error if not. -func ValidateEmail(email string) error { - if email == "" { - return errors.New("no email provided") - } - - _, err := mail.ParseAddress(email) - return err -} - -// ValidateLanguage 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 { - if lang == "" { - return errors.New("no language provided") - } - _, err := language.ParseBase(lang) - return err -} - -// ValidateSignUpReason checks that a sufficient reason is given for a server signup request -func ValidateSignUpReason(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 - return nil - } - - if reason == "" { - return errors.New("no reason provided") - } - - if len(reason) < minimumReasonLength { - return fmt.Errorf("reason should be at least %d chars but '%s' was %d", minimumReasonLength, reason, len(reason)) - } - - if len(reason) > maximumReasonLength { - return fmt.Errorf("reason should be no more than %d chars but given reason was %d", maximumReasonLength, len(reason)) - } - return nil -} - -// ValidateDisplayName checks that a requested display name is valid -func ValidateDisplayName(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 { - // 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 { - // TODO: add some validation logic here -- length, characters, etc - return nil -} - -// ValidateEmojiShortcode 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) { - 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 { - if len(siteTitle) > maximumSiteTitleLength { - return fmt.Errorf("site title should be no more than %d chars but given title was %d", maximumSiteTitleLength, len(siteTitle)) - } - - return nil -} - -// ValidateSiteShortDescription ensures that the given site short description is within spec. -func ValidateSiteShortDescription(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)) - } - - return nil -} - -// ValidateSiteDescription ensures that the given site description is within spec. -func ValidateSiteDescription(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)) - } - - return nil -} - -// ValidateSiteTerms ensures that the given site terms string is within spec. -func ValidateSiteTerms(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 -} - -// ValidateULID returns true if the passed string is a valid ULID. -func ValidateULID(i string) bool { - return ulidRegex.MatchString(i) -} diff --git a/internal/util/validation_test.go b/internal/util/validation_test.go @@ -1,283 +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_test - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -type ValidationTestSuite struct { - suite.Suite -} - -func (suite *ValidationTestSuite) TestCheckPasswordStrength() { - empty := "" - terriblePassword := "password" - weakPassword := "OKPassword" - shortPassword := "Ok12" - specialPassword := "Ok12%" - longPassword := "thisisafuckinglongpasswordbutnospecialchars" - tooLong := "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque a enim nibh. Vestibulum bibendum leo ac porttitor auctor." - strongPassword := "3dX5@Zc%mV*W2MBNEy$@" - var err error - - err = util.ValidateNewPassword(empty) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("no password provided"), err) - } - - err = util.ValidateNewPassword(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) - 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) - 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) - 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) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateNewPassword(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) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } -} - -func (suite *ValidationTestSuite) TestValidateUsername() { - empty := "" - tooLong := "holycrapthisisthelongestusernameiveeverseeninmylifethatstoomuchman" - withSpaces := "this username has spaces in it" - weirdChars := "thisusername&&&&&&&istooweird!!" - leadingSpace := " see_that_leading_space" - trailingSpace := "thisusername_ends_with_a_space " - newlines := "this_is\n_almost_ok" - goodUsername := "this_is_a_good_username" - var err error - - err = util.ValidateUsername(empty) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("no username provided"), err) - } - - err = util.ValidateUsername(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) - 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) - 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) - 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) - 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) - 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) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } -} - -func (suite *ValidationTestSuite) TestValidateEmail() { - empty := "" - notAnEmailAddress := "this-is-no-email-address!" - almostAnEmailAddress := "@thisisalmostan@email.address" - aWebsite := "https://thisisawebsite.com" - emailAddress := "thisis.actually@anemail.address" - var err error - - err = util.ValidateEmail(empty) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("no email provided"), err) - } - - err = util.ValidateEmail(notAnEmailAddress) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err) - } - - err = util.ValidateEmail(almostAnEmailAddress) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("mail: no angle-addr"), err) - } - - err = util.ValidateEmail(aWebsite) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err) - } - - err = util.ValidateEmail(emailAddress) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } -} - -func (suite *ValidationTestSuite) TestValidateLanguage() { - empty := "" - notALanguage := "this isn't a language at all!" - english := "en" - capitalEnglish := "EN" - arabic3Letters := "ara" - mixedCapsEnglish := "eN" - englishUS := "en-us" - dutch := "nl" - german := "de" - var err error - - err = util.ValidateLanguage(empty) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("no language provided"), err) - } - - err = util.ValidateLanguage(notALanguage) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err) - } - - err = util.ValidateLanguage(english) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateLanguage(capitalEnglish) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateLanguage(arabic3Letters) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateLanguage(mixedCapsEnglish) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateLanguage(englishUS) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err) - } - - err = util.ValidateLanguage(dutch) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateLanguage(german) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } -} - -func (suite *ValidationTestSuite) TestValidateReason() { - empty := "" - badReason := "because" - goodReason := "to smash the state and destroy capitalism ultimately and completely" - tooLong := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris auctor mollis viverra. Maecenas maximus mollis sem, nec fermentum velit consectetur non. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque a enim nibh. Vestibulum bibendum leo ac porttitor auctor. Curabitur velit tellus, facilisis vitae lorem a, ullamcorper efficitur leo. Sed a auctor tortor. Sed ut finibus ante, sit amet laoreet sapien. Donec ullamcorper tellus a nibh sodales vulputate. Donec id dolor eu odio mollis bibendum. Pellentesque habitant morbi tristique senectus et netus at." - var err error - - // check with no reason required - err = util.ValidateSignUpReason(empty, false) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateSignUpReason(badReason, false) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateSignUpReason(tooLong, false) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - err = util.ValidateSignUpReason(goodReason, false) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } - - // check with reason required - err = util.ValidateSignUpReason(empty, true) - if assert.Error(suite.T(), err) { - assert.Equal(suite.T(), errors.New("no reason provided"), err) - } - - err = util.ValidateSignUpReason(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) - 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) - if assert.NoError(suite.T(), err) { - assert.Equal(suite.T(), nil, err) - } -} - -func TestValidationTestSuite(t *testing.T) { - suite.Run(t, new(ValidationTestSuite)) -} diff --git a/internal/validate/block_test.go 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 &gtsmodel.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 + d := happyBlock() + err := validate.Struct(*d) + suite.NoError(err) +} + +func (suite *BlockValidateTestSuite) TestValidateBlockBadID() { + d := happyBlock() + + d.ID = "" + err := validate.Struct(*d) + suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'required' tag") + + d.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" + err = validate.Struct(*d) + suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *BlockValidateTestSuite) TestValidateBlockNoCreatedAt() { + d := happyBlock() + + d.CreatedAt = time.Time{} + err := validate.Struct(*d) + suite.NoError(err) +} + +func (suite *BlockValidateTestSuite) TestValidateBlockCreatedByAccountID() { + d := happyBlock() + + d.AccountID = "" + err := validate.Struct(*d) + suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag") + + d.AccountID = "this-is-not-a-valid-ulid" + err = validate.Struct(*d) + suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'ulid' tag") +} + +func (suite *BlockValidateTestSuite) TestValidateBlockTargetAccountID() { + d := happyBlock() + + d.TargetAccountID = "invalid-ulid" + err := validate.Struct(*d) + suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'ulid' tag") + + d.TargetAccountID = "01FEEDHX4G7EGHF5GD9E82Y51Q" + err = validate.Struct(*d) + suite.NoError(err) + + d.TargetAccountID = "" + err = validate.Struct(*d) + suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'required' tag") +} + +func (suite *BlockValidateTestSuite) TestValidateBlockURI() { + d := happyBlock() + + d.URI = "invalid-uri" + err := validate.Struct(*d) + suite.EqualError(err, "Key: 'Block.URI' Error:Field validation for 'URI' failed on the 'url' tag") + + d.URI = "" + err = validate.Struct(*d) + 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/domainblock_test.go 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 &gtsmodel.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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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/validate/formvalidation.go b/internal/validate/formvalidation.go @@ -0,0 +1,182 @@ +/* + 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 ( + "errors" + "fmt" + "net/mail" + + "github.com/superseriousbusiness/gotosocial/internal/regexes" + pwv "github.com/wagslane/go-password-validator" + "golang.org/x/text/language" +) + +const ( + maximumPasswordLength = 64 + minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator + minimumReasonLength = 40 + maximumReasonLength = 500 + maximumSiteTitleLength = 40 + maximumShortDescriptionLength = 500 + maximumDescriptionLength = 5000 + maximumSiteTermsLength = 5000 + maximumUsernameLength = 64 + maximumEmojiShortcodeLength = 30 + maximumHashtagLength = 30 +) + +// 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") + } + + if len(password) > maximumPasswordLength { + return fmt.Errorf("password should be no more than %d chars", maximumPasswordLength) + } + + return pwv.Validate(password, minimumPasswordEntropy) +} + +// Username makes sure that a given username is valid (ie., letters, numbers, underscores, check length). +// Returns an error if not. +func Username(username string) error { + if username == "" { + return errors.New("no username provided") + } + + 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 +} + +// Email makes sure that a given email address is a valid address. +// Returns an error if not. +func Email(email string) error { + if email == "" { + return errors.New("no email provided") + } + + _, err := mail.ParseAddress(email) + return err +} + +// 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 Language(lang string) error { + if lang == "" { + return errors.New("no language provided") + } + _, err := language.ParseBase(lang) + return err +} + +// 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 + return nil + } + + if reason == "" { + return errors.New("no reason provided") + } + + if len(reason) < minimumReasonLength { + return fmt.Errorf("reason should be at least %d chars but '%s' was %d", minimumReasonLength, reason, len(reason)) + } + + if len(reason) > maximumReasonLength { + return fmt.Errorf("reason should be no more than %d chars but given reason was %d", maximumReasonLength, len(reason)) + } + return nil +} + +// 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 +} + +// 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 +} + +// 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 +} + +// 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 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 +} + +// 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)) + } + + return nil +} + +// 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)) + } + + return nil +} + +// 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)) + } + + return nil +} + +// 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/validate/formvalidation_test.go b/internal/validate/formvalidation_test.go @@ -0,0 +1,283 @@ +/* + 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 ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/validate" +) + +type ValidationTestSuite struct { + suite.Suite +} + +func (suite *ValidationTestSuite) TestCheckPasswordStrength() { + empty := "" + terriblePassword := "password" + weakPassword := "OKPassword" + shortPassword := "Ok12" + specialPassword := "Ok12%" + longPassword := "thisisafuckinglongpasswordbutnospecialchars" + tooLong := "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque a enim nibh. Vestibulum bibendum leo ac porttitor auctor." + strongPassword := "3dX5@Zc%mV*W2MBNEy$@" + var err error + + err = validate.NewPassword(empty) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("no password provided"), err) + } + + 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 = 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 = 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 = 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 = validate.NewPassword(longPassword) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + 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 = validate.NewPassword(strongPassword) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } +} + +func (suite *ValidationTestSuite) TestValidateUsername() { + empty := "" + tooLong := "holycrapthisisthelongestusernameiveeverseeninmylifethatstoomuchman" + withSpaces := "this username has spaces in it" + weirdChars := "thisusername&&&&&&&istooweird!!" + leadingSpace := " see_that_leading_space" + trailingSpace := "thisusername_ends_with_a_space " + newlines := "this_is\n_almost_ok" + goodUsername := "this_is_a_good_username" + var err error + + err = validate.Username(empty) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("no username provided"), err) + } + + 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 = 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 = 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 = 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 = 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 = 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 = validate.Username(goodUsername) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } +} + +func (suite *ValidationTestSuite) TestValidateEmail() { + empty := "" + notAnEmailAddress := "this-is-no-email-address!" + almostAnEmailAddress := "@thisisalmostan@email.address" + aWebsite := "https://thisisawebsite.com" + emailAddress := "thisis.actually@anemail.address" + var err error + + err = validate.Email(empty) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("no email provided"), err) + } + + err = validate.Email(notAnEmailAddress) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err) + } + + err = validate.Email(almostAnEmailAddress) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("mail: no angle-addr"), err) + } + + err = validate.Email(aWebsite) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err) + } + + err = validate.Email(emailAddress) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } +} + +func (suite *ValidationTestSuite) TestValidateLanguage() { + empty := "" + notALanguage := "this isn't a language at all!" + english := "en" + capitalEnglish := "EN" + arabic3Letters := "ara" + mixedCapsEnglish := "eN" + englishUS := "en-us" + dutch := "nl" + german := "de" + var err error + + err = validate.Language(empty) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("no language provided"), err) + } + + err = validate.Language(notALanguage) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err) + } + + err = validate.Language(english) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.Language(capitalEnglish) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.Language(arabic3Letters) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.Language(mixedCapsEnglish) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.Language(englishUS) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err) + } + + err = validate.Language(dutch) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.Language(german) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } +} + +func (suite *ValidationTestSuite) TestValidateReason() { + empty := "" + badReason := "because" + goodReason := "to smash the state and destroy capitalism ultimately and completely" + tooLong := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris auctor mollis viverra. Maecenas maximus mollis sem, nec fermentum velit consectetur non. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque a enim nibh. Vestibulum bibendum leo ac porttitor auctor. Curabitur velit tellus, facilisis vitae lorem a, ullamcorper efficitur leo. Sed a auctor tortor. Sed ut finibus ante, sit amet laoreet sapien. Donec ullamcorper tellus a nibh sodales vulputate. Donec id dolor eu odio mollis bibendum. Pellentesque habitant morbi tristique senectus et netus at." + var err error + + // check with no reason required + err = validate.SignUpReason(empty, false) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.SignUpReason(badReason, false) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.SignUpReason(tooLong, false) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + err = validate.SignUpReason(goodReason, false) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } + + // check with reason required + err = validate.SignUpReason(empty, true) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("no reason provided"), err) + } + + 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 = 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 = validate.SignUpReason(goodReason, true) + if assert.NoError(suite.T(), err) { + assert.Equal(suite.T(), nil, err) + } +} + +func TestValidationTestSuite(t *testing.T) { + suite.Run(t, new(ValidationTestSuite)) +} diff --git a/internal/validate/instance_test.go 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 &gtsmodel.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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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 + m := happyNotification() + err := validate.Struct(*m) + suite.NoError(err) +} + +func (suite *NotificationValidateTestSuite) TestValidateNotificationBadID() { + m := happyNotification() + + m.ID = "" + err := validate.Struct(*m) + suite.EqualError(err, "Key: 'Notification.ID' Error:Field validation for 'ID' failed on the 'required' tag") + + m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" + err = validate.Struct(*m) + suite.EqualError(err, "Key: 'Notification.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *NotificationValidateTestSuite) TestValidateNotificationStatusID() { + m := happyNotification() + + m.StatusID = "" + err := validate.Struct(*m) + suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'required_if' tag") + + m.StatusID = "9HZJ76B6VXSKF" + err = validate.Struct(*m) + suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + + m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" + err = validate.Struct(*m) + suite.EqualError(err, "Key: 'Notification.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + + m.StatusID = "" + m.NotificationType = gtsmodel.NotificationFollowRequest + err = validate.Struct(*m) + suite.NoError(err) +} + +func (suite *NotificationValidateTestSuite) TestValidateNotificationNoCreatedAt() { + m := happyNotification() + + m.CreatedAt = time.Time{} + err := validate.Struct(*m) + 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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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 + m := happyStatusBookmark() + err := validate.Struct(*m) + suite.NoError(err) +} + +func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkBadID() { + m := happyStatusBookmark() + + m.ID = "" + err := validate.Struct(*m) + suite.EqualError(err, "Key: 'StatusBookmark.ID' Error:Field validation for 'ID' failed on the 'required' tag") + + m.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB" + err = validate.Struct(*m) + suite.EqualError(err, "Key: 'StatusBookmark.ID' Error:Field validation for 'ID' failed on the 'ulid' tag") +} + +func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkDodgyStatusID() { + m := happyStatusBookmark() + + m.StatusID = "9HZJ76B6VXSKF" + err := validate.Struct(*m) + suite.EqualError(err, "Key: 'StatusBookmark.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") + + m.StatusID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!!!!!!!!" + err = validate.Struct(*m) + suite.EqualError(err, "Key: 'StatusBookmark.StatusID' Error:Field validation for 'StatusID' failed on the 'ulid' tag") +} + +func (suite *StatusBookmarkValidateTestSuite) TestValidateStatusBookmarkNoCreatedAt() { + m := happyStatusBookmark() + + m.CreatedAt = time.Time{} + err := validate.Struct(*m) + 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 @@ -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 &gtsmodel.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 @@ -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 &gtsmodel.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 @@ -0,0 +1,75 @@ +/* + 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 + +// Validation Panic messages +const ( + PointerPanic = "validate function was passed pointer" + InvalidPanic = "validate function was passed invalid item" +) + +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() + v.RegisterValidation("ulid", ulidValidator) +} + +// Struct validates the passed struct, returning validator.ValidationErrors if invalid, or nil if OK. +func Struct(s interface{}) error { + switch reflect.ValueOf(s).Kind() { + case reflect.Invalid: + panic(InvalidPanic) + case reflect.Ptr: + panic(PointerPanic) + } + + err := v.Struct(s) + return processValidationError(err) +} + +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 @@ -0,0 +1,65 @@ +/* + 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) TestValidatePointer() { + var nilUser *gtsmodel.User + suite.PanicsWithValue(validate.PointerPanic, func() { + validate.Struct(nilUser) + }) +} + +func (suite *ValidateTestSuite) TestValidateNil() { + suite.PanicsWithValue(validate.InvalidPanic, 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 @@ -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 &gtsmodel.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/user_test.go b/internal/validate/user_test.go @@ -0,0 +1,125 @@ +/* + 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 &gtsmodel.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 TestUserValidateTestSuite(t *testing.T) { + suite.Run(t, new(UserValidateTestSuite)) +} diff --git a/testrig/testmodels.go b/testrig/testmodels.go @@ -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