Start messing around with federation

This commit is contained in:
tsmethurst 2021-04-24 11:19:18 +02:00
commit 1c3ad47f52
10 changed files with 245 additions and 83 deletions

View file

@ -34,7 +34,7 @@ import (
const ( const (
// BasePath is the base API path for this module // BasePath is the base API path for this module
BasePath = "/api/v1/admin" BasePath = "/api/v1/admin"
// EmojiPath is used for posting/deleting custom emojis // EmojiPath is used for posting/deleting custom emojis
EmojiPath = BasePath + "/custom_emojis" EmojiPath = BasePath + "/custom_emojis"
) )

View file

@ -32,9 +32,9 @@ import (
const ( const (
// AuthSignInPath is the API path for users to sign in through // AuthSignInPath is the API path for users to sign in through
AuthSignInPath = "/auth/sign_in" AuthSignInPath = "/auth/sign_in"
// OauthTokenPath is the API path to use for granting token requests to users with valid credentials // OauthTokenPath is the API path to use for granting token requests to users with valid credentials
OauthTokenPath = "/oauth/token" OauthTokenPath = "/oauth/token"
// OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user) // OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user)
OauthAuthorizePath = "/oauth/authorize" OauthAuthorizePath = "/oauth/authorize"
) )

View file

@ -39,7 +39,7 @@ const (
// MediaSizeKey is the url key for the desired media size--original/small/static // MediaSizeKey is the url key for the desired media size--original/small/static
MediaSizeKey = "media_size" MediaSizeKey = "media_size"
// FileNameKey is the actual filename being sought. Will usually be a UUID then something like .jpeg // FileNameKey is the actual filename being sought. Will usually be a UUID then something like .jpeg
FileNameKey = "file_name" FileNameKey = "file_name"
) )
// FileServer implements the RESTAPIModule interface. // FileServer implements the RESTAPIModule interface.

View file

@ -37,9 +37,9 @@ import (
const ( const (
// IDKey is for status UUIDs // IDKey is for status UUIDs
IDKey = "id" IDKey = "id"
// BasePath is the base path for serving the status API // BasePath is the base path for serving the status API
BasePath = "/api/v1/statuses" BasePath = "/api/v1/statuses"
// BasePathWithID is just the base path with the ID key in it. // BasePathWithID is just the base path with the ID key in it.
// Use this anywhere you need to know the ID of the status being queried. // Use this anywhere you need to know the ID of the status being queried.
BasePathWithID = BasePath + "/:" + IDKey BasePathWithID = BasePath + "/:" + IDKey
@ -48,31 +48,31 @@ const (
ContextPath = BasePathWithID + "/context" ContextPath = BasePathWithID + "/context"
// FavouritedPath is for seeing who's faved a given status // FavouritedPath is for seeing who's faved a given status
FavouritedPath = BasePathWithID + "/favourited_by" FavouritedPath = BasePathWithID + "/favourited_by"
// FavouritePath is for posting a fave on a status // FavouritePath is for posting a fave on a status
FavouritePath = BasePathWithID + "/favourite" FavouritePath = BasePathWithID + "/favourite"
// UnfavouritePath is for removing a fave from a status // UnfavouritePath is for removing a fave from a status
UnfavouritePath = BasePathWithID + "/unfavourite" UnfavouritePath = BasePathWithID + "/unfavourite"
// RebloggedPath is for seeing who's boosted a given status // RebloggedPath is for seeing who's boosted a given status
RebloggedPath = BasePathWithID + "/reblogged_by" RebloggedPath = BasePathWithID + "/reblogged_by"
// ReblogPath is for boosting/reblogging a given status // ReblogPath is for boosting/reblogging a given status
ReblogPath = BasePathWithID + "/reblog" ReblogPath = BasePathWithID + "/reblog"
// UnreblogPath is for undoing a boost/reblog of a given status // UnreblogPath is for undoing a boost/reblog of a given status
UnreblogPath = BasePathWithID + "/unreblog" UnreblogPath = BasePathWithID + "/unreblog"
// BookmarkPath is for creating a bookmark on a given status // BookmarkPath is for creating a bookmark on a given status
BookmarkPath = BasePathWithID + "/bookmark" BookmarkPath = BasePathWithID + "/bookmark"
// UnbookmarkPath is for removing a bookmark from a given status // UnbookmarkPath is for removing a bookmark from a given status
UnbookmarkPath = BasePathWithID + "/unbookmark" UnbookmarkPath = BasePathWithID + "/unbookmark"
// MutePath is for muting a given status so that notifications will no longer be received about it. // MutePath is for muting a given status so that notifications will no longer be received about it.
MutePath = BasePathWithID + "/mute" MutePath = BasePathWithID + "/mute"
// UnmutePath is for undoing an existing mute // UnmutePath is for undoing an existing mute
UnmutePath = BasePathWithID + "/unmute" UnmutePath = BasePathWithID + "/unmute"
// PinPath is for pinning a status to an account profile so that it's the first thing people see // PinPath is for pinning a status to an account profile so that it's the first thing people see
PinPath = BasePathWithID + "/pin" PinPath = BasePathWithID + "/pin"
// UnpinPath is for undoing a pin and returning a status to the ever-swirling drain of time and entropy // UnpinPath is for undoing a pin and returning a status to the ever-swirling drain of time and entropy
UnpinPath = BasePathWithID + "/unpin" UnpinPath = BasePathWithID + "/unpin"
) )

View file

@ -21,12 +21,16 @@ package db
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/url" "net/url"
"sync" "sync"
"github.com/go-fed/activity/pub" "github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
) )
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface. // FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.
@ -35,13 +39,15 @@ type federatingDB struct {
locks *sync.Map locks *sync.Map
db DB db DB
config *config.Config config *config.Config
log *logrus.Entry
} }
func newFederatingDB(db DB, config *config.Config) pub.Database { func newFederatingDB(db DB, config *config.Config, log *logrus.Entry) pub.Database {
return &federatingDB{ return &federatingDB{
locks: new(sync.Map), locks: new(sync.Map),
db: db, db: db,
config: config, config: config,
log: log,
} }
} }
@ -118,11 +124,75 @@ func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOr
return nil return nil
} }
// Owns returns true if the database has an entry for the IRI and it // Owns returns true if the IRI belongs to this instance, and if
// exists in the database. // the database has an entry for the IRI.
//
// The library makes this call only after acquiring a lock first. // The library makes this call only after acquiring a lock first.
func (f *federatingDB) Owns(c context.Context, id *url.URL) (owns bool, err error) { func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
l := f.log.WithFields(logrus.Fields{
"func": "Owns",
"activityID": id.String(),
})
// if the id host isn't this instance host, we don't own this IRI
if id.Host != f.config.Host {
return false, nil
}
// apparently we own it, so what *is* it?
// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
if util.IsStatusesPath(id) {
username, uid, err := util.ParseStatusesPath(id)
if err != nil {
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
}
acct := &gtsmodel.Account{}
if err := f.db.GetWhere("username", username, acct); err != nil {
if _, ok := err.(ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
if acct.Domain != "" {
// this is a remote account so we don't own it after all
return false, nil
}
status := &gtsmodel.Status{}
if err := f.db.GetByID(uid, status); err != nil {
if _, ok := err.(ErrNoEntries); ok {
// there are no entries for this status
return false, nil
}
return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err)
}
// the user exists, the status exists, we own both, we're good
return true, nil
}
// check if it's a user, eg /users/example_username
if util.IsUserPath(id) {
username, err := util.ParseUserPath(id)
if err != nil {
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
}
acct := &gtsmodel.Account{}
if err := f.db.GetWhere("username", username, acct); err != nil {
if _, ok := err.(ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
if acct.Domain != "" {
// this is a remote account so we don't own it after all
return false, nil
}
// the user exists, we own it, we're good
return true, nil
}
l.Info("could not match activityID")
return false, nil return false, nil
} }

View file

@ -95,7 +95,7 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry
cancel: cancel, cancel: cancel,
} }
federatingDB := newFederatingDB(ps, c) federatingDB := newFederatingDB(ps, c, log)
ps.federationDB = federatingDB ps.federationDB = federatingDB
// we can confidently return this useable postgres service now // we can confidently return this useable postgres service now

View file

@ -18,19 +18,70 @@
package util package util
import "regexp" import (
"fmt"
"regexp"
)
const (
minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator
minimumReasonLength = 40
maximumReasonLength = 500
maximumEmailLength = 256
maximumUsernameLength = 64
maximumPasswordLength = 64
maximumEmojiShortcodeLength = 30
maximumHashtagLength = 30
)
var ( var (
// mention regex can be played around with here: https://regex101.com/r/qwM9D3/1 // mention regex can be played around with here: https://regex101.com/r/qwM9D3/1
mentionRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?: |\n)` mentionFinderRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?: |\n)`
mentionRegex = regexp.MustCompile(mentionRegexString) mentionFinderRegex = regexp.MustCompile(mentionFinderRegexString)
// hashtag regex can be played with here: https://regex101.com/r/Vhy8pg/1 // hashtag regex can be played with here: https://regex101.com/r/Vhy8pg/1
hashtagRegexString = `(?: |^|\W)?#([a-zA-Z0-9]{1,30})(?:\b|\r)` hashtagFinderRegexString = fmt.Sprintf(`(?: |^|\W)?#([a-zA-Z0-9]{1,%d})(?:\b|\r)`, maximumHashtagLength)
hashtagRegex = regexp.MustCompile(hashtagRegexString) hashtagFinderRegex = regexp.MustCompile(hashtagFinderRegexString)
// emoji regex can be played with here: https://regex101.com/r/478XGM/1
emojiRegexString = `(?: |^|\W)?:([a-zA-Z0-9_]{2,30}):(?:\b|\r)?`
emojiRegex = regexp.MustCompile(emojiRegexString)
// emoji shortcode regex can be played with here: https://regex101.com/r/zMDRaG/1 // emoji shortcode regex can be played with here: https://regex101.com/r/zMDRaG/1
emojiShortcodeString = `^[a-z0-9_]{2,30}$` emojiShortcodeRegexString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumEmojiShortcodeLength)
emojiShortcodeRegex = regexp.MustCompile(emojiShortcodeString) emojiShortcodeValidationRegex = regexp.MustCompile(fmt.Sprintf("^%s$", emojiShortcodeRegexString))
// emoji regex can be played with here: https://regex101.com/r/478XGM/1
emojiFinderRegexString = fmt.Sprintf(`(?: |^|\W)?:(%s):(?:\b|\r)?`, 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)
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)
likedPathRegexString = fmt.Sprintf(`^/?%s/%s/%s$`, UsersPath, usernameRegexString, LikedPath)
// followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked
likedPathRegex = regexp.MustCompile(likedPathRegexString)
// see https://ihateregex.io/expr/uuid/
uuidRegexString = `[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}`
statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, uuidRegexString)
// statusesPathRegex parses a path that validates and captures the username part and the uuid part
// from eg /users/example_username/statuses/123e4567-e89b-12d3-a456-426655440000.
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
statusesPathRegex = regexp.MustCompile(statusesPathRegexString)
) )

View file

@ -31,7 +31,7 @@ import (
// The case of the returned mentions will be lowered, for consistency. // The case of the returned mentions will be lowered, for consistency.
func DeriveMentions(status string) []string { func DeriveMentions(status string) []string {
mentionedAccounts := []string{} mentionedAccounts := []string{}
for _, m := range mentionRegex.FindAllStringSubmatch(status, -1) { for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) {
mentionedAccounts = append(mentionedAccounts, m[1]) mentionedAccounts = append(mentionedAccounts, m[1])
} }
return lower(unique(mentionedAccounts)) return lower(unique(mentionedAccounts))
@ -43,7 +43,7 @@ func DeriveMentions(status string) []string {
// tags will be lowered, for consistency. // tags will be lowered, for consistency.
func DeriveHashtags(status string) []string { func DeriveHashtags(status string) []string {
tags := []string{} tags := []string{}
for _, m := range hashtagRegex.FindAllStringSubmatch(status, -1) { for _, m := range hashtagFinderRegex.FindAllStringSubmatch(status, -1) {
tags = append(tags, m[1]) tags = append(tags, m[1])
} }
return lower(unique(tags)) return lower(unique(tags))
@ -55,7 +55,7 @@ func DeriveHashtags(status string) []string {
// emojis will be lowered, for consistency. // emojis will be lowered, for consistency.
func DeriveEmojis(status string) []string { func DeriveEmojis(status string) []string {
emojis := []string{} emojis := []string{}
for _, m := range emojiRegex.FindAllStringSubmatch(status, -1) { for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) {
emojis = append(emojis, m[1]) emojis = append(emojis, m[1])
} }
return lower(unique(emojis)) return lower(unique(emojis))

View file

@ -21,44 +21,55 @@ package util
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strings"
) )
const ( const (
// UsersPath is for serving users info // UsersPath is for serving users info
UsersPath = "users" UsersPath = "users"
// ActorsPath is for serving actors info
ActorsPath = "actors"
// StatusesPath is for serving statuses // StatusesPath is for serving statuses
StatusesPath = "statuses" StatusesPath = "statuses"
// InboxPath represents the webfinger inbox location // InboxPath represents the webfinger inbox location
InboxPath = "inbox" InboxPath = "inbox"
// OutboxPath represents the webfinger outbox location // OutboxPath represents the webfinger outbox location
OutboxPath = "outbox" OutboxPath = "outbox"
// FollowersPath represents the webfinger followers location // FollowersPath represents the webfinger followers location
FollowersPath = "followers" FollowersPath = "followers"
// FollowingPath represents the webfinger following location
FollowingPath = "following"
// LikedPath represents the webfinger liked location
LikedPath = "liked"
// CollectionsPath represents the webfinger collections location // CollectionsPath represents the webfinger collections location
CollectionsPath = "collections" CollectionsPath = "collections"
// FeaturedPath represents the webfinger featured location // FeaturedPath represents the webfinger featured location
FeaturedPath = "featured" FeaturedPath = "featured"
) )
// UserURIs contains a bunch of UserURIs and URLs for a user, host, account, etc. // UserURIs contains a bunch of UserURIs and URLs for a user, host, account, etc.
type UserURIs struct { type UserURIs struct {
// The web URL of the instance host, eg https://example.org // The web URL of the instance host, eg https://example.org
HostURL string HostURL string
// The web URL of the user, eg., https://example.org/@example_user // The web URL of the user, eg., https://example.org/@example_user
UserURL string UserURL string
// The web URL for statuses of this user, eg., https://example.org/@example_user/statuses // The web URL for statuses of this user, eg., https://example.org/@example_user/statuses
StatusesURL string StatusesURL string
// The webfinger URI of this user, eg., https://example.org/users/example_user // The webfinger URI of this user, eg., https://example.org/users/example_user
UserURI string UserURI string
// The webfinger URI for this user's statuses, eg., https://example.org/users/example_user/statuses // The webfinger URI for this user's statuses, eg., https://example.org/users/example_user/statuses
StatusesURI string StatusesURI string
// The webfinger URI for this user's activitypub inbox, eg., https://example.org/users/example_user/inbox // The webfinger URI for this user's activitypub inbox, eg., https://example.org/users/example_user/inbox
InboxURI string InboxURI string
// The webfinger URI for this user's activitypub outbox, eg., https://example.org/users/example_user/outbox // The webfinger URI for this user's activitypub outbox, eg., https://example.org/users/example_user/outbox
OutboxURI string OutboxURI string
// The webfinger URI for this user's followers, eg., https://example.org/users/example_user/followers // The webfinger URI for this user's followers, eg., https://example.org/users/example_user/followers
FollowersURI string FollowersURI string
// The webfinger URI for this user's following, eg., https://example.org/users/example_user/following
FollowingURI string
// The webfinger URI for this user's liked posts eg., https://example.org/users/example_user/liked
LikedURI string
// The webfinger URI for this user's featured collections, eg., https://example.org/users/example_user/collections/featured // The webfinger URI for this user's featured collections, eg., https://example.org/users/example_user/collections/featured
CollectionURI string CollectionURI string
} }
@ -76,6 +87,8 @@ func GenerateURIsForAccount(username string, protocol string, host string) *User
inboxURI := fmt.Sprintf("%s/%s", userURI, InboxPath) inboxURI := fmt.Sprintf("%s/%s", userURI, InboxPath)
outboxURI := fmt.Sprintf("%s/%s", userURI, OutboxPath) outboxURI := fmt.Sprintf("%s/%s", userURI, OutboxPath)
followersURI := fmt.Sprintf("%s/%s", userURI, FollowersPath) followersURI := fmt.Sprintf("%s/%s", userURI, FollowersPath)
followingURI := fmt.Sprintf("%s/%s", userURI, FollowingPath)
likedURI := fmt.Sprintf("%s/%s", userURI, LikedPath)
collectionURI := fmt.Sprintf("%s/%s/%s", userURI, CollectionsPath, FeaturedPath) collectionURI := fmt.Sprintf("%s/%s/%s", userURI, CollectionsPath, FeaturedPath)
return &UserURIs{ return &UserURIs{
HostURL: hostURL, HostURL: hostURL,
@ -87,10 +100,61 @@ func GenerateURIsForAccount(username string, protocol string, host string) *User
InboxURI: inboxURI, InboxURI: inboxURI,
OutboxURI: outboxURI, OutboxURI: outboxURI,
FollowersURI: followersURI, FollowersURI: followersURI,
FollowingURI: followingURI,
LikedURI: likedURI,
CollectionURI: collectionURI, CollectionURI: collectionURI,
} }
} }
func ParseActivityPubRequestURL(id *url.URL) error { // IsUserPath returns true if the given URL path corresponds to eg /users/example_username
return nil func IsUserPath(id *url.URL) bool {
return userPathRegex.MatchString(strings.ToLower(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(strings.ToLower(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(strings.ToLower(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(strings.ToLower(id.Path))
}
// IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked
func IsLikedPath(id *url.URL) bool {
return followingPathRegex.MatchString(strings.ToLower(id.Path))
}
// IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
func IsStatusesPath(id *url.URL) bool {
return statusesPathRegex.MatchString(strings.ToLower(id.Path))
}
// ParseStatusesPath returns the username and uuid from a path such as /users/example_username/statuses/SOME_UUID_OF_A_STATUS
func ParseStatusesPath(id *url.URL) (username string, uuid string, err error) {
matches := statusesPathRegex.FindStringSubmatch(id.Path)
if len(matches) != 3 {
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
return
}
username = matches[1]
uuid = matches[2]
return
}
// 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)
if len(matches) != 2 {
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
return
}
username = matches[1]
return
} }

View file

@ -22,45 +22,22 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/mail" "net/mail"
"regexp"
pwv "github.com/wagslane/go-password-validator" pwv "github.com/wagslane/go-password-validator"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
const (
// MinimumPasswordEntropy dictates password strength. See https://github.com/wagslane/go-password-validator
MinimumPasswordEntropy = 60
// MinimumReasonLength is the length of chars we expect as a bare minimum effort
MinimumReasonLength = 40
// MaximumReasonLength is the maximum amount of chars we're happy to accept
MaximumReasonLength = 500
// MaximumEmailLength is the maximum length of an email address we're happy to accept
MaximumEmailLength = 256
// MaximumUsernameLength is the maximum length of a username we're happy to accept
MaximumUsernameLength = 64
// MaximumPasswordLength is the maximum length of a password we're happy to accept
MaximumPasswordLength = 64
// NewUsernameRegexString is string representation of the regular expression for validating usernames
NewUsernameRegexString = `^[a-z0-9_]+$`
)
var (
// NewUsernameRegex is the compiled regex for validating new usernames
NewUsernameRegex = regexp.MustCompile(NewUsernameRegexString)
)
// ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. // ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok.
func ValidateNewPassword(password string) error { func ValidateNewPassword(password string) error {
if password == "" { if password == "" {
return errors.New("no password provided") return errors.New("no password provided")
} }
if len(password) > MaximumPasswordLength { if len(password) > maximumPasswordLength {
return fmt.Errorf("password should be no more than %d chars", MaximumPasswordLength) return fmt.Errorf("password should be no more than %d chars", maximumPasswordLength)
} }
return pwv.Validate(password, MinimumPasswordEntropy) return pwv.Validate(password, minimumPasswordEntropy)
} }
// ValidateUsername makes sure that a given username is valid (ie., letters, numbers, underscores, check length). // ValidateUsername makes sure that a given username is valid (ie., letters, numbers, underscores, check length).
@ -70,11 +47,11 @@ func ValidateUsername(username string) error {
return errors.New("no username provided") return errors.New("no username provided")
} }
if len(username) > MaximumUsernameLength { if len(username) > maximumUsernameLength {
return fmt.Errorf("username should be no more than %d chars but '%s' was %d", MaximumUsernameLength, username, len(username)) return fmt.Errorf("username should be no more than %d chars but '%s' was %d", maximumUsernameLength, username, len(username))
} }
if !NewUsernameRegex.MatchString(username) { if !usernameValidationRegex.MatchString(username) {
return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", username) return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", username)
} }
@ -88,8 +65,8 @@ func ValidateEmail(email string) error {
return errors.New("no email provided") return errors.New("no email provided")
} }
if len(email) > MaximumEmailLength { if len(email) > maximumEmailLength {
return fmt.Errorf("email address should be no more than %d chars but '%s' was %d", MaximumEmailLength, email, len(email)) return fmt.Errorf("email address should be no more than %d chars but '%s' was %d", maximumEmailLength, email, len(email))
} }
_, err := mail.ParseAddress(email) _, err := mail.ParseAddress(email)
@ -118,12 +95,12 @@ func ValidateSignUpReason(reason string, reasonRequired bool) error {
return errors.New("no reason provided") return errors.New("no reason provided")
} }
if len(reason) < MinimumReasonLength { if len(reason) < minimumReasonLength {
return fmt.Errorf("reason should be at least %d chars but '%s' was %d", MinimumReasonLength, reason, len(reason)) return fmt.Errorf("reason should be at least %d chars but '%s' was %d", minimumReasonLength, reason, len(reason))
} }
if len(reason) > MaximumReasonLength { if len(reason) > maximumReasonLength {
return fmt.Errorf("reason should be no more than %d chars but given reason was %d", MaximumReasonLength, len(reason)) return fmt.Errorf("reason should be no more than %d chars but given reason was %d", maximumReasonLength, len(reason))
} }
return nil return nil
} }
@ -150,7 +127,7 @@ func ValidatePrivacy(privacy string) error {
// for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters, // for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters,
// lowercase a-z, numbers, and underscores. // lowercase a-z, numbers, and underscores.
func ValidateEmojiShortcode(shortcode string) error { func ValidateEmojiShortcode(shortcode string) error {
if !emojiShortcodeRegex.MatchString(shortcode) { 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 fmt.Errorf("shortcode %s did not pass validation, must be between 2 and 30 characters, lowercase letters, numbers, and underscores only", shortcode)
} }
return nil return nil