mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:12:25 -05:00 
			
		
		
		
	Start messing around with federation
This commit is contained in:
		
					parent
					
						
							
								b0819c1a63
							
						
					
				
			
			
				commit
				
					
						1c3ad47f52
					
				
			
		
					 10 changed files with 245 additions and 83 deletions
				
			
		|  | @ -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 := >smodel.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 := >smodel.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 := >smodel.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 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -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)) | ||||||
|  |  | ||||||
|  | @ -21,11 +21,14 @@ 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 | ||||||
|  | @ -34,6 +37,10 @@ const ( | ||||||
| 	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 | ||||||
|  | @ -59,6 +66,10 @@ type UserURIs struct { | ||||||
| 	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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue