forked from mirrors/gotosocial
[bugfix] Punycode fixes (#1743)
Co-authored-by: kim <grufwub@gmail.com> Co-authored-by: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>
This commit is contained in:
parent
b7dd32da42
commit
37b4d9d179
11 changed files with 409 additions and 211 deletions
|
@ -142,6 +142,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringNoResolve()
|
||||||
suite.Len(searchResult.Accounts, 0)
|
suite.Len(searchResult.Accounts, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringSpecialChars() {
|
||||||
|
query := "@üser@ëxample.org"
|
||||||
|
resolve := false
|
||||||
|
|
||||||
|
searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(searchResult.Accounts); l != 1 {
|
||||||
|
suite.FailNow("", "expected %d accounts, got %d", 1, l)
|
||||||
|
}
|
||||||
|
suite.Equal("üser@ëxample.org", searchResult.Accounts[0].Acct)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringSpecialCharsPunycode() {
|
||||||
|
query := "@üser@xn--xample-ova.org"
|
||||||
|
resolve := false
|
||||||
|
|
||||||
|
searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(searchResult.Accounts); l != 1 {
|
||||||
|
suite.FailNow("", "expected %d accounts, got %d", 1, l)
|
||||||
|
}
|
||||||
|
suite.Equal("üser@ëxample.org", searchResult.Accounts[0].Acct)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *SearchGetTestSuite) TestSearchLocalAccountByNamestring() {
|
func (suite *SearchGetTestSuite) TestSearchLocalAccountByNamestring() {
|
||||||
query := "@the_mighty_zork"
|
query := "@the_mighty_zork"
|
||||||
resolve := false
|
resolve := false
|
||||||
|
|
|
@ -27,9 +27,11 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"github.com/uptrace/bun/dialect"
|
"github.com/uptrace/bun/dialect"
|
||||||
)
|
)
|
||||||
|
@ -82,6 +84,15 @@ func (a *accountDB) GetAccountByURL(ctx context.Context, url string) (*gtsmodel.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *accountDB) GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, db.Error) {
|
func (a *accountDB) GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, db.Error) {
|
||||||
|
if domain != "" {
|
||||||
|
// Normalize the domain as punycode
|
||||||
|
var err error
|
||||||
|
domain, err = util.Punify(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return a.getAccount(
|
return a.getAccount(
|
||||||
ctx,
|
ctx,
|
||||||
"Username.Domain",
|
"Username.Domain",
|
||||||
|
@ -220,7 +231,10 @@ func (a *accountDB) getAccount(ctx context.Context, lookup string, dbQuery func(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Account) error {
|
func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
var err error
|
var (
|
||||||
|
err error
|
||||||
|
errs = make(gtserror.MultiError, 0, 3)
|
||||||
|
)
|
||||||
|
|
||||||
if account.AvatarMediaAttachment == nil && account.AvatarMediaAttachmentID != "" {
|
if account.AvatarMediaAttachment == nil && account.AvatarMediaAttachmentID != "" {
|
||||||
// Account avatar attachment is not set, fetch from database.
|
// Account avatar attachment is not set, fetch from database.
|
||||||
|
@ -229,7 +243,7 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
||||||
account.AvatarMediaAttachmentID,
|
account.AvatarMediaAttachmentID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error populating account avatar: %w", err)
|
errs.Append(fmt.Errorf("error populating account avatar: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +254,7 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
||||||
account.HeaderMediaAttachmentID,
|
account.HeaderMediaAttachmentID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error populating account header: %w", err)
|
errs.Append(fmt.Errorf("error populating account header: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,11 +265,11 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
||||||
account.EmojiIDs,
|
account.EmojiIDs,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error populating account emojis: %w", err)
|
errs.Append(fmt.Errorf("error populating account emojis: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return errs.Combine()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) db.Error {
|
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) db.Error {
|
||||||
|
|
|
@ -20,14 +20,13 @@ package bundb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type domainDB struct {
|
type domainDB struct {
|
||||||
|
@ -35,22 +34,10 @@ type domainDB struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeDomain converts the given domain to lowercase
|
|
||||||
// then to punycode (for international domain names).
|
|
||||||
//
|
|
||||||
// Returns the resulting domain or an error if the
|
|
||||||
// punycode conversion fails.
|
|
||||||
func normalizeDomain(domain string) (out string, err error) {
|
|
||||||
out = strings.ToLower(domain)
|
|
||||||
out, err = idna.ToASCII(out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) db.Error {
|
func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) db.Error {
|
||||||
var err error
|
|
||||||
|
|
||||||
// Normalize the domain as punycode
|
// Normalize the domain as punycode
|
||||||
block.Domain, err = normalizeDomain(block.Domain)
|
var err error
|
||||||
|
block.Domain, err = util.Punify(block.Domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -69,10 +56,8 @@ func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *domainDB) GetDomainBlock(ctx context.Context, domain string) (*gtsmodel.DomainBlock, db.Error) {
|
func (d *domainDB) GetDomainBlock(ctx context.Context, domain string) (*gtsmodel.DomainBlock, db.Error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
// Normalize the domain as punycode
|
// Normalize the domain as punycode
|
||||||
domain, err = normalizeDomain(domain)
|
domain, err := util.Punify(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -98,9 +83,8 @@ func (d *domainDB) GetDomainBlock(ctx context.Context, domain string) (*gtsmodel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *domainDB) DeleteDomainBlock(ctx context.Context, domain string) db.Error {
|
func (d *domainDB) DeleteDomainBlock(ctx context.Context, domain string) db.Error {
|
||||||
var err error
|
// Normalize the domain as punycode
|
||||||
|
domain, err := util.Punify(domain)
|
||||||
domain, err = normalizeDomain(domain)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -121,7 +105,7 @@ func (d *domainDB) DeleteDomainBlock(ctx context.Context, domain string) db.Erro
|
||||||
|
|
||||||
func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, db.Error) {
|
func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, db.Error) {
|
||||||
// Normalize the domain as punycode
|
// Normalize the domain as punycode
|
||||||
domain, err := normalizeDomain(domain)
|
domain, err := util.Punify(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
|
@ -79,8 +80,15 @@ func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, i := range instances {
|
for _, i := range instances {
|
||||||
domain := &apimodel.Domain{Domain: i.Domain}
|
// Domain may be in Punycode,
|
||||||
domains = append(domains, domain)
|
// de-punify it just in case.
|
||||||
|
d, err := util.DePunify(i.Domain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "couldn't depunify domain %s: %s", i.Domain, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
domains = append(domains, &apimodel.Domain{Domain: d})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,17 +98,25 @@ func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool,
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range domainBlocks {
|
for _, domainBlock := range domainBlocks {
|
||||||
if *d.Obfuscate {
|
// Domain may be in Punycode,
|
||||||
d.Domain = obfuscate(d.Domain)
|
// de-punify it just in case.
|
||||||
|
d, err := util.DePunify(domainBlock.Domain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "couldn't depunify domain %s: %s", domainBlock.Domain, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
domain := &apimodel.Domain{
|
if *domainBlock.Obfuscate {
|
||||||
Domain: d.Domain,
|
// Obfuscate the de-punified version.
|
||||||
SuspendedAt: util.FormatISO8601(d.CreatedAt),
|
d = obfuscate(d)
|
||||||
PublicComment: d.PublicComment,
|
|
||||||
}
|
}
|
||||||
domains = append(domains, domain)
|
|
||||||
|
domains = append(domains, &apimodel.Domain{
|
||||||
|
Domain: d,
|
||||||
|
SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt),
|
||||||
|
PublicComment: domainBlock.PublicComment,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ package regexes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
@ -39,15 +38,42 @@ const (
|
||||||
follow = "follow"
|
follow = "follow"
|
||||||
blocks = "blocks"
|
blocks = "blocks"
|
||||||
reports = "reports"
|
reports = "reports"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
schemes = `(http|https)://` // Allowed URI protocols for parsing links in text.
|
||||||
maximumUsernameLength = 64
|
alphaNumeric = `\p{L}\p{M}*|\p{N}` // A single number or script character in any language, including chars with accents.
|
||||||
maximumEmojiShortcodeLength = 30
|
usernameGrp = `(?:` + alphaNumeric + `|\.|\-|\_)` // Non-capturing group that matches against a single valid username character.
|
||||||
|
domainGrp = `(?:` + alphaNumeric + `|\.|\-|\:)` // Non-capturing group that matches against a single valid domain character.
|
||||||
|
mentionName = `^@(` + usernameGrp + `+)(?:@(` + domainGrp + `+))?$` // Extract parts of one mention, maybe including domain.
|
||||||
|
mentionFinder = `(?:^|\s)(@` + usernameGrp + `+(?:@` + domainGrp + `+)?)` // Extract all mentions from a text, each mention may include domain.
|
||||||
|
emojiShortcode = `\w{2,30}` // Pattern for emoji shortcodes. maximumEmojiShortcodeLength = 30
|
||||||
|
emojiFinder = `(?:\b)?:(` + emojiShortcode + `):(?:\b)?` // Extract all emoji shortcodes from a text.
|
||||||
|
usernameStrict = `^[a-z0-9_]{2,64}$` // Pattern for usernames on THIS instance. maximumUsernameLength = 64
|
||||||
|
usernameRelaxed = `[a-z0-9_\.]{2,}` // Relaxed version of username that can match instance accounts too.
|
||||||
|
misskeyReportNotesFinder = `(?m)(?:^Note: ((?:http|https):\/\/.*)$)` // Extract reported Note URIs from the text of a Misskey report/flag.
|
||||||
|
ulid = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}` // Pattern for ULID.
|
||||||
|
ulidValidate = `^` + ulid + `$` // Validate one ULID.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Path parts / capture.
|
||||||
|
*/
|
||||||
|
|
||||||
|
userPathPrefix = `^/?` + users + `/(` + usernameRelaxed + `)`
|
||||||
|
userPath = userPathPrefix + `$`
|
||||||
|
publicKeyPath = userPathPrefix + `/` + publicKey + `$`
|
||||||
|
inboxPath = userPathPrefix + `/` + inbox + `$`
|
||||||
|
outboxPath = userPathPrefix + `/` + outbox + `$`
|
||||||
|
followersPath = userPathPrefix + `/` + followers + `$`
|
||||||
|
followingPath = userPathPrefix + `/` + following + `$`
|
||||||
|
likedPath = userPathPrefix + `/` + liked + `$`
|
||||||
|
followPath = userPathPrefix + `/` + follow + `/(` + ulid + `)$`
|
||||||
|
likePath = userPathPrefix + `/` + liked + `/(` + ulid + `)$`
|
||||||
|
statusesPath = userPathPrefix + `/` + statuses + `/(` + ulid + `)$`
|
||||||
|
blockPath = userPathPrefix + `/` + blocks + `/(` + ulid + `)$`
|
||||||
|
reportPath = `^/?` + reports + `/(` + ulid + `)$`
|
||||||
|
filePath = `^/?(` + ulid + `)/([a-z]+)/([a-z]+)/(` + ulid + `)\.([a-z]+)$`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
schemes = `(http|https)://`
|
|
||||||
// LinkScheme captures http/https schemes in URLs.
|
// LinkScheme captures http/https schemes in URLs.
|
||||||
LinkScheme = func() *regexp.Regexp {
|
LinkScheme = func() *regexp.Regexp {
|
||||||
rgx, err := xurls.StrictMatchingScheme(schemes)
|
rgx, err := xurls.StrictMatchingScheme(schemes)
|
||||||
|
@ -57,107 +83,80 @@ var (
|
||||||
return rgx
|
return rgx
|
||||||
}()
|
}()
|
||||||
|
|
||||||
mentionName = `^@([\w\-\.]+)(?:@([\w\-\.:]+))?$`
|
// MentionName captures the username and domain part from
|
||||||
// MentionName captures the username and domain part from a mention string
|
// a mention string such as @whatever_user@example.org,
|
||||||
// such as @whatever_user@example.org, returning whatever_user and example.org (without the @ symbols)
|
// returning whatever_user and example.org (without the @ symbols).
|
||||||
|
// Will also work for characters with umlauts and other accents.
|
||||||
|
// See: https://regex101.com/r/9tjNUy/1 for explanation and examples.
|
||||||
MentionName = regexp.MustCompile(mentionName)
|
MentionName = regexp.MustCompile(mentionName)
|
||||||
|
|
||||||
// mention regex can be played around with here: https://regex101.com/r/P0vpYG/1
|
// MentionFinder extracts whole mentions from a piece of text.
|
||||||
mentionFinder = `(?:^|\s)(@\w+(?:@[a-zA-Z0-9_\-\.]+)?)`
|
|
||||||
// MentionFinder extracts mentions from a piece of text.
|
|
||||||
MentionFinder = regexp.MustCompile(mentionFinder)
|
MentionFinder = regexp.MustCompile(mentionFinder)
|
||||||
|
|
||||||
emojiShortcode = fmt.Sprintf(`\w{2,%d}`, maximumEmojiShortcodeLength)
|
|
||||||
// EmojiShortcode validates an emoji name.
|
// EmojiShortcode validates an emoji name.
|
||||||
EmojiShortcode = regexp.MustCompile(fmt.Sprintf("^%s$", emojiShortcode))
|
EmojiShortcode = regexp.MustCompile(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 extracts emoji strings from a piece of text.
|
||||||
EmojiFinder = regexp.MustCompile(emojiFinderString)
|
// See: https://regex101.com/r/478XGM/1
|
||||||
|
EmojiFinder = regexp.MustCompile(emojiFinder)
|
||||||
|
|
||||||
// usernameString defines an acceptable username for a new account on this instance
|
// Username can be used to validate usernames of new signups on this instance.
|
||||||
usernameString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumUsernameLength)
|
Username = regexp.MustCompile(usernameStrict)
|
||||||
// Username can be used to validate usernames of new signups
|
|
||||||
Username = regexp.MustCompile(fmt.Sprintf(`^%s$`, usernameString))
|
|
||||||
|
|
||||||
// usernameStringRelaxed is like usernameString, but also allows the '.' character,
|
// MisskeyReportNotes captures a list of Note URIs from report content created by Misskey.
|
||||||
// so it can also be used to match the instance account, which will have a username
|
// See: https://regex101.com/r/EnTOBV/1
|
||||||
// like 'example.org', and it has no upper length limit, so will work for long domains.
|
MisskeyReportNotes = regexp.MustCompile(misskeyReportNotesFinder)
|
||||||
usernameStringRelaxed = `[a-z0-9_\.]{2,}`
|
|
||||||
|
|
||||||
userPathString = fmt.Sprintf(`^/?%s/(%s)$`, users, usernameStringRelaxed)
|
// UserPath validates and captures the username part from eg /users/example_username.
|
||||||
// UserPath parses a path that validates and captures the username part from eg /users/example_username
|
UserPath = regexp.MustCompile(userPath)
|
||||||
UserPath = regexp.MustCompile(userPathString)
|
|
||||||
|
|
||||||
publicKeyPath = fmt.Sprintf(`^/?%s/(%s)/%s`, users, usernameStringRelaxed, publicKey)
|
|
||||||
// PublicKeyPath parses a path that validates and captures the username part from eg /users/example_username/main-key
|
// PublicKeyPath parses a path that validates and captures the username part from eg /users/example_username/main-key
|
||||||
PublicKeyPath = regexp.MustCompile(publicKeyPath)
|
PublicKeyPath = regexp.MustCompile(publicKeyPath)
|
||||||
|
|
||||||
inboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, inbox)
|
|
||||||
// InboxPath parses a path that validates and captures the username part from eg /users/example_username/inbox
|
// InboxPath parses a path that validates and captures the username part from eg /users/example_username/inbox
|
||||||
InboxPath = regexp.MustCompile(inboxPath)
|
InboxPath = regexp.MustCompile(inboxPath)
|
||||||
|
|
||||||
outboxPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, outbox)
|
|
||||||
// OutboxPath parses a path that validates and captures the username part from eg /users/example_username/outbox
|
// OutboxPath parses a path that validates and captures the username part from eg /users/example_username/outbox
|
||||||
OutboxPath = regexp.MustCompile(outboxPath)
|
OutboxPath = regexp.MustCompile(outboxPath)
|
||||||
|
|
||||||
actorPath = fmt.Sprintf(`^/?%s/(%s)$`, actors, usernameStringRelaxed)
|
|
||||||
// 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, usernameStringRelaxed, followers)
|
|
||||||
// FollowersPath parses a path that validates and captures the username part from eg /users/example_username/followers
|
// FollowersPath parses a path that validates and captures the username part from eg /users/example_username/followers
|
||||||
FollowersPath = regexp.MustCompile(followersPath)
|
FollowersPath = regexp.MustCompile(followersPath)
|
||||||
|
|
||||||
followingPath = fmt.Sprintf(`^/?%s/(%s)/%s$`, users, usernameStringRelaxed, following)
|
|
||||||
// FollowingPath parses a path that validates and captures the username part from eg /users/example_username/following
|
// FollowingPath parses a path that validates and captures the username part from eg /users/example_username/following
|
||||||
FollowingPath = regexp.MustCompile(followingPath)
|
FollowingPath = regexp.MustCompile(followingPath)
|
||||||
|
|
||||||
followPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameStringRelaxed, follow, ulid)
|
// LikedPath parses a path that validates and captures the username part from eg /users/example_username/liked
|
||||||
|
LikedPath = regexp.MustCompile(likedPath)
|
||||||
|
|
||||||
|
// ULID parses and validate a ULID.
|
||||||
|
ULID = regexp.MustCompile(ulidValidate)
|
||||||
|
|
||||||
// FollowPath parses a path that validates and captures the username part and the ulid part
|
// FollowPath parses a path that validates and captures the username part and the ulid part
|
||||||
// from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH
|
// from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
FollowPath = regexp.MustCompile(followPath)
|
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, usernameStringRelaxed, 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, usernameStringRelaxed, liked, ulid)
|
|
||||||
// LikePath parses a path that validates and captures the username part and the ulid part
|
// LikePath parses a path that validates and captures the username part and the ulid part
|
||||||
// from eg /users/example_username/like/01F7XT5JZW1WMVSW1KADS8PVDH
|
// from eg /users/example_username/liked/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
LikePath = regexp.MustCompile(likePath)
|
LikePath = regexp.MustCompile(likePath)
|
||||||
|
|
||||||
statusesPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameStringRelaxed, statuses, ulid)
|
|
||||||
// StatusesPath parses a path that validates and captures the username part and the ulid part
|
// StatusesPath parses a path that validates and captures the username part and the ulid part
|
||||||
// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH
|
// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
|
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
|
||||||
StatusesPath = regexp.MustCompile(statusesPath)
|
StatusesPath = regexp.MustCompile(statusesPath)
|
||||||
|
|
||||||
blockPath = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, users, usernameStringRelaxed, blocks, ulid)
|
|
||||||
// BlockPath parses a path that validates and captures the username part and the ulid part
|
// BlockPath parses a path that validates and captures the username part and the ulid part
|
||||||
// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH
|
// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
BlockPath = regexp.MustCompile(blockPath)
|
BlockPath = regexp.MustCompile(blockPath)
|
||||||
|
|
||||||
reportPath = fmt.Sprintf(`^/?%s/(%s)$`, reports, ulid)
|
|
||||||
// ReportPath parses a path that validates and captures the ulid part
|
// ReportPath parses a path that validates and captures the ulid part
|
||||||
// from eg /reports/01GP3AWY4CRDVRNZKW0TEAMB5R
|
// from eg /reports/01GP3AWY4CRDVRNZKW0TEAMB5R
|
||||||
ReportPath = regexp.MustCompile(reportPath)
|
ReportPath = regexp.MustCompile(reportPath)
|
||||||
|
|
||||||
filePath = fmt.Sprintf(`^(%s)/([a-z]+)/([a-z]+)/(%s)\.([a-z]+)$`, ulid, ulid)
|
|
||||||
// FilePath parses a file storage path of the form [ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]
|
// FilePath parses a file storage path of the form [ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]
|
||||||
// eg 01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpeg
|
// eg 01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpeg
|
||||||
// It captures the account id, media type, media size, file name, and file extension, eg
|
// It captures the account id, media type, media size, file name, and file extension, eg
|
||||||
// `01F8MH1H7YV1Z7D2C8K2730QBF`, `attachment`, `small`, `01F8MH8RMYQ6MSNY3JM2XT1CQ5`, `jpeg`.
|
// `01F8MH1H7YV1Z7D2C8K2730QBF`, `attachment`, `small`, `01F8MH8RMYQ6MSNY3JM2XT1CQ5`, `jpeg`.
|
||||||
FilePath = regexp.MustCompile(filePath)
|
FilePath = regexp.MustCompile(filePath)
|
||||||
|
|
||||||
// MisskeyReportNotes captures a list of Note URIs from report content created by Misskey.
|
|
||||||
// https://regex101.com/r/EnTOBV/1
|
|
||||||
MisskeyReportNotes = regexp.MustCompile(`(?m)(?:^Note: ((?:http|https):\/\/.*)$)`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// bufpool is a memory pool of byte buffers for use in our regex utility functions.
|
// bufpool is a memory pool of byte buffers for use in our regex utility functions.
|
||||||
|
|
|
@ -19,6 +19,7 @@ package typeutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -26,6 +27,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -83,99 +85,110 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
||||||
// count followers
|
if err := c.db.PopulateAccount(ctx, a); err != nil {
|
||||||
|
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic account stats:
|
||||||
|
// - Followers count
|
||||||
|
// - Following count
|
||||||
|
// - Statuses count
|
||||||
|
// - Last status time
|
||||||
|
|
||||||
followersCount, err := c.db.CountAccountFollowers(ctx, a.ID)
|
followersCount, err := c.db.CountAccountFollowers(ctx, a.ID)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, fmt.Errorf("error counting followers: %s", err)
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting followers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// count following
|
|
||||||
followingCount, err := c.db.CountAccountFollows(ctx, a.ID)
|
followingCount, err := c.db.CountAccountFollows(ctx, a.ID)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, fmt.Errorf("error counting following: %s", err)
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting following: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// count statuses
|
|
||||||
statusesCount, err := c.db.CountAccountStatuses(ctx, a.ID)
|
statusesCount, err := c.db.CountAccountStatuses(ctx, a.ID)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, fmt.Errorf("error counting statuses: %s", err)
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check when the last status was
|
|
||||||
var lastStatusAt *string
|
var lastStatusAt *string
|
||||||
lastPosted, err := c.db.GetAccountLastPosted(ctx, a.ID, false)
|
lastPosted, err := c.db.GetAccountLastPosted(ctx, a.ID, false)
|
||||||
if err == nil && !lastPosted.IsZero() {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
lastStatusAtTemp := util.FormatISO8601(lastPosted)
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err)
|
||||||
lastStatusAt = &lastStatusAtTemp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set account avatar fields if available
|
if !lastPosted.IsZero() {
|
||||||
var aviURL string
|
lastStatusAt = func() *string { t := util.FormatISO8601(lastPosted); return &t }()
|
||||||
var aviURLStatic string
|
|
||||||
if a.AvatarMediaAttachmentID != "" {
|
|
||||||
if a.AvatarMediaAttachment == nil {
|
|
||||||
avi, err := c.db.GetAttachmentByID(ctx, a.AvatarMediaAttachmentID)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf(ctx, "error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err)
|
|
||||||
}
|
|
||||||
a.AvatarMediaAttachment = avi
|
|
||||||
}
|
|
||||||
if a.AvatarMediaAttachment != nil {
|
|
||||||
aviURL = a.AvatarMediaAttachment.URL
|
|
||||||
aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set account header fields if available
|
// Profile media + nice extras:
|
||||||
var headerURL string
|
// - Avatar
|
||||||
var headerURLStatic string
|
// - Header
|
||||||
if a.HeaderMediaAttachmentID != "" {
|
// - Fields
|
||||||
if a.HeaderMediaAttachment == nil {
|
// - Emojis
|
||||||
avi, err := c.db.GetAttachmentByID(ctx, a.HeaderMediaAttachmentID)
|
|
||||||
if err != nil {
|
var (
|
||||||
log.Errorf(ctx, "error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err)
|
aviURL string
|
||||||
}
|
aviURLStatic string
|
||||||
a.HeaderMediaAttachment = avi
|
headerURL string
|
||||||
}
|
headerURLStatic string
|
||||||
if a.HeaderMediaAttachment != nil {
|
fields = make([]apimodel.Field, len(a.Fields))
|
||||||
headerURL = a.HeaderMediaAttachment.URL
|
)
|
||||||
headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL
|
|
||||||
}
|
if a.AvatarMediaAttachment != nil {
|
||||||
|
aviURL = a.AvatarMediaAttachment.URL
|
||||||
|
aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// preallocate frontend fields slice
|
if a.HeaderMediaAttachment != nil {
|
||||||
fields := make([]apimodel.Field, len(a.Fields))
|
headerURL = a.HeaderMediaAttachment.URL
|
||||||
|
headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL
|
||||||
|
}
|
||||||
|
|
||||||
// Convert account GTS model fields to frontend
|
// GTS model fields -> frontend.
|
||||||
for i, field := range a.Fields {
|
for i, field := range a.Fields {
|
||||||
mField := apimodel.Field{
|
mField := apimodel.Field{
|
||||||
Name: field.Name,
|
Name: field.Name,
|
||||||
Value: field.Value,
|
Value: field.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !field.VerifiedAt.IsZero() {
|
if !field.VerifiedAt.IsZero() {
|
||||||
mField.VerifiedAt = util.FormatISO8601(field.VerifiedAt)
|
mField.VerifiedAt = util.FormatISO8601(field.VerifiedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
fields[i] = mField
|
fields[i] = mField
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert account gts model emojis to frontend api model emojis
|
// GTS model emojis -> frontend.
|
||||||
apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, a.Emojis, a.EmojiIDs)
|
apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, a.Emojis, a.EmojiIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error converting account emojis: %v", err)
|
log.Errorf(ctx, "error converting account emojis: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var acct string
|
// Bits that vary between remote + local accounts:
|
||||||
var role *apimodel.AccountRole
|
// - Account (acct) string.
|
||||||
|
// - Role.
|
||||||
|
|
||||||
if a.Domain != "" {
|
var (
|
||||||
// this is a remote user
|
acct string
|
||||||
acct = a.Username + "@" + a.Domain
|
role *apimodel.AccountRole
|
||||||
|
)
|
||||||
|
|
||||||
|
if a.IsRemote() {
|
||||||
|
// Domain may be in Punycode,
|
||||||
|
// de-punify it just in case.
|
||||||
|
d, err := util.DePunify(a.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acct = a.Username + "@" + d
|
||||||
} else {
|
} else {
|
||||||
// this is a local user
|
// This is a local user.
|
||||||
acct = a.Username
|
acct = a.Username
|
||||||
|
|
||||||
user, err := c.db.GetUserByAccountID(ctx, a.ID)
|
user, err := c.db.GetUserByAccountID(ctx, a.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %s", a.ID, err)
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %w", a.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -188,10 +201,8 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var suspended bool
|
// Remaining properties are simple and
|
||||||
if !a.SuspendedAt.IsZero() {
|
// can be populated directly below.
|
||||||
suspended = true
|
|
||||||
}
|
|
||||||
|
|
||||||
accountFrontend := &apimodel.Account{
|
accountFrontend := &apimodel.Account{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
|
@ -214,12 +225,14 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
LastStatusAt: lastStatusAt,
|
LastStatusAt: lastStatusAt,
|
||||||
Emojis: apiEmojis,
|
Emojis: apiEmojis,
|
||||||
Fields: fields,
|
Fields: fields,
|
||||||
Suspended: suspended,
|
Suspended: !a.SuspendedAt.IsZero(),
|
||||||
CustomCSS: a.CustomCSS,
|
CustomCSS: a.CustomCSS,
|
||||||
EnableRSS: *a.EnableRSS,
|
EnableRSS: *a.EnableRSS,
|
||||||
Role: role,
|
Role: role,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bodge default avatar + header in,
|
||||||
|
// if we didn't have one already.
|
||||||
c.ensureAvatar(accountFrontend)
|
c.ensureAvatar(accountFrontend)
|
||||||
c.ensureHeader(accountFrontend)
|
c.ensureHeader(accountFrontend)
|
||||||
|
|
||||||
|
@ -227,18 +240,37 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
||||||
var acct string
|
var (
|
||||||
if a.Domain != "" {
|
acct string
|
||||||
// this is a remote user
|
role *apimodel.AccountRole
|
||||||
acct = fmt.Sprintf("%s@%s", a.Username, a.Domain)
|
)
|
||||||
} else {
|
|
||||||
// this is a local user
|
|
||||||
acct = a.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
var suspended bool
|
if a.IsRemote() {
|
||||||
if !a.SuspendedAt.IsZero() {
|
// Domain may be in Punycode,
|
||||||
suspended = true
|
// de-punify it just in case.
|
||||||
|
d, err := util.DePunify(a.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acct = a.Username + "@" + d
|
||||||
|
} else {
|
||||||
|
// This is a local user.
|
||||||
|
acct = a.Username
|
||||||
|
|
||||||
|
user, err := c.db.GetUserByAccountID(ctx, a.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %s", a.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case *user.Admin:
|
||||||
|
role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin}
|
||||||
|
case *user.Moderator:
|
||||||
|
role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator}
|
||||||
|
default:
|
||||||
|
role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &apimodel.Account{
|
return &apimodel.Account{
|
||||||
|
@ -249,7 +281,8 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
|
||||||
Bot: *a.Bot,
|
Bot: *a.Bot,
|
||||||
CreatedAt: util.FormatISO8601(a.CreatedAt),
|
CreatedAt: util.FormatISO8601(a.CreatedAt),
|
||||||
URL: a.URL,
|
URL: a.URL,
|
||||||
Suspended: suspended,
|
Suspended: !a.SuspendedAt.IsZero(),
|
||||||
|
Role: role,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,15 +296,20 @@ func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
|
||||||
inviteRequest *string
|
inviteRequest *string
|
||||||
approved bool
|
approved bool
|
||||||
disabled bool
|
disabled bool
|
||||||
silenced bool
|
|
||||||
suspended bool
|
|
||||||
role = apimodel.AccountRole{Name: apimodel.AccountRoleUser} // assume user by default
|
role = apimodel.AccountRole{Name: apimodel.AccountRoleUser} // assume user by default
|
||||||
createdByApplicationID string
|
createdByApplicationID string
|
||||||
)
|
)
|
||||||
|
|
||||||
// take user-level information if possible
|
// take user-level information if possible
|
||||||
if a.IsRemote() {
|
if a.IsRemote() {
|
||||||
domain = &a.Domain
|
// Domain may be in Punycode,
|
||||||
|
// de-punify it just in case.
|
||||||
|
d, err := util.DePunify(a.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AccountToAdminAPIAccount: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
domain = &d
|
||||||
} else {
|
} else {
|
||||||
user, err := c.db.GetUserByAccountID(ctx, a.ID)
|
user, err := c.db.GetUserByAccountID(ctx, a.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -303,9 +341,6 @@ func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
|
||||||
createdByApplicationID = user.CreatedByApplicationID
|
createdByApplicationID = user.CreatedByApplicationID
|
||||||
}
|
}
|
||||||
|
|
||||||
silenced = !a.SilencedAt.IsZero()
|
|
||||||
suspended = !a.SuspendedAt.IsZero()
|
|
||||||
|
|
||||||
apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
|
apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("AccountToAdminAPIAccount: error converting account to api account for account id %s: %w", a.ID, err)
|
return nil, fmt.Errorf("AccountToAdminAPIAccount: error converting account to api account for account id %s: %w", a.ID, err)
|
||||||
|
@ -325,8 +360,8 @@ func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
|
||||||
Confirmed: confirmed,
|
Confirmed: confirmed,
|
||||||
Approved: approved,
|
Approved: approved,
|
||||||
Disabled: disabled,
|
Disabled: disabled,
|
||||||
Silenced: silenced,
|
Silenced: !a.SilencedAt.IsZero(),
|
||||||
Suspended: suspended,
|
Suspended: !a.SuspendedAt.IsZero(),
|
||||||
Account: apiAccount,
|
Account: apiAccount,
|
||||||
CreatedByApplicationID: createdByApplicationID,
|
CreatedByApplicationID: createdByApplicationID,
|
||||||
InvitedByAccountID: "", // not implemented (yet)
|
InvitedByAccountID: "", // not implemented (yet)
|
||||||
|
@ -428,16 +463,19 @@ func (c *converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention
|
||||||
m.TargetAccount = targetAccount
|
m.TargetAccount = targetAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
var local bool
|
|
||||||
if m.TargetAccount.Domain == "" {
|
|
||||||
local = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var acct string
|
var acct string
|
||||||
if local {
|
if m.TargetAccount.IsLocal() {
|
||||||
acct = m.TargetAccount.Username
|
acct = m.TargetAccount.Username
|
||||||
} else {
|
} else {
|
||||||
acct = fmt.Sprintf("%s@%s", m.TargetAccount.Username, m.TargetAccount.Domain)
|
// Domain may be in Punycode,
|
||||||
|
// de-punify it just in case.
|
||||||
|
d, err := util.DePunify(m.TargetAccount.Domain)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("MentionToAPIMention: error de-punifying domain %s for account id %s: %w", m.TargetAccount.Domain, m.TargetAccountID, err)
|
||||||
|
return apimodel.Mention{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acct = m.TargetAccount.Username + "@" + d
|
||||||
}
|
}
|
||||||
|
|
||||||
return apimodel.Mention{
|
return apimodel.Mention{
|
||||||
|
@ -476,6 +514,17 @@ func (c *converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.Domain != "" {
|
||||||
|
// Domain may be in Punycode,
|
||||||
|
// de-punify it just in case.
|
||||||
|
var err error
|
||||||
|
e.Domain, err = util.DePunify(e.Domain)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("EmojiToAdminAPIEmoji: error de-punifying domain %s for emoji id %s: %w", e.Domain, e.ID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &apimodel.AdminEmoji{
|
return &apimodel.AdminEmoji{
|
||||||
Emoji: emoji,
|
Emoji: emoji,
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
|
@ -942,9 +991,16 @@ func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmod
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) {
|
func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) {
|
||||||
|
// Domain may be in Punycode,
|
||||||
|
// de-punify it just in case.
|
||||||
|
d, err := util.DePunify(b.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("DomainBlockToAPIDomainBlock: error de-punifying domain %s: %w", b.Domain, err)
|
||||||
|
}
|
||||||
|
|
||||||
domainBlock := &apimodel.DomainBlock{
|
domainBlock := &apimodel.DomainBlock{
|
||||||
Domain: apimodel.Domain{
|
Domain: apimodel.Domain{
|
||||||
Domain: b.Domain,
|
Domain: d,
|
||||||
PublicComment: b.PublicComment,
|
PublicComment: b.PublicComment,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,10 +70,12 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() {
|
func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() {
|
||||||
testAccount := suite.testAccounts["local_account_1"] // take zork for this test
|
testAccount := >smodel.Account{}
|
||||||
|
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
|
||||||
testEmoji := suite.testEmojis["rainbow"]
|
testEmoji := suite.testEmojis["rainbow"]
|
||||||
|
|
||||||
testAccount.Emojis = []*gtsmodel.Emoji{testEmoji}
|
testAccount.Emojis = []*gtsmodel.Emoji{testEmoji}
|
||||||
|
testAccount.EmojiIDs = []string{testEmoji.ID}
|
||||||
|
|
||||||
apiAccount, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), testAccount)
|
apiAccount, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), testAccount)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
@ -210,6 +212,42 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *InternalToFrontendTestSuite) TestAccountToFrontendPublicPunycode() {
|
||||||
|
testAccount := suite.testAccounts["remote_account_4"]
|
||||||
|
apiAccount, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), testAccount)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(apiAccount)
|
||||||
|
|
||||||
|
b, err := json.MarshalIndent(apiAccount, "", " ")
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// Even though account domain is stored in
|
||||||
|
// punycode, it should be served in its
|
||||||
|
// unicode representation in the 'acct' field.
|
||||||
|
suite.Equal(`{
|
||||||
|
"id": "07GZRBAEMBNKGZ8Z9VSKSXKR98",
|
||||||
|
"username": "üser",
|
||||||
|
"acct": "üser@ëxample.org",
|
||||||
|
"display_name": "",
|
||||||
|
"locked": false,
|
||||||
|
"discoverable": false,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-08-10T12:13:28.000Z",
|
||||||
|
"note": "",
|
||||||
|
"url": "https://xn--xample-ova.org/users/@%C3%BCser",
|
||||||
|
"avatar": "",
|
||||||
|
"avatar_static": "",
|
||||||
|
"header": "http://localhost:8080/assets/default_header.png",
|
||||||
|
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"statuses_count": 0,
|
||||||
|
"last_status_at": null,
|
||||||
|
"emojis": [],
|
||||||
|
"fields": []
|
||||||
|
}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
||||||
testStatus := suite.testStatuses["admin_account_status_1"]
|
testStatus := suite.testStatuses["admin_account_status_1"]
|
||||||
requestingAccount := suite.testAccounts["local_account_1"]
|
requestingAccount := suite.testAccounts["local_account_1"]
|
||||||
|
|
|
@ -193,11 +193,6 @@ func IsOutboxPath(id *url.URL) bool {
|
||||||
return regexes.OutboxPath.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 regexes.ActorPath.MatchString(id.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsFollowersPath returns true if the given URL path corresponds to eg /users/example_username/followers
|
// IsFollowersPath returns true if the given URL path corresponds to eg /users/example_username/followers
|
||||||
func IsFollowersPath(id *url.URL) bool {
|
func IsFollowersPath(id *url.URL) bool {
|
||||||
return regexes.FollowersPath.MatchString(id.Path)
|
return regexes.FollowersPath.MatchString(id.Path)
|
||||||
|
|
44
internal/util/punycode.go
Normal file
44
internal/util/punycode.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Punify converts the given domain to lowercase
|
||||||
|
// then to punycode (for international domain names).
|
||||||
|
//
|
||||||
|
// Returns the resulting domain or an error if the
|
||||||
|
// punycode conversion fails.
|
||||||
|
func Punify(domain string) (string, error) {
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
return idna.ToASCII(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DePunify converts the given punycode string
|
||||||
|
// to its original unicode representation (lowercased).
|
||||||
|
// Noop if the domain is (already) not puny.
|
||||||
|
//
|
||||||
|
// Returns an error if conversion fails.
|
||||||
|
func DePunify(domain string) (string, error) {
|
||||||
|
out, err := idna.ToUnicode(domain)
|
||||||
|
return strings.ToLower(out), err
|
||||||
|
}
|
|
@ -96,44 +96,28 @@ func (suite *ValidationTestSuite) TestValidateUsername() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
err = validate.Username(empty)
|
err = validate.Username(empty)
|
||||||
if assert.Error(suite.T(), err) {
|
suite.EqualError(err, "no username provided")
|
||||||
assert.Equal(suite.T(), errors.New("no username provided"), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validate.Username(tooLong)
|
err = validate.Username(tooLong)
|
||||||
if assert.Error(suite.T(), err) {
|
suite.EqualError(err, fmt.Sprintf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", tooLong))
|
||||||
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)
|
err = validate.Username(withSpaces)
|
||||||
if assert.Error(suite.T(), err) {
|
suite.EqualError(err, fmt.Sprintf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", withSpaces))
|
||||||
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)
|
err = validate.Username(weirdChars)
|
||||||
if assert.Error(suite.T(), err) {
|
suite.EqualError(err, fmt.Sprintf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", weirdChars))
|
||||||
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)
|
err = validate.Username(leadingSpace)
|
||||||
if assert.Error(suite.T(), err) {
|
suite.EqualError(err, fmt.Sprintf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", leadingSpace))
|
||||||
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)
|
err = validate.Username(trailingSpace)
|
||||||
if assert.Error(suite.T(), err) {
|
suite.EqualError(err, fmt.Sprintf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", trailingSpace))
|
||||||
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)
|
err = validate.Username(newlines)
|
||||||
if assert.Error(suite.T(), err) {
|
suite.EqualError(err, fmt.Sprintf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max 64 characters", newlines))
|
||||||
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)
|
err = validate.Username(goodUsername)
|
||||||
if assert.NoError(suite.T(), err) {
|
suite.NoError(err)
|
||||||
assert.Equal(suite.T(), nil, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ValidationTestSuite) TestValidateEmail() {
|
func (suite *ValidationTestSuite) TestValidateEmail() {
|
||||||
|
|
|
@ -617,6 +617,43 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
|
||||||
SuspensionOrigin: "",
|
SuspensionOrigin: "",
|
||||||
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R",
|
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R",
|
||||||
},
|
},
|
||||||
|
"remote_account_4": {
|
||||||
|
ID: "07GZRBAEMBNKGZ8Z9VSKSXKR98",
|
||||||
|
Username: "üser",
|
||||||
|
Domain: "xn--xample-ova.org",
|
||||||
|
DisplayName: "",
|
||||||
|
Note: "",
|
||||||
|
Memorial: FalseBool(),
|
||||||
|
MovedToAccountID: "",
|
||||||
|
CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
|
||||||
|
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||||
|
Bot: FalseBool(),
|
||||||
|
Locked: FalseBool(),
|
||||||
|
Discoverable: FalseBool(),
|
||||||
|
Sensitive: FalseBool(),
|
||||||
|
Language: "de",
|
||||||
|
URI: "https://xn--xample-ova.org/users/%C3%BCser",
|
||||||
|
URL: "https://xn--xample-ova.org/users/@%C3%BCser",
|
||||||
|
FetchedAt: time.Time{},
|
||||||
|
InboxURI: "https://xn--xample-ova.org/users/%C3%BCser/inbox",
|
||||||
|
SharedInboxURI: StringPtr(""),
|
||||||
|
OutboxURI: "https://xn--xample-ova.org/users/%C3%BCser/outbox",
|
||||||
|
FollowersURI: "https://xn--xample-ova.org/users/%C3%BCser/followers",
|
||||||
|
FollowingURI: "https://xn--xample-ova.org/users/%C3%BCser/following",
|
||||||
|
FeaturedCollectionURI: "https://xn--xample-ova.org/users/%C3%BCser/collections/featured",
|
||||||
|
ActorType: ap.ActorPerson,
|
||||||
|
AlsoKnownAs: "",
|
||||||
|
PrivateKey: &rsa.PrivateKey{},
|
||||||
|
PublicKey: &rsa.PublicKey{},
|
||||||
|
PublicKeyURI: "https://xn--xample-ova.org/users/%C3%BCser#main-key",
|
||||||
|
SensitizedAt: time.Time{},
|
||||||
|
SilencedAt: time.Time{},
|
||||||
|
SuspendedAt: time.Time{},
|
||||||
|
HideCollections: FalseBool(),
|
||||||
|
SuspensionOrigin: "",
|
||||||
|
HeaderMediaAttachmentID: "",
|
||||||
|
EnableRSS: FalseBool(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var accountsSorted []*gtsmodel.Account
|
var accountsSorted []*gtsmodel.Account
|
||||||
|
@ -629,6 +666,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
|
||||||
})
|
})
|
||||||
|
|
||||||
preserializedKeys := []string{
|
preserializedKeys := []string{
|
||||||
|
"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",
|
||||||
|
|
Loading…
Reference in a new issue