diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go
index 56a792ab4..90eb41bdb 100644
--- a/internal/gtsmodel/statusmute.go
+++ b/internal/gtsmodel/statusmute.go
@@ -20,19 +20,14 @@
import "time"
-// StatusMute refers to one account having muted the status of another account or its own
+// StatusMute refers to one account having muted the status of another account or its own.
type StatusMute struct {
- // id of this mute in the database
- ID string `bun:"type:CHAR(26),pk,notnull,unique"`
- // when was this mute created
- CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
- // id of the account that created ('did') the mute
- AccountID string `bun:"type:CHAR(26),notnull"`
- Account *Account `bun:"rel:belongs-to"`
- // id the account owning the muted status (can be the same as accountID)
- TargetAccountID string `bun:"type:CHAR(26),notnull"`
- TargetAccount *Account `bun:"rel:belongs-to"`
- // database id of the status that has been muted
- StatusID string `bun:"type:CHAR(26),notnull"`
- Status *Status `bun:"rel:belongs-to"`
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"required" 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/gtsmodel/tag.go b/internal/gtsmodel/tag.go
index d4be0b66c..359f9a91c 100644
--- a/internal/gtsmodel/tag.go
+++ b/internal/gtsmodel/tag.go
@@ -20,24 +20,15 @@
import "time"
-// Tag represents a hashtag for gathering public statuses together
+// Tag represents a hashtag for gathering public statuses together.
type Tag struct {
- // id of this tag in the database
- ID string `bun:",unique,type:CHAR(26),pk,notnull"`
- // Href of this tag, eg https://example.org/tags/somehashtag
- URL string `bun:",nullzero"`
- // name of this tag -- the tag without the hash part
- Name string `bun:",unique,notnull"`
- // Which account ID is the first one we saw using this tag?
- FirstSeenFromAccountID string `bun:"type:CHAR(26),nullzero"`
- // when was this tag created
- CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
- // when was this tag last updated
- UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
- // can our instance users use this tag?
- Useable bool `bun:",notnull,default:true"`
- // can our instance users look up this tag?
- Listable bool `bun:",notnull,default:true"`
- // when was this tag last used?
- LastStatusAt time.Time `bun:",nullzero"`
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated
+ URL string `validate:"required,url" bun:",nullzero,notnull"` // Href 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:"ulid" bun:"type:CHAR(26),nullzero"` // Which account ID is the first one we saw using this tag?
+ Useable bool `validate:"-" bun:",nullzero,notnull,default:true"` // can our instance users use this tag?
+ Listable bool `validate:"-" bun:",nullzero,notnull,default:true"` // can our instance users look up this tag?
+ LastStatusAt time.Time `validate:"required" bun:",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
new file mode 100644
index 000000000..baafe55bd
--- /dev/null
+++ b/internal/gtsmodel/tag_test.go
@@ -0,0 +1,92 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package gtsmodel_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func happyTag() *gtsmodel.Tag {
+ return >smodel.Tag{
+ ID: "01FE91RJR88PSEEE30EV35QR8N",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ URL: "https://example.org/tags/some_tag",
+ Name: "some_tag",
+ FirstSeenFromAccountID: "01FE91SR5P2GW06K3AJ98P72MT",
+ Useable: true,
+ Listable: true,
+ LastStatusAt: time.Now(),
+ }
+}
+
+type TagValidateTestSuite struct {
+ suite.Suite
+}
+
+func (suite *TagValidateTestSuite) TestValidateTagHappyPath() {
+ // no problem here
+ t := happyTag()
+ err := 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/user.go b/internal/gtsmodel/user.go
index c36d75c8c..27089763d 100644
--- a/internal/gtsmodel/user.go
+++ b/internal/gtsmodel/user.go
@@ -26,97 +26,45 @@
// User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account.
// To cross reference this local user with their account (which can be local or remote), use the AccountID field.
type User struct {
- /*
- BASIC INFO
- */
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated
+ Email string `validate:"required_with=ConfirmedAt" bun:",nullzero,notnull,unique"` // confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported
+ 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:"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:"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:",nullzero,notnull,default:false"` // Is this user a moderator?
+ Admin bool `validate:"-" bun:",nullzero,notnull,default:false"` // Is this user an admin?
+ Disabled bool `validate:"-" bun:",nullzero,notnull,default:false"` // Is this user disabled from posting?
+ Approved bool `validate:"-" bun:",nullzero,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?
- // id of this user in the local database; the end-user will never need to know this, it's strictly internal
- ID string `bun:"type:CHAR(26),pk,notnull,unique"`
- // confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported
- Email string `bun:"default:null,unique,nullzero"`
- // The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet)
- AccountID string `bun:"type:CHAR(26),unique,nullzero"`
- Account *Account `bun:"rel:belongs-to"`
- // The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables
- EncryptedPassword string `bun:",notnull"`
-
- /*
- USER METADATA
- */
-
- // When was this user created?
- CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
- // From what IP was this user created?
- SignUpIP net.IP `bun:",nullzero"`
- // When was this user updated (eg., password changed, email address changed)?
- UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
- // When did this user sign in for their current session?
- CurrentSignInAt time.Time `bun:",nullzero"`
- // What's the most recent IP of this user
- CurrentSignInIP net.IP `bun:",nullzero"`
- // When did this user last sign in?
- LastSignInAt time.Time `bun:",nullzero"`
- // What's the previous IP of this user?
- LastSignInIP net.IP `bun:",nullzero"`
- // How many times has this user signed in?
- SignInCount int
- // id of the user who invited this user (who let this guy in?)
- InviteID string `bun:"type:CHAR(26),nullzero"`
- // What languages does this user want to see?
- ChosenLanguages []string
- // What languages does this user not want to see?
- FilteredLanguages []string
- // In what timezone/locale is this user located?
- Locale string `bun:",nullzero"`
- // Which application id created this user? See gtsmodel.Application
- CreatedByApplicationID string `bun:"type:CHAR(26),nullzero"`
- CreatedByApplication *Application `bun:"rel:belongs-to"`
- // When did we last contact this user
- LastEmailedAt time.Time `bun:",nullzero"`
-
- /*
- USER CONFIRMATION
- */
-
- // What confirmation token did we send this user/what are we expecting back?
- ConfirmationToken string `bun:",nullzero"`
- // When did the user confirm their email address
- ConfirmedAt time.Time `bun:",nullzero"`
- // When did we send email confirmation to this user?
- ConfirmationSentAt time.Time `bun:",nullzero"`
- // Email address that hasn't yet been confirmed
- UnconfirmedEmail string `bun:",nullzero"`
-
- /*
- ACL FLAGS
- */
-
- // Is this user a moderator?
- Moderator bool
- // Is this user an admin?
- Admin bool
- // Is this user disabled from posting?
- Disabled bool
- // Has this user been approved by a moderator?
- Approved bool
-
- /*
- USER SECURITY
- */
-
- // The generated token that the user can use to reset their password
- ResetPasswordToken string `bun:",nullzero"`
- // When did we email the user their reset-password email?
- ResetPasswordSentAt time.Time `bun:",nullzero"`
-
- EncryptedOTPSecret string `bun:",nullzero"`
- EncryptedOTPSecretIv string `bun:",nullzero"`
- EncryptedOTPSecretSalt string `bun:",nullzero"`
- OTPRequiredForLogin bool
- OTPBackupCodes []string
- ConsumedTimestamp int
- RememberToken string `bun:",nullzero"`
- SignInToken string `bun:",nullzero"`
- SignInTokenSentAt time.Time `bun:",nullzero"`
- WebauthnID string `bun:",nullzero"`
+ EncryptedOTPSecret string `validate:"-" bun:",nullzero"`
+ EncryptedOTPSecretIv string `validate:"-" bun:",nullzero"`
+ EncryptedOTPSecretSalt string `validate:"-" bun:",nullzero"`
+ OTPRequiredForLogin bool `validate:"-" bun:",nullzero"`
+ 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/gtsmodel/user_test.go b/internal/gtsmodel/user_test.go
new file mode 100644
index 000000000..c1a9bf849
--- /dev/null
+++ b/internal/gtsmodel/user_test.go
@@ -0,0 +1,106 @@
+package gtsmodel_test
+
+import (
+ "net"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func happyUser() *gtsmodel.User {
+ return >smodel.User{
+ ID: "01FE8TTK9F34BR0KG7639AJQTX",
+ Email: "whatever@example.org",
+ AccountID: "01FE8TWA7CN8J7237K5DFS1RY5",
+ Account: nil,
+ EncryptedPassword: "$2y$10$tkRapNGW.RWkEuCMWdgArunABFvsPGRvFQY3OibfSJo0RDL3z8WfC",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ SignUpIP: net.ParseIP("128.64.32.16"),
+ CurrentSignInAt: time.Now(),
+ CurrentSignInIP: net.ParseIP("128.64.32.16"),
+ LastSignInAt: time.Now(),
+ LastSignInIP: net.ParseIP("128.64.32.16"),
+ SignInCount: 0,
+ InviteID: "",
+ ChosenLanguages: []string{},
+ FilteredLanguages: []string{},
+ Locale: "en",
+ CreatedByApplicationID: "01FE8Y5EHMWCA1MHMTNHRVZ1X4",
+ CreatedByApplication: nil,
+ LastEmailedAt: time.Now(),
+ ConfirmationToken: "",
+ ConfirmedAt: time.Now(),
+ ConfirmationSentAt: time.Now(),
+ 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
new file mode 100644
index 000000000..720aad463
--- /dev/null
+++ b/internal/gtsmodel/validate.go
@@ -0,0 +1,78 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package gtsmodel
+
+import (
+ "reflect"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+var v *validator.Validate
+
+const (
+ PointerValidationPanic = "validate function was passed pointer"
+ InvalidValidationPanic = "validate function was passed invalid item"
+)
+
+var ulidValidator = func(fl validator.FieldLevel) bool {
+ value, kind, _ := fl.ExtractType(fl.Field())
+
+ if kind != reflect.String {
+ return false
+ }
+
+ // we want either an empty string, or a proper ULID, nothing else
+ // if the string is empty, the `required` tag will take care of it so we don't need to worry about it here
+ s := value.String()
+ if len(s) == 0 {
+ return true
+ }
+ return util.ValidateULID(s)
+}
+
+func init() {
+ v = validator.New()
+ v.RegisterValidation("ulid", ulidValidator)
+}
+
+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
new file mode 100644
index 000000000..7200522bc
--- /dev/null
+++ b/internal/gtsmodel/validate_test.go
@@ -0,0 +1,64 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package gtsmodel_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/id/ulid.go b/internal/id/ulid.go
index b488ddfc4..f9fbd4d88 100644
--- a/internal/id/ulid.go
+++ b/internal/id/ulid.go
@@ -10,6 +10,8 @@
const randomRange = 631152381 // ~20 years in seconds
+type ULID string
+
// NewULID returns a new ULID string using the current time, or an error if something goes wrong.
func NewULID() (string, error) {
newUlid, err := ulid.New(ulid.Timestamp(time.Now()), rand.Reader)
diff --git a/internal/util/regexes.go b/internal/util/regexes.go
index 88212fc43..d3a42d1fc 100644
--- a/internal/util/regexes.go
+++ b/internal/util/regexes.go
@@ -90,6 +90,7 @@
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
diff --git a/internal/util/validation.go b/internal/util/validation.go
index 446f7a70e..aa25ccd16 100644
--- a/internal/util/validation.go
+++ b/internal/util/validation.go
@@ -171,3 +171,8 @@ func ValidateSiteTerms(t string) error {
return nil
}
+
+// ValidateULID returns true if the passed string is a valid ULID.
+func ValidateULID(i string) bool {
+ return ulidRegex.MatchString(i)
+}