summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/ap/extract.go11
-rw-r--r--internal/api/activitypub/users/userget_test.go4
-rw-r--r--internal/federation/dereferencing/account.go39
-rw-r--r--internal/federation/dereferencing/finger.go18
-rw-r--r--internal/typeutils/astointernal.go36
-rw-r--r--internal/typeutils/astointernal_test.go66
-rw-r--r--internal/util/punycode.go51
7 files changed, 148 insertions, 77 deletions
diff --git a/internal/ap/extract.go b/internal/ap/extract.go
index ce1e2d421..f69fb299e 100644
--- a/internal/ap/extract.go
+++ b/internal/ap/extract.go
@@ -195,17 +195,12 @@ func ExtractPollOptionables(arr []TypeOrIRI) ([]PollOptionable, []TypeOrIRI) {
// ExtractPreferredUsername returns a string representation of
// an interface's preferredUsername property. Will return an
// error if preferredUsername is nil, not a string, or empty.
-func ExtractPreferredUsername(i WithPreferredUsername) (string, error) {
+func ExtractPreferredUsername(i WithPreferredUsername) string {
u := i.GetActivityStreamsPreferredUsername()
if u == nil || !u.IsXMLSchemaString() {
- return "", gtserror.New("preferredUsername nil or not a string")
- }
-
- if u.GetXMLSchemaString() == "" {
- return "", gtserror.New("preferredUsername was empty")
+ return ""
}
-
- return u.GetXMLSchemaString(), nil
+ return u.GetXMLSchemaString()
}
// ExtractName returns the first string representation it
diff --git a/internal/api/activitypub/users/userget_test.go b/internal/api/activitypub/users/userget_test.go
index 2a68b309c..504b69806 100644
--- a/internal/api/activitypub/users/userget_test.go
+++ b/internal/api/activitypub/users/userget_test.go
@@ -86,7 +86,7 @@ func (suite *UserGetTestSuite) TestGetUser() {
suite.True(ok)
// convert person to account
- a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "")
+ a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "", "")
suite.NoError(err)
suite.EqualValues(targetAccount.Username, a.Username)
}
@@ -154,7 +154,7 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
suite.True(ok)
// convert person to account
- a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "")
+ a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "", "")
suite.NoError(err)
suite.EqualValues(targetAccount.Username, a.Username)
}
diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go
index 3abf628d1..b0118380d 100644
--- a/internal/federation/dereferencing/account.go
+++ b/internal/federation/dereferencing/account.go
@@ -18,6 +18,7 @@
package dereferencing
import (
+ "cmp"
"context"
"errors"
"net/url"
@@ -509,10 +510,16 @@ func (d *Dereferencer) enrichAccount(
}
if account.Username != "" {
- // A username was provided so we can attempt a webfinger, this ensures up-to-date accountdomain info.
- accDomain, accURI, err := d.fingerRemoteAccount(ctx, tsport, account.Username, account.Domain)
- switch {
+ // A username was provided so we can attempt to webfinger,
+ // this ensures up-to-date account domain, and handles some
+ // edge cases where servers don't provide a preferred_username.
+ accUsername, accDomain, accURI, err := d.fingerRemoteAccount(ctx,
+ tsport,
+ account.Username,
+ account.Domain,
+ )
+ switch {
case err != nil && account.URI == "":
// This is a new account (to us) with username@domain
// but failed webfinger, nothing more we can do.
@@ -554,6 +561,9 @@ func (d *Dereferencer) enrichAccount(
account.URI = accURI.String()
account.Domain = accDomain
uri = accURI
+
+ // Specifically only update username if not already set.
+ account.Username = cmp.Or(account.Username, accUsername)
}
}
@@ -609,7 +619,7 @@ func (d *Dereferencer) enrichAccount(
if err != nil {
// ResolveAccountable will set gtserror.WrongType
// on the returned error, so we don't need to do it here.
- err = gtserror.Newf("error resolving accountable %s: %w", uri, err)
+ err := gtserror.Newf("error resolving accountable %s: %w", uri, err)
return nil, nil, err
}
@@ -656,15 +666,18 @@ func (d *Dereferencer) enrichAccount(
latestAcc, err := d.converter.ASRepresentationToAccount(ctx,
apubAcc,
account.Domain,
+ account.Username,
)
if err != nil {
// ASRepresentationToAccount will set Malformed on the
// returned error, so we don't need to do it here.
- err = gtserror.Newf("error converting %s to gts model: %w", uri, err)
+ err := gtserror.Newf("error converting %s to gts model: %w", uri, err)
return nil, nil, err
}
if account.Username == "" {
+ var accUsername string
+
// Assume the host from the
// ActivityPub representation.
id := ap.GetJSONLDId(apubAcc)
@@ -685,7 +698,7 @@ func (d *Dereferencer) enrichAccount(
// https://example.org/@someone@somewhere.else and we've been redirected
// from example.org to somewhere.else: we want to take somewhere.else
// as the accountDomain then, not the example.org we were redirected from.
- latestAcc.Domain, _, err = d.fingerRemoteAccount(ctx,
+ accUsername, latestAcc.Domain, _, err = d.fingerRemoteAccount(ctx,
tsport,
latestAcc.Username,
accHost,
@@ -698,6 +711,9 @@ func (d *Dereferencer) enrichAccount(
latestAcc.Username, accHost, err,
)
}
+
+ // Specifically only update username if not already set.
+ latestAcc.Username = cmp.Or(latestAcc.Username, accUsername)
}
if latestAcc.Domain == "" {
@@ -706,23 +722,20 @@ func (d *Dereferencer) enrichAccount(
return nil, nil, gtserror.Newf("empty domain for %s", uri)
}
- // Ensure the final parsed account URI or URL matches
+ // Ensure the final parsed account URI matches
// the input URI we fetched (or received) it as.
- matches, err := util.URIMatches(
+ if matches, err := util.URIMatches(
uri,
append(
ap.GetURL(apubAcc), // account URL(s)
ap.GetJSONLDId(apubAcc), // account URI
)...,
- )
- if err != nil {
+ ); err != nil {
return nil, nil, gtserror.Newf(
"error checking dereferenced account uri %s: %w",
latestAcc.URI, err,
)
- }
-
- if !matches {
+ } else if !matches {
return nil, nil, gtserror.Newf(
"dereferenced account uri %s does not match %s",
latestAcc.URI, uri.String(),
diff --git a/internal/federation/dereferencing/finger.go b/internal/federation/dereferencing/finger.go
index 1b3e915ba..d11950a1e 100644
--- a/internal/federation/dereferencing/finger.go
+++ b/internal/federation/dereferencing/finger.go
@@ -45,6 +45,7 @@ func (d *Dereferencer) fingerRemoteAccount(
username string,
host string,
) (
+ string, // discovered username
string, // discovered account domain
*url.URL, // discovered account URI
error,
@@ -55,31 +56,30 @@ func (d *Dereferencer) fingerRemoteAccount(
b, err := transport.Finger(ctx, username, host)
if err != nil {
err = gtserror.Newf("error webfingering %s: %w", target, err)
- return "", nil, err
+ return "", "", nil, err
}
var resp apimodel.WellKnownResponse
if err := json.Unmarshal(b, &resp); err != nil {
err = gtserror.Newf("error parsing response as JSON for %s: %w", target, err)
- return "", nil, err
+ return "", "", nil, err
}
if len(resp.Links) == 0 {
err = gtserror.Newf("no links found in response for %s", target)
- return "", nil, err
+ return "", "", nil, err
}
if resp.Subject == "" {
err = gtserror.Newf("no subject found in response for %s", target)
- return "", nil, err
+ return "", "", nil, err
}
accUsername, accDomain, err := util.ExtractWebfingerParts(resp.Subject)
if err != nil {
- err = gtserror.Newf("error extracting subject parts for %s: %w", target, err)
- return "", nil, err
+ return "", "", nil, gtserror.Newf("error extracting subject parts for %s: %w", target, err)
} else if accUsername != username {
- return "", nil, gtserror.Newf("response username does not match input for %s: %w", target, err)
+ return "", "", nil, gtserror.Newf("response username does not match input for %s: %w", target, err)
}
// Look through links for the first
@@ -122,8 +122,8 @@ func (d *Dereferencer) fingerRemoteAccount(
}
// All looks good, return happily!
- return accDomain, uri, nil
+ return accUsername, accDomain, uri, nil
}
- return "", nil, gtserror.Newf("no suitable self, AP-type link found in webfinger response for %s", target)
+ return "", "", nil, gtserror.Newf("no suitable self, AP-type link found in webfinger response for %s", target)
}
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index 2946c8d09..5ff60b09c 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -18,6 +18,7 @@
package typeutils
import (
+ "cmp"
"context"
"errors"
"net/url"
@@ -33,10 +34,24 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-// ASRepresentationToAccount converts a remote account/person/application representation into a gts model account.
+// ASRepresentationToAccount converts a remote account / person
+// / application representation into a gts model account.
//
-// If accountDomain is provided then this value will be used as the account's Domain, else the AP ID host.
-func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable ap.Accountable, accountDomain string) (*gtsmodel.Account, error) {
+// If accountDomain is provided then this value will be
+// used as the account's Domain, else the AP ID host.
+//
+// If accountUsername is provided then this is used as
+// a fallback when no preferredUsername is provided. Else
+// a lack of username will result in error return.
+func (c *Converter) ASRepresentationToAccount(
+ ctx context.Context,
+ accountable ap.Accountable,
+ accountDomain string,
+ accountUsername string,
+) (
+ *gtsmodel.Account,
+ error,
+) {
var err error
// Extract URI from accountable
@@ -70,10 +85,17 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a
return nil, gtserror.SetMalformed(err)
}
- // Extract preferredUsername, this is a *requirement*.
- acct.Username, err = ap.ExtractPreferredUsername(accountable)
- if err != nil {
- err := gtserror.Newf("unusable username for %s", uri)
+ // Set account username.
+ acct.Username = cmp.Or(
+
+ // Prefer the AP model provided username.
+ ap.ExtractPreferredUsername(accountable),
+
+ // Fallback username.
+ accountUsername,
+ )
+ if acct.Username == "" {
+ err := gtserror.Newf("missing username for %s", uri)
return nil, gtserror.SetMalformed(err)
}
diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go
index 3efc15999..998a3e974 100644
--- a/internal/typeutils/astointernal_test.go
+++ b/internal/typeutils/astointernal_test.go
@@ -65,7 +65,7 @@ func (suite *ASToInternalTestSuite) jsonToType(in string) vocab.Type {
func (suite *ASToInternalTestSuite) TestParsePerson() {
testPerson := suite.testPeople["https://unknown-instance.com/users/brand_new_person"]
- acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), testPerson, "")
+ acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), testPerson, "", "")
suite.NoError(err)
suite.Equal("https://unknown-instance.com/users/brand_new_person", acct.URI)
@@ -87,7 +87,7 @@ func (suite *ASToInternalTestSuite) TestParsePerson() {
func (suite *ASToInternalTestSuite) TestParsePersonWithSharedInbox() {
testPerson := suite.testPeople["https://turnip.farm/users/turniplover6969"]
- acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), testPerson, "")
+ acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), testPerson, "", "")
suite.NoError(err)
suite.Equal("https://turnip.farm/users/turniplover6969", acct.URI)
@@ -145,7 +145,7 @@ func (suite *ASToInternalTestSuite) TestParseGargron() {
suite.FailNow("type not coercible")
}
- acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "")
+ acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", "")
suite.NoError(err)
suite.Equal("https://mastodon.social/inbox", *acct.SharedInboxURI)
suite.Equal([]string{"https://tooting.ai/users/Gargron"}, acct.AlsoKnownAsURIs)
@@ -196,7 +196,7 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() {
suite.FailNow("type not coercible")
}
- acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "")
+ acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", "")
suite.NoError(err)
suite.Equal("rgh", acct.Username)
@@ -547,7 +547,7 @@ func (suite *ASToInternalTestSuite) TestParseHonkAccount() {
suite.FailNow("type not coercible")
}
- acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "")
+ acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "", "")
suite.NoError(err)
suite.Equal("https://honk.example.org/u/honk_user/followers", acct.FollowersURI)
suite.Equal("https://honk.example.org/u/honk_user/following", acct.FollowingURI)
@@ -651,6 +651,62 @@ func (suite *ASToInternalTestSuite) TestParseHonkAccount() {
suite.False(*dbAcct.Discoverable)
}
+func (suite *ASToInternalTestSuite) TestParseAccountableWithoutPreferredUsername() {
+ ctx, cncl := context.WithCancel(context.Background())
+ defer cncl()
+
+ testPerson := suite.testPeople["https://unknown-instance.com/users/brand_new_person"]
+ // preferredUsername := "newish_person_actually"
+ username := "brand_new_person"
+
+ // Specifically unset the preferred_username field.
+ testPerson.SetActivityStreamsPreferredUsername(nil)
+
+ // Attempt to parse account model from ActivityStreams.
+ // This should fall back to the passed username argument as no preferred_username is set.
+ acc, err := suite.typeconverter.ASRepresentationToAccount(ctx, testPerson, "", username)
+ suite.NoError(err)
+ suite.Equal(acc.Username, username)
+}
+
+func (suite *ASToInternalTestSuite) TestParseAccountableWithoutAnyUsername() {
+ ctx, cncl := context.WithCancel(context.Background())
+ defer cncl()
+
+ testPerson := suite.testPeople["https://unknown-instance.com/users/brand_new_person"]
+ // preferredUsername := "newish_person_actually"
+ // username := "brand_new_person"
+
+ // Specifically unset the preferred_username field.
+ testPerson.SetActivityStreamsPreferredUsername(nil)
+
+ // Attempt to parse account model from ActivityStreams.
+ // This should return error as we provide no username and no preferred_username is set.
+ acc, err := suite.typeconverter.ASRepresentationToAccount(ctx, testPerson, "", "")
+ suite.Equal(err.Error(), "ASRepresentationToAccount: missing username for https://unknown-instance.com/users/brand_new_person")
+ suite.Nil(acc)
+}
+
+func (suite *ASToInternalTestSuite) TestParseAccountableWithPreferredUsername() {
+ ctx, cncl := context.WithCancel(context.Background())
+ defer cncl()
+
+ testPerson := suite.testPeople["https://unknown-instance.com/users/brand_new_person"]
+ preferredUsername := "newish_person_actually"
+ username := "brand_new_person"
+
+ // Specifically set a known preferred_username field.
+ prop := streams.NewActivityStreamsPreferredUsernameProperty()
+ prop.SetXMLSchemaString(preferredUsername)
+ testPerson.SetActivityStreamsPreferredUsername(prop)
+
+ // Attempt to parse account model from ActivityStreams.
+ // This should use the ActivityStreams preferred_username, instead of the passed argument.
+ acc, err := suite.typeconverter.ASRepresentationToAccount(ctx, testPerson, "", username)
+ suite.NoError(err)
+ suite.Equal(acc.Username, preferredUsername)
+}
+
func TestASToInternalTestSuite(t *testing.T) {
suite.Run(t, new(ASToInternalTestSuite))
}
diff --git a/internal/util/punycode.go b/internal/util/punycode.go
index cc1b57a27..3e9f71408 100644
--- a/internal/util/punycode.go
+++ b/internal/util/punycode.go
@@ -48,19 +48,18 @@ func DePunify(domain string) (string, error) {
// any of the given URIs, taking account of punycode.
func URIMatches(expect *url.URL, uris ...*url.URL) (bool, error) {
// Normalize expect to punycode.
- expectPuny, err := PunifyURI(expect)
+ expectStr, err := PunifyURIToStr(expect)
if err != nil {
return false, err
}
- expectStr := expectPuny.String()
for _, uri := range uris {
- uriPuny, err := PunifyURI(uri)
+ uriStr, err := PunifyURIToStr(uri)
if err != nil {
return false, err
}
- if uriPuny.String() == expectStr {
+ if uriStr == expectStr {
// Looks good.
return true, nil
}
@@ -73,40 +72,26 @@ func URIMatches(expect *url.URL, uris ...*url.URL) (bool, error) {
// PunifyURI returns a copy of the given URI
// with the 'host' part converted to punycode.
func PunifyURI(in *url.URL) (*url.URL, error) {
- // Take a copy of in.
+ punyHost, err := Punify(in.Host)
+ if err != nil {
+ return nil, err
+ }
out := new(url.URL)
*out = *in
-
- // Normalize host to punycode.
- var err error
- out.Host, err = Punify(in.Host)
- return out, err
+ out.Host = punyHost
+ return out, nil
}
-// PunifyURIStr returns a copy of the given URI
-// string with the 'host' part converted to punycode.
-func PunifyURIStr(in string) (string, error) {
- inURI, err := url.Parse(in)
- if err != nil {
- return "", err
- }
-
- outURIPuny, err := Punify(inURI.Host)
+// PunifyURIToStr returns given URI serialized
+// with the 'host' part converted to punycode.
+func PunifyURIToStr(in *url.URL) (string, error) {
+ punyHost, err := Punify(in.Host)
if err != nil {
return "", err
}
-
- if outURIPuny == in {
- // Punify did nothing, so in was
- // already punified, return as-is.
- return in, nil
- }
-
- // Take a copy of in.
- outURI := new(url.URL)
- *outURI = *inURI
-
- // Normalize host to punycode.
- outURI.Host = outURIPuny
- return outURI.String(), err
+ oldHost := in.Host
+ in.Host = punyHost
+ str := in.String()
+ in.Host = oldHost
+ return str, nil
}