mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat(activitypub): use structure @PreferredUsername@host.tld:port for actors (#9254)
This modifies usernames of ActivityPub accounts to use the @example@example.tld format with an additional optional port component (e.g. @user@example.tld:42). This allows accounts from ActivityPub servers with more relaxed username requirements than those of Forgejo's to interact with Forgejo. Forgejo would also follow a "de facto" standard of ActivityPub implementations. By separating different information using @'s, we also gain future opportunities to store more information about ActivityPub accounts internally, so that we won't have to rely on e.g. the amount of dashes in a username as my migration currently does. Continuation of Aravinth's work: https://codeberg.org/forgejo/forgejo/pulls/4778 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9254 Reviewed-by: jerger <jerger@noreply.codeberg.org> Reviewed-by: Ellen Εμιλία Άννα Zscheile <fogti@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Panagiotis "Ivory" Vasilopoulos <git@n0toose.net> Co-committed-by: Panagiotis "Ivory" Vasilopoulos <git@n0toose.net>
This commit is contained in:
parent
90b3352ed5
commit
81601eab85
13 changed files with 422 additions and 19 deletions
|
|
@ -71,12 +71,13 @@ func (id PersonID) AsLoginName() string {
|
|||
return result
|
||||
}
|
||||
|
||||
// HostSuffix returns the host part of a handle, i.e. @host.tld (if port is supplemented) or @host.tld:1234
|
||||
func (id PersonID) HostSuffix() string {
|
||||
var result string
|
||||
if !id.IsPortSupplemented {
|
||||
result = fmt.Sprintf("-%s-%d", strings.ToLower(id.Host), id.HostPort)
|
||||
result = fmt.Sprintf("@%s:%d", strings.ToLower(id.Host), id.HostPort)
|
||||
} else {
|
||||
result = fmt.Sprintf("-%s", strings.ToLower(id.Host))
|
||||
result = fmt.Sprintf("@%s", strings.ToLower(id.Host))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,8 +246,19 @@ func TestForgePersonValidation(t *testing.T) {
|
|||
|
||||
func TestAsloginName(t *testing.T) {
|
||||
sut, _ := forgefed.NewPersonID("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo")
|
||||
assert.Equal(t, "12345-codeberg.org", sut.AsLoginName())
|
||||
assert.Equal(t, "12345@codeberg.org", sut.AsLoginName())
|
||||
|
||||
sut, _ = forgefed.NewPersonID("https://codeberg.org:443/api/v1/activitypub/user-id/12345", "forgejo")
|
||||
assert.Equal(t, "12345-codeberg.org-443", sut.AsLoginName())
|
||||
assert.Equal(t, "12345@codeberg.org:443", sut.AsLoginName())
|
||||
}
|
||||
|
||||
func TestHostSuffix(t *testing.T) {
|
||||
sut, _ := forgefed.NewPersonID("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo")
|
||||
sut.Host = "forgejo.example.tld"
|
||||
sut.HostPort = 80
|
||||
|
||||
// sut.IsPortSupplemented is true by default at time of writing.
|
||||
assert.Equal(t, "@forgejo.example.tld", sut.HostSuffix())
|
||||
sut.IsPortSupplemented = false
|
||||
assert.Equal(t, "@forgejo.example.tld:80", sut.HostSuffix())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,21 @@ var (
|
|||
|
||||
// No consecutive or trailing non-alphanumeric chars, catches both cases
|
||||
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`)
|
||||
|
||||
// This is intended to accept any character, in any language, with accent symbols,
|
||||
// as well as an arbitrary amount of subdomains and an optional port number defined
|
||||
// through `:12345`.
|
||||
//
|
||||
// This is intended to cover username cases from distant servers in the fediverse, which
|
||||
// can have much laxer requirements than those of Forgejo. It is not intended to check for
|
||||
// invalid, non-standard compliant domains.
|
||||
//
|
||||
// For instance, the following should work:
|
||||
// @user.όνομαß_21__@subdomain1.subdomain2.example.tld:65536
|
||||
// @42@42.example.tld
|
||||
// @user@example.tld:99999 (presumed to be an impossible case)
|
||||
// @-@-.tld (also impossible)
|
||||
validFediverseUsernamePattern = regexp.MustCompile(`^(@[\p{L}\p{M}0-9_\.\-]{1,})(@[\p{L}\p{M}0-9_\.\-]{1,})(:[1-9][0-9]{0,4})?$`)
|
||||
)
|
||||
|
||||
// IsValidUsername checks if username is valid
|
||||
|
|
@ -114,3 +129,11 @@ func IsValidUsername(name string) bool {
|
|||
|
||||
return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||
}
|
||||
|
||||
// IsValidActivityPubUsername checks whether the username can be a valid ActivityPub handle.
|
||||
//
|
||||
// Username refers to the Forgejo user account's username for consistency, and not
|
||||
// e.g. "username" in @username@example.tld.
|
||||
func IsValidActivityPubUsername(name string) bool {
|
||||
return validFediverseUsernamePattern.MatchString(name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package validation
|
||||
|
|
@ -213,3 +214,109 @@ func TestIsValidUsernameBanDots(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidActivityPubUsername(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
username string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
description: "Username without domain",
|
||||
username: "@user",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
description: "Username with domain",
|
||||
username: "@user@example.tld",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Numeric username with subdomain",
|
||||
username: "@42@42.example.tld",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Username with two subdomains",
|
||||
username: "@user@forgejo.activitypub.example.tld",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Username with domain and without port",
|
||||
username: "@user@social.example.tld:",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
description: "Username with domain and invalid port 0",
|
||||
username: "@user@social.example.tld:0",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
// We do not validate the port and assume that federationHost.HostPort
|
||||
// cannot present such invalid ports. That also makes the previous case
|
||||
// (port: 0) redundant, but it doesn't hurt.
|
||||
description: "Username with domain and valid port",
|
||||
username: "@user@social.example.tld:65536",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Username with Latin letters and special symbols",
|
||||
username: "@$username$@example.tld",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
description: "Strictly numeric handle, domain, TLD",
|
||||
username: "@0123456789@0123456789.0123456789.123",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Handle with Latin characters and dashes",
|
||||
username: "@0-O@O-O.tld",
|
||||
valid: true,
|
||||
},
|
||||
// This is an impossible case, but we assume that this will never happen
|
||||
// to begin with.
|
||||
{
|
||||
description: "Handle that only has dashes",
|
||||
username: "@-@-.-",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Username with a mix of Latin and non-Latin letters containing accents",
|
||||
username: "@usernäme.όνομαß_21__@example.tld",
|
||||
valid: true,
|
||||
},
|
||||
// Note: Our regex should accept any character, in any language and with accent symbols.
|
||||
// The list is neither exhaustive, nor does it represent all possible cases.
|
||||
// I chose some TLDs from https://en.wikipedia.org/wiki/Country_code_top-level_domain,
|
||||
// although only one test case should suffice in theory. Nevertheless, to play it safe,
|
||||
// I included four from different geographic regions whose scripts were legible using my
|
||||
// IDE's default font to play it safe.
|
||||
{
|
||||
description: "Username, domain and ccTLD in Greek",
|
||||
username: "@ευ@ευ.ευ",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Username, domain and ccTLD in Georgian (Mkhedruli)",
|
||||
username: "@გე@გე.გე",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Username, domain and ccTLD of Malaysia (Arabic Jawi)",
|
||||
username: "@مليسيا@ລمليسيا.مليسيا",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
description: "Username, domain and ccTLD of China (Simplified)",
|
||||
username: "@中国@中国.中国",
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.valid, IsValidActivityPubUsername(testCase.username))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue