diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go
index 4f8e76190..4c23ba27b 100644
--- a/internal/api/activitypub/users/inboxpost_test.go
+++ b/internal/api/activitypub/users/inboxpost_test.go
@@ -376,10 +376,9 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.EqualValues(requestingAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment)
suite.EqualValues(requestingAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL)
suite.EqualValues(requestingAccount.Note, dbUpdatedAccount.Note)
- suite.EqualValues(requestingAccount.Memorial, dbUpdatedAccount.Memorial)
+ suite.EqualValues(requestingAccount.MemorializedAt, dbUpdatedAccount.MemorializedAt)
suite.EqualValues(requestingAccount.AlsoKnownAsURIs, dbUpdatedAccount.AlsoKnownAsURIs)
suite.EqualValues(requestingAccount.MovedToURI, dbUpdatedAccount.MovedToURI)
- suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot)
suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked)
suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable)
suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI)
diff --git a/internal/api/client/accounts/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go
index df5c21389..eaa22abcf 100644
--- a/internal/api/client/accounts/accountverify_test.go
+++ b/internal/api/client/accounts/accountverify_test.go
@@ -88,7 +88,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
suite.Equal(testAccount.Username, apimodelAccount.Acct)
suite.Equal(testAccount.DisplayName, apimodelAccount.DisplayName)
suite.Equal(*testAccount.Locked, apimodelAccount.Locked)
- suite.Equal(*testAccount.Bot, apimodelAccount.Bot)
+ suite.False(apimodelAccount.Bot)
suite.WithinDuration(testAccount.CreatedAt, createdAt, 30*time.Second) // we lose a bit of accuracy serializing so fuzz this a bit
suite.Equal(testAccount.URL, apimodelAccount.URL)
suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", apimodelAccount.Avatar)
diff --git a/internal/api/client/admin/accountsgetv2_test.go b/internal/api/client/admin/accountsgetv2_test.go
index 339c97431..2a2c89780 100644
--- a/internal/api/client/admin/accountsgetv2_test.go
+++ b/internal/api/client/admin/accountsgetv2_test.go
@@ -204,7 +204,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
"display_name": "",
"locked": false,
"discoverable": true,
- "bot": false,
+ "bot": true,
"created_at": "2020-05-17T13:10:59.000Z",
"note": "",
"url": "http://localhost:8080/@localhost:8080",
diff --git a/internal/api/client/search/searchget_test.go b/internal/api/client/search/searchget_test.go
index 53f0a993c..7d5b73572 100644
--- a/internal/api/client/search/searchget_test.go
+++ b/internal/api/client/search/searchget_test.go
@@ -31,7 +31,6 @@ import (
"testing"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api/client/search"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
@@ -1402,7 +1401,7 @@ func (suite *SearchGetTestSuite) TestSearchRemoteInstanceAccountPartial() {
FollowersURI: "http://" + theirDomain + "/users/" + theirDomain + "/followers",
FollowingURI: "http://" + theirDomain + "/users/" + theirDomain + "/following",
FeaturedCollectionURI: "http://" + theirDomain + "/users/" + theirDomain + "/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: key,
PublicKey: &key.PublicKey,
}); err != nil {
diff --git a/internal/api/wellknown/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go
index 1707584a5..94c084146 100644
--- a/internal/api/wellknown/webfinger/webfingerget_test.go
+++ b/internal/api/wellknown/webfinger/webfingerget_test.go
@@ -30,7 +30,6 @@ import (
"testing"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
@@ -124,7 +123,7 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom
FollowingURI: "http://" + host + "/users/new_account_domain_user/following",
FollowersURI: "http://" + host + "/users/new_account_domain_user/followers",
FeaturedCollectionURI: "http://" + host + "/users/new_account_domain_user/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: privateKey,
PublicKey: publicKey,
PublicKeyURI: "http://" + host + "/users/new_account_domain_user/main-key",
diff --git a/internal/cache/db.go b/internal/cache/db.go
index 695b19b8f..82cd9ac5f 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -306,13 +306,8 @@ func (c *Caches) initAccount() {
Indices: []structr.IndexConfig{
{Fields: "ID"},
{Fields: "URI"},
- {Fields: "URL"},
- {Fields: "Username,Domain", AllowZero: true},
{Fields: "PublicKeyURI"},
- {Fields: "InboxURI"},
- {Fields: "OutboxURI"},
- {Fields: "FollowersURI"},
- {Fields: "FollowingURI"},
+ {Fields: "Username,Domain", AllowZero: true},
},
MaxSize: cap,
IgnoreErr: ignoreErrors,
diff --git a/internal/cache/size.go b/internal/cache/size.go
index cdaf3a03b..9a30d5f08 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -240,13 +240,12 @@ func sizeofAccount() uintptr {
DisplayName: exampleUsername,
Note: exampleText,
NoteRaw: exampleText,
- Memorial: func() *bool { ok := false; return &ok }(),
+ MemorializedAt: exampleTime,
CreatedAt: exampleTime,
UpdatedAt: exampleTime,
FetchedAt: exampleTime,
- Bot: func() *bool { ok := true; return &ok }(),
- Locked: func() *bool { ok := true; return &ok }(),
- Discoverable: func() *bool { ok := false; return &ok }(),
+ Locked: util.Ptr(true),
+ Discoverable: util.Ptr(false),
URI: exampleURI,
URL: exampleURI,
InboxURI: exampleURI,
@@ -254,7 +253,7 @@ func sizeofAccount() uintptr {
FollowersURI: exampleURI,
FollowingURI: exampleURI,
FeaturedCollectionURI: exampleURI,
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{},
PublicKeyURI: exampleURI,
diff --git a/internal/db/account.go b/internal/db/account.go
index cfb81308f..0caac3453 100644
--- a/internal/db/account.go
+++ b/internal/db/account.go
@@ -27,37 +27,37 @@ import (
// Account contains functions related to account getting/setting/creation.
type Account interface {
- // GetAccountByID returns one account with the given ID, or an error if something goes wrong.
+ // GetAccountByID returns one account with the given ID.
GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, error)
// GetAccountsByIDs returns accounts corresponding to given IDs.
GetAccountsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Account, error)
- // GetAccountByURI returns one account with the given URI, or an error if something goes wrong.
+ // GetAccountByURI returns one account with the given ActivityStreams URI.
GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
- // GetAccountByURL returns one account with the given URL, or an error if something goes wrong.
- GetAccountByURL(ctx context.Context, uri string) (*gtsmodel.Account, error)
+ // GetOneAccountByURL returns *one* account with the given ActivityStreams URL.
+ // If more than one account has the given url, ErrMultipleEntries will be returned.
+ GetOneAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, error)
- // GetAccountByUsernameDomain returns one account with the given username and domain, or an error if something goes wrong.
+ // GetAccountsByURL returns accounts with the given ActivityStreams URL.
+ GetAccountsByURL(ctx context.Context, url string) ([]*gtsmodel.Account, error)
+
+ // GetAccountByUsernameDomain returns one account with the given username and domain.
GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, error)
- // GetAccountByPubkeyID returns one account with the given public key URI (ID), or an error if something goes wrong.
+ // GetAccountByPubkeyID returns one account with the given public key URI (ID).
GetAccountByPubkeyID(ctx context.Context, id string) (*gtsmodel.Account, error)
- // GetAccountByInboxURI returns one account with the given inbox_uri, or an error if something goes wrong.
- GetAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
+ // GetOneAccountByInboxURI returns one account with the given inbox_uri.
+ // If more than one account has the given URL, ErrMultipleEntries will be returned.
+ GetOneAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
- // GetAccountByOutboxURI returns one account with the given outbox_uri, or an error if something goes wrong.
- GetAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
+ // GetOneAccountByOutboxURI returns one account with the given outbox_uri.
+ // If more than one account has the given uri, ErrMultipleEntries will be returned.
+ GetOneAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
- // GetAccountByFollowingURI returns one account with the given following_uri, or an error if something goes wrong.
- GetAccountByFollowingURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
-
- // GetAccountByFollowersURI returns one account with the given followers_uri, or an error if something goes wrong.
- GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
-
- // GetAccountByMovedToURI returns any accounts with given moved_to_uri set.
+ // GetAccountsByMovedToURI returns any accounts with given moved_to_uri set.
GetAccountsByMovedToURI(ctx context.Context, uri string) ([]*gtsmodel.Account, error)
// GetAccounts returns accounts
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index aacfcd247..88a923ecf 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -121,18 +121,46 @@ func (a *accountDB) GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.
)
}
-func (a *accountDB) GetAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, error) {
- return a.getAccount(
- ctx,
- "URL",
- func(account *gtsmodel.Account) error {
- return a.db.NewSelect().
- Model(account).
- Where("? = ?", bun.Ident("account.url"), url).
- Scan(ctx)
- },
- url,
- )
+func (a *accountDB) GetOneAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, error) {
+ // Select IDs of all
+ // accounts with this url.
+ var ids []string
+ if err := a.db.NewSelect().
+ TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
+ Column("account.id").
+ Where("? = ?", bun.Ident("account.url"), url).
+ Scan(ctx, &ids); err != nil {
+ return nil, err
+ }
+
+ // Ensure exactly one account.
+ if len(ids) == 0 {
+ return nil, db.ErrNoEntries
+ }
+ if len(ids) > 1 {
+ return nil, db.ErrMultipleEntries
+ }
+
+ return a.GetAccountByID(ctx, ids[0])
+}
+
+func (a *accountDB) GetAccountsByURL(ctx context.Context, url string) ([]*gtsmodel.Account, error) {
+ // Select IDs of all
+ // accounts with this url.
+ var ids []string
+ if err := a.db.NewSelect().
+ TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
+ Column("account.id").
+ Where("? = ?", bun.Ident("account.url"), url).
+ Scan(ctx, &ids); err != nil {
+ return nil, err
+ }
+
+ if len(ids) == 0 {
+ return nil, db.ErrNoEntries
+ }
+
+ return a.GetAccountsByIDs(ctx, ids)
}
func (a *accountDB) GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, error) {
@@ -184,60 +212,50 @@ func (a *accountDB) GetAccountByPubkeyID(ctx context.Context, id string) (*gtsmo
)
}
-func (a *accountDB) GetAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
- return a.getAccount(
- ctx,
- "InboxURI",
- func(account *gtsmodel.Account) error {
- return a.db.NewSelect().
- Model(account).
- Where("? = ?", bun.Ident("account.inbox_uri"), uri).
- Scan(ctx)
- },
- uri,
- )
+func (a *accountDB) GetOneAccountByInboxURI(ctx context.Context, inboxURI string) (*gtsmodel.Account, error) {
+ // Select IDs of all accounts
+ // with this inbox_uri.
+ var ids []string
+ if err := a.db.NewSelect().
+ TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
+ Column("account.id").
+ Where("? = ?", bun.Ident("account.inbox_uri"), inboxURI).
+ Scan(ctx, &ids); err != nil {
+ return nil, err
+ }
+
+ // Ensure exactly one account.
+ if len(ids) == 0 {
+ return nil, db.ErrNoEntries
+ }
+ if len(ids) > 1 {
+ return nil, db.ErrMultipleEntries
+ }
+
+ return a.GetAccountByID(ctx, ids[0])
}
-func (a *accountDB) GetAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
- return a.getAccount(
- ctx,
- "OutboxURI",
- func(account *gtsmodel.Account) error {
- return a.db.NewSelect().
- Model(account).
- Where("? = ?", bun.Ident("account.outbox_uri"), uri).
- Scan(ctx)
- },
- uri,
- )
-}
+func (a *accountDB) GetOneAccountByOutboxURI(ctx context.Context, outboxURI string) (*gtsmodel.Account, error) {
+ // Select IDs of all accounts
+ // with this outbox_uri.
+ var ids []string
+ if err := a.db.NewSelect().
+ TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
+ Column("account.id").
+ Where("? = ?", bun.Ident("account.outbox_uri"), outboxURI).
+ Scan(ctx, &ids); err != nil {
+ return nil, err
+ }
-func (a *accountDB) GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
- return a.getAccount(
- ctx,
- "FollowersURI",
- func(account *gtsmodel.Account) error {
- return a.db.NewSelect().
- Model(account).
- Where("? = ?", bun.Ident("account.followers_uri"), uri).
- Scan(ctx)
- },
- uri,
- )
-}
+ // Ensure exactly one account.
+ if len(ids) == 0 {
+ return nil, db.ErrNoEntries
+ }
+ if len(ids) > 1 {
+ return nil, db.ErrMultipleEntries
+ }
-func (a *accountDB) GetAccountByFollowingURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
- return a.getAccount(
- ctx,
- "FollowingURI",
- func(account *gtsmodel.Account) error {
- return a.db.NewSelect().
- Model(account).
- Where("? = ?", bun.Ident("account.following_uri"), uri).
- Scan(ctx)
- },
- uri,
- )
+ return a.GetAccountByID(ctx, ids[0])
}
func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gtsmodel.Account, error) {
@@ -587,7 +605,11 @@ func (a *accountDB) GetAccounts(
return a.state.DB.GetAccountsByIDs(ctx, accountIDs)
}
-func (a *accountDB) getAccount(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Account) error, keyParts ...any) (*gtsmodel.Account, error) {
+func (a *accountDB) getAccount(
+ ctx context.Context,
+ lookup string,
+ dbQuery func(*gtsmodel.Account) error, keyParts ...any,
+) (*gtsmodel.Account, error) {
// Fetch account from database cache with loader callback
account, err := a.state.Caches.DB.Account.LoadOne(lookup, func() (*gtsmodel.Account, error) {
var account gtsmodel.Account
diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go
index e3d36855e..ffd44de79 100644
--- a/internal/db/bundb/account_test.go
+++ b/internal/db/bundb/account_test.go
@@ -32,11 +32,10 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/bundb"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/uptrace/bun"
)
type AccountTestSuite struct {
@@ -255,7 +254,20 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
if account.URL == "" {
return nil, sentinelErr
}
- return suite.db.GetAccountByURL(ctx, account.URL)
+ return suite.db.GetOneAccountByURL(ctx, account.URL)
+ },
+
+ "url_multi": func() (*gtsmodel.Account, error) {
+ if account.URL == "" {
+ return nil, sentinelErr
+ }
+
+ accounts, err := suite.db.GetAccountsByURL(ctx, account.URL)
+ if err != nil {
+ return nil, err
+ }
+
+ return accounts[0], nil
},
"username@domain": func() (*gtsmodel.Account, error) {
@@ -281,28 +293,14 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
if account.InboxURI == "" {
return nil, sentinelErr
}
- return suite.db.GetAccountByInboxURI(ctx, account.InboxURI)
+ return suite.db.GetOneAccountByInboxURI(ctx, account.InboxURI)
},
"outbox_uri": func() (*gtsmodel.Account, error) {
if account.OutboxURI == "" {
return nil, sentinelErr
}
- return suite.db.GetAccountByOutboxURI(ctx, account.OutboxURI)
- },
-
- "following_uri": func() (*gtsmodel.Account, error) {
- if account.FollowingURI == "" {
- return nil, sentinelErr
- }
- return suite.db.GetAccountByFollowingURI(ctx, account.FollowingURI)
- },
-
- "followers_uri": func() (*gtsmodel.Account, error) {
- if account.FollowersURI == "" {
- return nil, sentinelErr
- }
- return suite.db.GetAccountByFollowersURI(ctx, account.FollowersURI)
+ return suite.db.GetOneAccountByOutboxURI(ctx, account.OutboxURI)
},
} {
@@ -345,71 +343,37 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
}
}
-func (suite *AccountTestSuite) TestUpdateAccount() {
+func (suite *AccountTestSuite) TestGetAccountsByURLMulti() {
ctx := context.Background()
- testAccount := suite.testAccounts["local_account_1"]
-
- testAccount.DisplayName = "new display name!"
- testAccount.EmojiIDs = []string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}
-
- err := suite.db.UpdateAccount(ctx, testAccount)
- suite.NoError(err)
-
- updated, err := suite.db.GetAccountByID(ctx, testAccount.ID)
- suite.NoError(err)
- suite.Equal("new display name!", updated.DisplayName)
- suite.Equal([]string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}, updated.EmojiIDs)
- suite.WithinDuration(time.Now(), updated.UpdatedAt, 5*time.Second)
-
- // get account without cache + make sure it's really in the db as desired
- dbService, ok := suite.db.(*bundb.DBService)
- if !ok {
- panic("db was not *bundb.DBService")
+ // Update admin account to have the same url as zork.
+ testAccount1 := suite.testAccounts["local_account_1"]
+ testAccount2 := new(gtsmodel.Account)
+ *testAccount2 = *suite.testAccounts["admin_account"]
+ testAccount2.URL = testAccount1.URL
+ if err := suite.state.DB.UpdateAccount(ctx, testAccount2, "url"); err != nil {
+ suite.FailNow(err.Error())
}
- noCache := >smodel.Account{}
- err = dbService.DB().
- NewSelect().
- Model(noCache).
- Where("? = ?", bun.Ident("account.id"), testAccount.ID).
- Relation("AvatarMediaAttachment").
- Relation("HeaderMediaAttachment").
- Relation("Emojis").
- Scan(ctx)
+ // Select all accounts with that URL.
+ // Should return 2.
+ accounts, err := suite.state.DB.GetAccountsByURL(
+ gtscontext.SetBarebones(ctx),
+ testAccount1.URL,
+ )
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.Len(accounts, 2)
- suite.NoError(err)
- suite.Equal("new display name!", noCache.DisplayName)
- suite.Equal([]string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}, noCache.EmojiIDs)
- suite.WithinDuration(time.Now(), noCache.UpdatedAt, 5*time.Second)
- suite.NotNil(noCache.AvatarMediaAttachment)
- suite.NotNil(noCache.HeaderMediaAttachment)
-
- // update again to remove emoji associations
- testAccount.EmojiIDs = []string{}
-
- err = suite.db.UpdateAccount(ctx, testAccount)
- suite.NoError(err)
-
- updated, err = suite.db.GetAccountByID(ctx, testAccount.ID)
- suite.NoError(err)
- suite.Equal("new display name!", updated.DisplayName)
- suite.Empty(updated.EmojiIDs)
- suite.WithinDuration(time.Now(), updated.UpdatedAt, 5*time.Second)
-
- err = dbService.DB().
- NewSelect().
- Model(noCache).
- Where("? = ?", bun.Ident("account.id"), testAccount.ID).
- Relation("AvatarMediaAttachment").
- Relation("HeaderMediaAttachment").
- Relation("Emojis").
- Scan(ctx)
-
- suite.NoError(err)
- suite.Equal("new display name!", noCache.DisplayName)
- suite.Empty(noCache.EmojiIDs)
- suite.WithinDuration(time.Now(), noCache.UpdatedAt, 5*time.Second)
+ // Try to select one account with that URL.
+ // Should error.
+ account, err := suite.state.DB.GetOneAccountByURL(
+ gtscontext.SetBarebones(ctx),
+ testAccount1.URL,
+ )
+ suite.Nil(account)
+ suite.ErrorIs(err, db.ErrMultipleEntries)
}
func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
@@ -422,7 +386,7 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
Domain: "example.org",
URI: "https://example.org/users/test_service",
URL: "https://example.org/@test_service",
- ActorType: ap.ActorService,
+ ActorType: gtsmodel.AccountActorTypeService,
PublicKey: &key.PublicKey,
PublicKeyURI: "https://example.org/users/test_service#main-key",
}
@@ -433,7 +397,6 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
suite.WithinDuration(time.Now(), newAccount.CreatedAt, 30*time.Second)
suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second)
suite.True(*newAccount.Locked)
- suite.False(*newAccount.Bot)
suite.False(*newAccount.Discoverable)
}
diff --git a/internal/db/bundb/admin.go b/internal/db/bundb/admin.go
index 02f10f44f..12cb6a6f7 100644
--- a/internal/db/bundb/admin.go
+++ b/internal/db/bundb/admin.go
@@ -28,7 +28,6 @@ import (
"time"
"github.com/google/uuid"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
@@ -131,7 +130,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
FollowingURI: uris.FollowingURI,
FollowersURI: uris.FollowersURI,
FeaturedCollectionURI: uris.FeaturedCollectionURI,
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: privKey,
PublicKey: &privKey.PublicKey,
PublicKeyURI: uris.PublicKeyURI,
@@ -283,7 +282,7 @@ func (a *adminDB) CreateInstanceAccount(ctx context.Context) error {
PrivateKey: key,
PublicKey: &key.PublicKey,
PublicKeyURI: newAccountURIs.PublicKeyURI,
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypeService,
URI: newAccountURIs.UserURI,
InboxURI: newAccountURIs.InboxURI,
OutboxURI: newAccountURIs.OutboxURI,
diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go
index 1f2d1ac48..4c5ea8d18 100644
--- a/internal/db/bundb/basic_test.go
+++ b/internal/db/bundb/basic_test.go
@@ -55,7 +55,7 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
URL: "https://example.org/@test",
InboxURI: "https://example.org/users/test/inbox",
OutboxURI: "https://example.org/users/test/outbox",
- ActorType: "Person",
+ ActorType: gtsmodel.AccountActorTypePerson,
PublicKeyURI: "https://example.org/test#main-key",
PublicKey: &key.PublicKey,
}
@@ -87,7 +87,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
suite.Empty(a.NoteRaw)
suite.Empty(a.AlsoKnownAsURIs)
suite.Empty(a.MovedToURI)
- suite.False(*a.Bot)
// Locked is especially important, since it's a bool that defaults
// to true, which is why we use pointers for bools in the first place
suite.True(*a.Locked)
diff --git a/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness.go b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness.go
new file mode 100644
index 000000000..acc37529d
--- /dev/null
+++ b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness.go
@@ -0,0 +1,398 @@
+// 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 .
+
+package migrations
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/new"
+ old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/old"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+
+ "github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect"
+)
+
+func init() {
+ up := func(ctx context.Context, bdb *bun.DB) error {
+ log.Info(ctx, "converting accounts to new model; this may take a while, please don't interrupt!")
+
+ return bdb.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+
+ var (
+ // We have to use different
+ // syntax for this query
+ // depending on dialect.
+ dbDialect = tx.Dialect().Name()
+
+ // ID for paging.
+ maxID string
+
+ // Batch size for
+ // selecting + updating.
+ batchsz = 100
+
+ // Number of accounts
+ // updated so far.
+ updated int
+
+ // We need to know our own host
+ // for updating instance account.
+ host = config.GetHost()
+ )
+
+ // Create the new accounts table.
+ if _, err := tx.
+ NewCreateTable().
+ ModelTableExpr("new_accounts").
+ Model(&new_gtsmodel.Account{}).
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Count number of accounts
+ // we need to update.
+ total, err := tx.
+ NewSelect().
+ Table("accounts").
+ Count(ctx)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return err
+ }
+
+ // Create a subquery for
+ // Postgres to reuse.
+ var orderQPG *bun.RawQuery
+ if dbDialect == dialect.PG {
+ orderQPG = tx.NewRaw(
+ "(COALESCE(?, ?) || ? || ?) COLLATE ?",
+ bun.Ident("domain"), "",
+ "/@",
+ bun.Ident("username"),
+ bun.Ident("C"),
+ )
+ }
+
+ var orderQSqlite *bun.RawQuery
+ if dbDialect == dialect.SQLite {
+ orderQSqlite = tx.NewRaw(
+ "(COALESCE(?, ?) || ? || ?)",
+ bun.Ident("domain"), "",
+ "/@",
+ bun.Ident("username"),
+ )
+ }
+
+ for {
+ // Batch of old model account IDs to select.
+ oldAccountIDs := make([]string, 0, batchsz)
+
+ // Start building IDs query.
+ idsQ := tx.
+ NewSelect().
+ Table("accounts").
+ Column("id").
+ Limit(batchsz)
+
+ if dbDialect == dialect.SQLite {
+ // For SQLite we can just select
+ // our indexed expression once
+ // as a column alias.
+ idsQ = idsQ.
+ ColumnExpr(
+ "(COALESCE(?, ?) || ? || ?) AS ?",
+ bun.Ident("domain"), "",
+ "/@",
+ bun.Ident("username"),
+ bun.Ident("domain_username"),
+ )
+ }
+
+ // Return only accounts with `[domain]/@[username]`
+ // later in the alphabet (a-z) than provided maxID.
+ if maxID != "" {
+ if dbDialect == dialect.SQLite {
+ idsQ = idsQ.Where("? > ?", bun.Ident("domain_username"), maxID)
+ } else {
+ idsQ = idsQ.Where("? > ?", orderQPG, maxID)
+ }
+ }
+
+ // Page down.
+ // It's counterintuitive because it
+ // says ASC in the query, but we're
+ // going forwards in the alphabet,
+ // and z > a in a string comparison.
+ if dbDialect == dialect.SQLite {
+ idsQ = idsQ.OrderExpr("? ASC", bun.Ident("domain_username"))
+ } else {
+ idsQ = idsQ.OrderExpr("? ASC", orderQPG)
+ }
+
+ // Select this batch, providing a
+ // slice to throw away username_domain.
+ err := idsQ.Scan(ctx, &oldAccountIDs, new([]string))
+ if err != nil {
+ return err
+ }
+
+ l := len(oldAccountIDs)
+ if len(oldAccountIDs) == 0 {
+ // Nothing left
+ // to update.
+ break
+ }
+
+ // Get ready to select old accounts by their IDs.
+ oldAccounts := make([]*old_gtsmodel.Account, 0, l)
+ batchQ := tx.
+ NewSelect().
+ Model(&oldAccounts).
+ Where("? IN (?)", bun.Ident("id"), bun.In(oldAccountIDs))
+
+ // Order batch by usernameDomain
+ // to ensure paging consistent.
+ if dbDialect == dialect.SQLite {
+ batchQ = batchQ.OrderExpr("? ASC", orderQSqlite)
+ } else {
+ batchQ = batchQ.OrderExpr("? ASC", orderQPG)
+ }
+
+ // Select old accounts.
+ if err := batchQ.Scan(ctx); err != nil {
+ return err
+ }
+
+ // Convert old accounts into new accounts.
+ newAccounts := make([]*new_gtsmodel.Account, 0, l)
+ for _, oldAccount := range oldAccounts {
+
+ var actorType new_gtsmodel.AccountActorType
+ if oldAccount.Domain == "" && oldAccount.Username == host {
+ // This is our instance account, override actor
+ // type to Service, as previously it was just person.
+ actorType = new_gtsmodel.AccountActorTypeService
+ } else {
+ // Not our instance account, just parse new actor type.
+ actorType = new_gtsmodel.ParseAccountActorType(oldAccount.ActorType)
+ }
+
+ if actorType == new_gtsmodel.AccountActorTypeUnknown {
+ // This should not really happen, but it if does
+ // just warn + set to person rather than failing.
+ log.Warnf(ctx,
+ "account %s actor type %s was not a recognized actor type, falling back to Person",
+ oldAccount.ID, oldAccount.ActorType,
+ )
+ actorType = new_gtsmodel.AccountActorTypePerson
+ }
+
+ newAccount := &new_gtsmodel.Account{
+ ID: oldAccount.ID,
+ CreatedAt: oldAccount.CreatedAt,
+ UpdatedAt: oldAccount.UpdatedAt,
+ FetchedAt: oldAccount.FetchedAt,
+ Username: oldAccount.Username,
+ Domain: oldAccount.Domain,
+ AvatarMediaAttachmentID: oldAccount.AvatarMediaAttachmentID,
+ AvatarRemoteURL: oldAccount.AvatarRemoteURL,
+ HeaderMediaAttachmentID: oldAccount.HeaderMediaAttachmentID,
+ HeaderRemoteURL: oldAccount.HeaderRemoteURL,
+ DisplayName: oldAccount.DisplayName,
+ EmojiIDs: oldAccount.EmojiIDs,
+ Fields: oldAccount.Fields,
+ FieldsRaw: oldAccount.FieldsRaw,
+ Note: oldAccount.Note,
+ NoteRaw: oldAccount.NoteRaw,
+ AlsoKnownAsURIs: oldAccount.AlsoKnownAsURIs,
+ MovedToURI: oldAccount.MovedToURI,
+ MoveID: oldAccount.MoveID,
+ Locked: oldAccount.Locked,
+ Discoverable: oldAccount.Discoverable,
+ URI: oldAccount.URI,
+ URL: oldAccount.URL,
+ InboxURI: oldAccount.InboxURI,
+ SharedInboxURI: oldAccount.SharedInboxURI,
+ OutboxURI: oldAccount.OutboxURI,
+ FollowingURI: oldAccount.FollowingURI,
+ FollowersURI: oldAccount.FollowersURI,
+ FeaturedCollectionURI: oldAccount.FeaturedCollectionURI,
+ ActorType: actorType,
+ PrivateKey: oldAccount.PrivateKey,
+ PublicKey: oldAccount.PublicKey,
+ PublicKeyURI: oldAccount.PublicKeyURI,
+ PublicKeyExpiresAt: oldAccount.PublicKeyExpiresAt,
+ SensitizedAt: oldAccount.SensitizedAt,
+ SilencedAt: oldAccount.SilencedAt,
+ SuspendedAt: oldAccount.SuspendedAt,
+ SuspensionOrigin: oldAccount.SuspensionOrigin,
+ }
+
+ newAccounts = append(newAccounts, newAccount)
+ }
+
+ // Insert this batch of accounts.
+ res, err := tx.
+ NewInsert().
+ Model(&newAccounts).
+ Returning("").
+ Exec(ctx)
+ if err != nil {
+ return err
+ }
+
+ rowsAffected, err := res.RowsAffected()
+ if err != nil {
+ return err
+ }
+
+ // Add to updated count.
+ updated += int(rowsAffected)
+ if updated == total {
+ // Done.
+ break
+ }
+
+ // Set next page.
+ fromAcct := oldAccounts[l-1]
+ maxID = fromAcct.Domain + "/@" + fromAcct.Username
+
+ // Log helpful message to admin.
+ log.Infof(ctx,
+ "migrated %d of %d accounts (next page will be from %s)",
+ updated, total, maxID,
+ )
+ }
+
+ if total != int(updated) {
+ // Return error here in order to rollback the whole transaction.
+ return fmt.Errorf("total=%d does not match updated=%d", total, updated)
+ }
+
+ log.Infof(ctx, "finished migrating %d accounts", total)
+
+ // Drop the old table.
+ log.Info(ctx, "dropping old accounts table")
+ if _, err := tx.
+ NewDropTable().
+ Table("accounts").
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Rename new table to old table.
+ log.Info(ctx, "renaming new accounts table")
+ if _, err := tx.
+ ExecContext(
+ ctx,
+ "ALTER TABLE ? RENAME TO ?",
+ bun.Ident("new_accounts"),
+ bun.Ident("accounts"),
+ ); err != nil {
+ return err
+ }
+
+ // Add all account indexes to the new table.
+ log.Info(ctx, "recreating indexes on new accounts table")
+ for index, columns := range map[string][]string{
+ "accounts_domain_idx": {"domain"},
+ "accounts_uri_idx": {"uri"},
+ "accounts_url_idx": {"url"},
+ "accounts_inbox_uri_idx": {"inbox_uri"},
+ "accounts_outbox_uri_idx": {"outbox_uri"},
+ "accounts_followers_uri_idx": {"followers_uri"},
+ "accounts_following_uri_idx": {"following_uri"},
+ } {
+ if _, err := tx.
+ NewCreateIndex().
+ Table("accounts").
+ Index(index).
+ Column(columns...).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ if dbDialect == dialect.PG {
+ log.Info(ctx, "moving postgres constraints from old table to new table")
+
+ type spec struct {
+ old string
+ new string
+ columns []string
+ }
+
+ // Rename uniqueness constraints from
+ // "new_accounts_*" to "accounts_*".
+ for _, spec := range []spec{
+ {
+ old: "new_accounts_pkey",
+ new: "accounts_pkey",
+ columns: []string{"id"},
+ },
+ {
+ old: "new_accounts_uri_key",
+ new: "accounts_uri_key",
+ columns: []string{"uri"},
+ },
+ {
+ old: "new_accounts_public_key_uri_key",
+ new: "accounts_public_key_uri_key",
+ columns: []string{"public_key_uri"},
+ },
+ } {
+ if _, err := tx.ExecContext(
+ ctx,
+ "ALTER TABLE ? DROP CONSTRAINT IF EXISTS ?",
+ bun.Ident("public.accounts"),
+ bun.Safe(spec.old),
+ ); err != nil {
+ return err
+ }
+
+ if _, err := tx.ExecContext(
+ ctx,
+ "ALTER TABLE ? ADD CONSTRAINT ? UNIQUE(?)",
+ bun.Ident("public.accounts"),
+ bun.Safe(spec.new),
+ bun.Safe(strings.Join(spec.columns, ",")),
+ ); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/common/common.go b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/common/common.go
new file mode 100644
index 000000000..8db9f1c31
--- /dev/null
+++ b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/common/common.go
@@ -0,0 +1,26 @@
+// 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 .
+
+package common
+
+import "time"
+
+type Field struct {
+ Name string
+ Value string
+ VerifiedAt time.Time `bun:",nullzero"`
+}
diff --git a/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/new/account.go b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/new/account.go
new file mode 100644
index 000000000..55fa7f1b4
--- /dev/null
+++ b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/new/account.go
@@ -0,0 +1,98 @@
+// 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 .
+
+package gtsmodel
+
+import (
+ "crypto/rsa"
+ "strings"
+ "time"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/common"
+ "github.com/uptrace/bun"
+)
+
+type Account struct {
+ bun.BaseModel `bun:"table:new_accounts"`
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
+ Username string `bun:",nullzero,notnull,unique:accounts_username_domain_uniq"`
+ Domain string `bun:",nullzero,unique:accounts_username_domain_uniq"`
+ AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
+ AvatarRemoteURL string `bun:",nullzero"`
+ HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
+ HeaderRemoteURL string `bun:",nullzero"`
+ DisplayName string `bun:",nullzero"`
+ EmojiIDs []string `bun:"emojis,array"`
+ Fields []*common.Field `bun:",nullzero"`
+ FieldsRaw []*common.Field `bun:",nullzero"`
+ Note string `bun:",nullzero"`
+ NoteRaw string `bun:",nullzero"`
+ MemorializedAt time.Time `bun:"type:timestamptz,nullzero"`
+ AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
+ MovedToURI string `bun:",nullzero"`
+ MoveID string `bun:"type:CHAR(26),nullzero"`
+ Locked *bool `bun:",nullzero,notnull,default:true"`
+ Discoverable *bool `bun:",nullzero,notnull,default:false"`
+ URI string `bun:",nullzero,notnull,unique"`
+ URL string `bun:",nullzero"`
+ InboxURI string `bun:",nullzero"`
+ SharedInboxURI *string `bun:""`
+ OutboxURI string `bun:",nullzero"`
+ FollowingURI string `bun:",nullzero"`
+ FollowersURI string `bun:",nullzero"`
+ FeaturedCollectionURI string `bun:",nullzero"`
+ ActorType AccountActorType `bun:",nullzero,notnull"`
+ PrivateKey *rsa.PrivateKey `bun:""`
+ PublicKey *rsa.PublicKey `bun:",notnull"`
+ PublicKeyURI string `bun:",nullzero,notnull,unique"`
+ PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
+ SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
+ SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
+ SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
+ SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
+}
+
+type AccountActorType int16
+
+const (
+ AccountActorTypeUnknown AccountActorType = 0
+ AccountActorTypeApplication AccountActorType = 1 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
+ AccountActorTypeGroup AccountActorType = 2 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
+ AccountActorTypeOrganization AccountActorType = 3 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
+ AccountActorTypePerson AccountActorType = 4 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
+ AccountActorTypeService AccountActorType = 5 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
+)
+
+func ParseAccountActorType(in string) AccountActorType {
+ switch strings.ToLower(in) {
+ case "application":
+ return AccountActorTypeApplication
+ case "group":
+ return AccountActorTypeGroup
+ case "organization":
+ return AccountActorTypeOrganization
+ case "person":
+ return AccountActorTypePerson
+ case "service":
+ return AccountActorTypeService
+ default:
+ return AccountActorTypeUnknown
+ }
+}
diff --git a/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/old/account.go b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/old/account.go
new file mode 100644
index 000000000..a630805d4
--- /dev/null
+++ b/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/old/account.go
@@ -0,0 +1,70 @@
+// 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 .
+
+package gtsmodel
+
+import (
+ "crypto/rsa"
+ "time"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/common"
+ "github.com/uptrace/bun"
+)
+
+type Account struct {
+ bun.BaseModel `bun:"table:accounts"`
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
+ Username string `bun:",nullzero,notnull,unique:usernamedomain"`
+ Domain string `bun:",nullzero,unique:usernamedomain"`
+ AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
+ AvatarRemoteURL string `bun:",nullzero"`
+ HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
+ HeaderRemoteURL string `bun:",nullzero"`
+ DisplayName string `bun:""`
+ EmojiIDs []string `bun:"emojis,array"`
+ Fields []*common.Field `bun:""`
+ FieldsRaw []*common.Field `bun:""`
+ Note string `bun:""`
+ NoteRaw string `bun:""`
+ Memorial *bool `bun:",default:false"`
+ AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
+ MovedToURI string `bun:",nullzero"`
+ MoveID string `bun:"type:CHAR(26),nullzero"`
+ Bot *bool `bun:",default:false"`
+ Locked *bool `bun:",default:true"`
+ Discoverable *bool `bun:",default:false"`
+ URI string `bun:",nullzero,notnull,unique"`
+ URL string `bun:",nullzero,unique"`
+ InboxURI string `bun:",nullzero,unique"`
+ SharedInboxURI *string `bun:""`
+ OutboxURI string `bun:",nullzero,unique"`
+ FollowingURI string `bun:",nullzero,unique"`
+ FollowersURI string `bun:",nullzero,unique"`
+ FeaturedCollectionURI string `bun:",nullzero,unique"`
+ ActorType string `bun:",nullzero,notnull"`
+ PrivateKey *rsa.PrivateKey `bun:""`
+ PublicKey *rsa.PublicKey `bun:",notnull"`
+ PublicKeyURI string `bun:",nullzero,notnull,unique"`
+ PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
+ SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
+ SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
+ SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
+ SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
+}
diff --git a/internal/db/error.go b/internal/db/error.go
index 43dd34df7..eccb219b3 100644
--- a/internal/db/error.go
+++ b/internal/db/error.go
@@ -29,4 +29,8 @@ var (
// ErrAlreadyExists is returned when a conflict was encountered in the db when doing an insert.
ErrAlreadyExists = errors.New("already exists")
+
+ // ErrMultipleEntries is returned when multiple entries
+ // are found in the db when only one entry is sought.
+ ErrMultipleEntries = errors.New("multiple entries")
)
diff --git a/internal/federation/authenticate.go b/internal/federation/authenticate.go
index 7d84774fd..363568f8b 100644
--- a/internal/federation/authenticate.go
+++ b/internal/federation/authenticate.go
@@ -199,9 +199,11 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
}
// Dereference the account located at owner URI.
+ // Use exact URI match, not URL match.
pubKeyAuth.Owner, _, err = f.GetAccountByURI(ctx,
requestedUsername,
pubKeyAuth.OwnerURI,
+ false,
)
if err != nil {
if gtserror.StatusCode(err) == http.StatusGone {
diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go
index 77de954c8..f882eb7c3 100644
--- a/internal/federation/dereferencing/account.go
+++ b/internal/federation/dereferencing/account.go
@@ -24,6 +24,7 @@ import (
"net/url"
"time"
+ errorsv2 "codeberg.org/gruf/go-errors/v2"
"codeberg.org/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
@@ -88,14 +89,30 @@ func accountFresh(
return !time.Now().After(staleAt)
}
-// GetAccountByURI will attempt to fetch an accounts by its URI, first checking the database. In the case of a newly-met remote model, or a remote model
-// whose last_fetched date is beyond a certain interval, the account will be dereferenced. In the case of dereferencing, some low-priority account information
-// may be enqueued for asynchronous fetching, e.g. featured account statuses (pins). An ActivityPub object indicates the account was dereferenced.
-func (d *Dereferencer) GetAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, ap.Accountable, error) {
+// GetAccountByURI will attempt to fetch an accounts by its
+// URI, first checking the database. In the case of a newly-met
+// remote model, or a remote model whose last_fetched date is
+// beyond a certain interval, the account will be dereferenced.
+// In the case of dereferencing, some low-priority account info
+// may be enqueued for asynchronous fetching, e.g. pinned statuses.
+// An ActivityPub object indicates the account was dereferenced.
+//
+// if tryURL is true, then the database will also check for a *single*
+// account where uri == account.url, not just uri == account.uri.
+// Because url does not guarantee uniqueness, you should only set
+// tryURL to true when doing searches set in motion by a user,
+// ie., when it's not important that an exact account is returned.
+func (d *Dereferencer) GetAccountByURI(
+ ctx context.Context,
+ requestUser string,
+ uri *url.URL,
+ tryURL bool,
+) (*gtsmodel.Account, ap.Accountable, error) {
// Fetch and dereference account if necessary.
account, accountable, err := d.getAccountByURI(ctx,
requestUser,
uri,
+ tryURL,
)
if err != nil {
return nil, nil, err
@@ -117,8 +134,15 @@ func (d *Dereferencer) GetAccountByURI(ctx context.Context, requestUser string,
return account, accountable, nil
}
-// getAccountByURI is a package internal form of .GetAccountByURI() that doesn't bother dereferencing featured posts on update.
-func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, ap.Accountable, error) {
+// getAccountByURI is a package internal form of
+// .GetAccountByURI() that doesn't bother dereferencing
+// featured posts on update.
+func (d *Dereferencer) getAccountByURI(
+ ctx context.Context,
+ requestUser string,
+ uri *url.URL,
+ tryURL bool,
+) (*gtsmodel.Account, ap.Accountable, error) {
var (
account *gtsmodel.Account
uriStr = uri.String()
@@ -126,9 +150,8 @@ func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string,
)
// Search the database for existing account with URI.
+ // URI is unique so if we get a hit it's that account for sure.
account, err = d.state.DB.GetAccountByURI(
- // request a barebones object, it may be in the
- // db but with related models not yet dereferenced.
gtscontext.SetBarebones(ctx),
uriStr,
)
@@ -136,13 +159,20 @@ func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string,
return nil, nil, gtserror.Newf("error checking database for account %s by uri: %w", uriStr, err)
}
- if account == nil {
- // Else, search the database for existing by URL.
- account, err = d.state.DB.GetAccountByURL(
+ if account == nil && tryURL {
+ // Else if we're permitted, search the database for *ONE*
+ // account with this URL. This can return multiple hits
+ // so check for ErrMultipleEntries. If we get exactly one
+ // hit it's *probably* the account we're looking for.
+ account, err = d.state.DB.GetOneAccountByURL(
gtscontext.SetBarebones(ctx),
uriStr,
)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ if err != nil && !errorsv2.IsV2(
+ err,
+ db.ErrNoEntries,
+ db.ErrMultipleEntries,
+ ) {
return nil, nil, gtserror.Newf("error checking database for account %s by url: %w", uriStr, err)
}
}
diff --git a/internal/federation/dereferencing/account_test.go b/internal/federation/dereferencing/account_test.go
index 8bcfce2d2..4af885468 100644
--- a/internal/federation/dereferencing/account_test.go
+++ b/internal/federation/dereferencing/account_test.go
@@ -54,6 +54,7 @@ func (suite *AccountTestSuite) TestDereferenceGroup() {
context.Background(),
fetchingAccount.Username,
groupURL,
+ false,
)
suite.NoError(err)
suite.NotNil(group)
@@ -67,7 +68,7 @@ func (suite *AccountTestSuite) TestDereferenceGroup() {
dbGroup, err := suite.db.GetAccountByURI(context.Background(), group.URI)
suite.NoError(err)
suite.Equal(group.ID, dbGroup.ID)
- suite.Equal(ap.ActorGroup, dbGroup.ActorType)
+ suite.Equal(ap.ActorGroup, dbGroup.ActorType.String())
}
func (suite *AccountTestSuite) TestDereferenceService() {
@@ -78,6 +79,7 @@ func (suite *AccountTestSuite) TestDereferenceService() {
context.Background(),
fetchingAccount.Username,
serviceURL,
+ false,
)
suite.NoError(err)
suite.NotNil(service)
@@ -91,7 +93,7 @@ func (suite *AccountTestSuite) TestDereferenceService() {
dbService, err := suite.db.GetAccountByURI(context.Background(), service.URI)
suite.NoError(err)
suite.Equal(service.ID, dbService.ID)
- suite.Equal(ap.ActorService, dbService.ActorType)
+ suite.Equal(ap.ActorService, dbService.ActorType.String())
suite.Equal("example.org", dbService.Domain)
}
@@ -110,6 +112,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURL() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
+ false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -129,6 +132,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURLNoSharedInb
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
+ false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -143,6 +147,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsername() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
+ false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -157,6 +162,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsernameDomain() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
+ false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -213,6 +219,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse("http://localhost:8080/users/thisaccountdoesnotexist"),
+ false,
)
suite.True(gtserror.IsUnretrievable(err))
suite.EqualError(err, db.ErrNoEntries.Error())
@@ -265,7 +272,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountByRedirect() {
uri := testrig.URLMustParse("https://this-will-be-redirected.butts/")
// Try dereference the test URI, since it correctly redirects to us it should return our account.
- account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri)
+ account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri, false)
suite.NoError(err)
suite.Nil(accountable)
suite.NotNil(account)
@@ -318,7 +325,7 @@ func (suite *AccountTestSuite) TestDereferenceMasqueradingLocalAccount() {
)
// Try dereference the test URI, since it correctly redirects to us it should return our account.
- account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri)
+ account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri, false)
suite.NotNil(err)
suite.Nil(account)
suite.Nil(accountable)
@@ -341,6 +348,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithNonMatchingURI()
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(remoteAltURI),
+ false,
)
suite.Equal(err.Error(), fmt.Sprintf("enrichAccount: account uri %s does not match %s", remoteURI, remoteAltURI))
suite.Nil(fetchedAccount)
@@ -357,6 +365,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithUnexpectedKeyChan
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
fetchingAcc.Username,
testrig.URLMustParse(remoteURI),
+ false,
)
suite.NoError(err)
suite.NotNil(remoteAcc)
@@ -395,6 +404,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithExpectedKeyChange
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
fetchingAcc.Username,
testrig.URLMustParse(remoteURI),
+ false,
)
suite.NoError(err)
suite.NotNil(remoteAcc)
@@ -436,6 +446,7 @@ func (suite *AccountTestSuite) TestRefreshFederatedRemoteAccountWithKeyChange()
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
fetchingAcc.Username,
testrig.URLMustParse(remoteURI),
+ false,
)
suite.NoError(err)
suite.NotNil(remoteAcc)
diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go
index 2718187cf..d99bae15b 100644
--- a/internal/federation/dereferencing/status.go
+++ b/internal/federation/dereferencing/status.go
@@ -454,7 +454,8 @@ func (d *Dereferencer) enrichStatus(
// Ensure we have the author account of the status dereferenced (+ up-to-date). If this is a new status
// (i.e. status.AccountID == "") then any error here is irrecoverable. status.AccountID must ALWAYS be set.
- if _, _, err := d.getAccountByURI(ctx, requestUser, attributedTo); err != nil && status.AccountID == "" {
+ // We want the exact URI match here as well, not the imprecise URL match.
+ if _, _, err := d.getAccountByURI(ctx, requestUser, attributedTo, false); err != nil && status.AccountID == "" {
// Note that we specifically DO NOT wrap the error, instead collapsing it as string.
// Errors fetching an account do not necessarily relate to dereferencing the status.
@@ -671,7 +672,7 @@ func (d *Dereferencer) fetchStatusMentions(
// Search existing status for a mention already stored,
// else ensure new mention's target account is populated.
- mention, alreadyExists, err = d.getPopulatedMention(ctx,
+ mention, alreadyExists, err = d.populateMentionTarget(ctx,
requestUser,
existing,
mention,
@@ -1290,7 +1291,7 @@ func (d *Dereferencer) handleStatusEdit(
return cols, nil
}
-// getPopulatedMention tries to populate the given
+// populateMentionTarget tries to populate the given
// mention with the correct TargetAccount and (if not
// yet set) TargetAccountURI, returning the populated
// mention.
@@ -1302,7 +1303,13 @@ func (d *Dereferencer) handleStatusEdit(
// Otherwise, this function will try to parse first
// the Href of the mention, and then the namestring,
// to see who it targets, and go fetch that account.
-func (d *Dereferencer) getPopulatedMention(
+//
+// Note: Ordinarily it would make sense to try the
+// namestring first, as it definitely can't be a URL
+// rather than a URI, but because some remotes do
+// silly things like only provide `@username` instead
+// of `@username@domain`, we try by URI first.
+func (d *Dereferencer) populateMentionTarget(
ctx context.Context,
requestUser string,
existing *gtsmodel.Status,
@@ -1312,8 +1319,9 @@ func (d *Dereferencer) getPopulatedMention(
bool, // True if mention already exists in the DB.
error,
) {
- // Mentions can be created using Name or Href.
- // Prefer Href (TargetAccountURI), fall back to Name.
+ // Mentions can be created using `name` or `href`.
+ //
+ // Prefer `href` (TargetAccountURI), fall back to Name.
if mention.TargetAccountURI != "" {
// Look for existing mention with target account's URI, if so use this.
@@ -1323,19 +1331,24 @@ func (d *Dereferencer) getPopulatedMention(
}
// Ensure that mention account URI is parseable.
- accountURI, err := url.Parse(mention.TargetAccountURI)
+ targetAccountURI, err := url.Parse(mention.TargetAccountURI)
if err != nil {
err := gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
return nil, false, err
}
- // Ensure we have account of the mention target dereferenced.
+ // Ensure we have the account of
+ // the mention target dereferenced.
+ //
+ // Use exact URI match only, not URL,
+ // as we want to be precise here.
mention.TargetAccount, _, err = d.getAccountByURI(ctx,
requestUser,
- accountURI,
+ targetAccountURI,
+ false,
)
if err != nil {
- err := gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
+ err := gtserror.Newf("failed to dereference account %s: %w", targetAccountURI, err)
return nil, false, err
}
} else {
@@ -1353,17 +1366,32 @@ func (d *Dereferencer) getPopulatedMention(
return existingMention, true, nil
}
- // Ensure we have the account of the mention target dereferenced.
+ // Ensure we have the account of
+ // the mention target dereferenced.
+ //
+ // This might fail if the remote does
+ // something silly like only setting
+ // `@username` and not `@username@domain`.
mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx,
requestUser,
username,
domain,
)
- if err != nil {
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
return nil, false, err
}
+ if mention.TargetAccount == nil {
+ // Probably failed for abovementioned
+ // silly reason. Nothing we can do about it.
+ err := gtserror.Newf(
+ "failed to populate mention target account (badly formatted namestring?) %s: %w",
+ mention.NameString, err,
+ )
+ return nil, false, err
+ }
+
// Look for existing mention with target account's URI, if so use this.
existingMention, ok = existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" {
diff --git a/internal/federation/federatingdb/followers.go b/internal/federation/federatingdb/followers.go
index eb46ce83f..eb224960e 100644
--- a/internal/federation/federatingdb/followers.go
+++ b/internal/federation/federatingdb/followers.go
@@ -33,7 +33,7 @@ import (
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Followers(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) {
- acct, err := f.getAccountForIRI(ctx, actorIRI)
+ acct, err := f.state.DB.GetAccountByURI(ctx, actorIRI.String())
if err != nil {
return nil, err
}
diff --git a/internal/federation/federatingdb/following.go b/internal/federation/federatingdb/following.go
index fc7b45268..793bdc9d5 100644
--- a/internal/federation/federatingdb/following.go
+++ b/internal/federation/federatingdb/following.go
@@ -32,7 +32,7 @@ import (
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Following(ctx context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) {
- acct, err := f.getAccountForIRI(ctx, actorIRI)
+ acct, err := f.state.DB.GetAccountByURI(ctx, actorIRI.String())
if err != nil {
return nil, err
}
diff --git a/internal/federation/federatingdb/outbox.go b/internal/federation/federatingdb/outbox.go
index 7bcc1f37a..116dde56f 100644
--- a/internal/federation/federatingdb/outbox.go
+++ b/internal/federation/federatingdb/outbox.go
@@ -46,12 +46,12 @@ func (f *federatingDB) SetOutbox(ctx context.Context, outbox vocab.ActivityStrea
return nil
}
-// OutboxForInbox fetches the corresponding actor's outbox IRI for the
+// OutboxForInbox fetches the corresponding local actor's outbox IRI for the
// actor's inbox IRI.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) OutboxForInbox(ctx context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) {
- acct, err := f.getAccountForIRI(ctx, inboxIRI)
+ acct, err := f.state.DB.GetOneAccountByInboxURI(ctx, inboxIRI.String())
if err != nil {
return nil, err
}
diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go
index 47390c0f6..6d5334076 100644
--- a/internal/federation/federatingdb/util.go
+++ b/internal/federation/federatingdb/util.go
@@ -28,7 +28,6 @@ import (
"codeberg.org/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
@@ -126,83 +125,30 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,
return url.Parse(fmt.Sprintf("%s://%s/%s", config.GetProtocol(), config.GetHost(), newID))
}
-// ActorForOutbox fetches the actor's IRI for the given outbox IRI.
+// ActorForOutbox fetches the local actor's IRI for the given outbox IRI.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) ActorForOutbox(ctx context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) {
- acct, err := f.getAccountForIRI(ctx, outboxIRI)
+ acct, err := f.state.DB.GetOneAccountByOutboxURI(ctx, outboxIRI.String())
if err != nil {
return nil, err
}
return url.Parse(acct.URI)
}
-// ActorForInbox fetches the actor's IRI for the given outbox IRI.
+// ActorForInbox fetches the local actor's IRI for the given inbox IRI.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) ActorForInbox(ctx context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) {
- acct, err := f.getAccountForIRI(ctx, inboxIRI)
+ acct, err := f.state.DB.GetOneAccountByInboxURI(ctx, inboxIRI.String())
if err != nil {
return nil, err
}
return url.Parse(acct.URI)
}
-// getAccountForIRI returns the account that corresponds to or owns the given IRI.
-func (f *federatingDB) getAccountForIRI(ctx context.Context, iri *url.URL) (*gtsmodel.Account, error) {
- var (
- acct *gtsmodel.Account
- err error
- )
-
- switch {
- case uris.IsUserPath(iri):
- if acct, err = f.state.DB.GetAccountByURI(ctx, iri.String()); err != nil {
- if err == db.ErrNoEntries {
- return nil, fmt.Errorf("no actor found that corresponds to uri %s", iri.String())
- }
- return nil, fmt.Errorf("db error searching for actor with uri %s", iri.String())
- }
- return acct, nil
- case uris.IsInboxPath(iri):
- if acct, err = f.state.DB.GetAccountByInboxURI(ctx, iri.String()); err != nil {
- if err == db.ErrNoEntries {
- return nil, fmt.Errorf("no actor found that corresponds to inbox %s", iri.String())
- }
- return nil, fmt.Errorf("db error searching for actor with inbox %s", iri.String())
- }
- return acct, nil
- case uris.IsOutboxPath(iri):
- if acct, err = f.state.DB.GetAccountByOutboxURI(ctx, iri.String()); err != nil {
- if err == db.ErrNoEntries {
- return nil, fmt.Errorf("no actor found that corresponds to outbox %s", iri.String())
- }
- return nil, fmt.Errorf("db error searching for actor with outbox %s", iri.String())
- }
- return acct, nil
- case uris.IsFollowersPath(iri):
- if acct, err = f.state.DB.GetAccountByFollowersURI(ctx, iri.String()); err != nil {
- if err == db.ErrNoEntries {
- return nil, fmt.Errorf("no actor found that corresponds to followers_uri %s", iri.String())
- }
- return nil, fmt.Errorf("db error searching for actor with followers_uri %s", iri.String())
- }
- return acct, nil
- case uris.IsFollowingPath(iri):
- if acct, err = f.state.DB.GetAccountByFollowingURI(ctx, iri.String()); err != nil {
- if err == db.ErrNoEntries {
- return nil, fmt.Errorf("no actor found that corresponds to following_uri %s", iri.String())
- }
- return nil, fmt.Errorf("db error searching for actor with following_uri %s", iri.String())
- }
- return acct, nil
- default:
- return nil, fmt.Errorf("getActorForIRI: iri %s not recognised", iri)
- }
-}
-
// collectFollows takes a slice of iris and converts them into ActivityStreamsCollection of IRIs.
-func (f *federatingDB) collectIRIs(ctx context.Context, iris []*url.URL) (vocab.ActivityStreamsCollection, error) {
+func (f *federatingDB) collectIRIs(_ context.Context, iris []*url.URL) (vocab.ActivityStreamsCollection, error) {
collection := streams.NewActivityStreamsCollection()
items := streams.NewActivityStreamsItemsProperty()
for _, i := range iris {
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index bb07b8b16..d681304ba 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -31,57 +31,247 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/log"
)
-// Account represents either a local or a remote fediverse
-// account, gotosocial or otherwise (mastodon, pleroma, etc).
+// Account represents either a local or a remote ActivityPub actor.
+// https://www.w3.org/TR/activitypub/#actor-objects
type Account struct {
- ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
- CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created.
- UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated.
- FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
- Username string `bun:",nullzero,notnull,unique:usernamedomain"` // Username of the account, should just be a string of [a-zA-Z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``. Username and domain should be unique *with* each other
- Domain string `bun:",nullzero,unique:usernamedomain"` // Domain of the account, will be null if this is a local account, otherwise something like ``example.org``. Should be unique with username.
- AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
- AvatarMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to avatarMediaAttachmentID
- AvatarRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched?
- HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
- HeaderMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID
- HeaderRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched?
- DisplayName string `bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
- EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this account's bio, display name, etc
- Emojis []*Emoji `bun:"attached_emojis,m2m:account_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
- Fields []*Field `bun:""` // A slice of of fields that this account has added to their profile.
- FieldsRaw []*Field `bun:""` // The raw (unparsed) content of fields that this account has added to their profile, without conversion to HTML, only available when requester = target
- Note string `bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves)
- NoteRaw string `bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target
- Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away?
- AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs.
- AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db).
- MovedToURI string `bun:",nullzero"` // This account has (or claims to have) moved to this account URI. Even if this field is set the move may not yet have been processed. Check `move` for this.
- MovedTo *Account `bun:"-"` // This account has moved to this account (field not stored in the db).
- MoveID string `bun:"type:CHAR(26),nullzero"` // ID of a Move in the database for this account. Only set if we received or created a Move activity for which this account URI was the origin.
- Move *Move `bun:"-"` // Move corresponding to MoveID, if set.
- Bot *bool `bun:",default:false"` // Does this account identify itself as a bot?
- Locked *bool `bun:",default:true"` // Does this account need an approval for new followers?
- Discoverable *bool `bun:",default:false"` // Should this account be shown in the instance's profile directory?
- URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI for this account.
- URL string `bun:",nullzero,unique"` // Web URL for this account's profile
- InboxURI string `bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to
- SharedInboxURI *string `bun:""` // Address of this account's ActivityPub sharedInbox. Gotcha warning: this is a string pointer because it has three possible states: 1. We don't know yet if the account has a shared inbox -- null. 2. We know it doesn't have a shared inbox -- empty string. 3. We know it does have a shared inbox -- url string.
- OutboxURI string `bun:",nullzero,unique"` // Address of this account's activitypub outbox
- FollowingURI string `bun:",nullzero,unique"` // URI for getting the following list of this account
- FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account
- FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account
- ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account?
- PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts
- PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for authorizing signed activitypub requests, will be defined for both local and remote accounts
- PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key
- PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // PublicKey will expire/has expired at given time, and should be fetched again as appropriate. Only ever set for remote accounts.
- SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive?
- SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
- SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
- SuspensionOrigin string `bun:"type:CHAR(26),nullzero"` // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID
- Settings *AccountSettings `bun:"-"` // gtsmodel.AccountSettings for this account.
- Stats *AccountStats `bun:"-"` // gtsmodel.AccountStats for this account.
+ // Database ID of the account.
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
+
+ // Datetime when the account was created.
+ // Corresponds to ActivityStreams `published` prop.
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+
+ // Datetime when was the account was last updated,
+ // ie., when the actor last sent out an Update
+ // activity, or if never, when it was `published`.
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+
+ // Datetime when the account was last fetched /
+ // dereferenced by this GoToSocial instance.
+ FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
+
+ // Username of the account.
+ //
+ // Corresponds to AS `preferredUsername` prop, which gives
+ // no uniqueness guarantee. However, we do enforce uniqueness
+ // for it as, in practice, it always is and we rely on this.
+ Username string `bun:",nullzero,notnull,unique:accounts_username_domain_uniq"`
+
+ // Domain of the account, discovered via webfinger.
+ //
+ // Null if this is a local account, otherwise
+ // something like `example.org`.
+ Domain string `bun:",nullzero,unique:accounts_username_domain_uniq"`
+
+ // Database ID of the account's avatar MediaAttachment, if set.
+ AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
+
+ // MediaAttachment corresponding to AvatarMediaAttachmentID.
+ AvatarMediaAttachment *MediaAttachment `bun:"-"`
+
+ // URL of the avatar media.
+ //
+ // Null for local accounts.
+ AvatarRemoteURL string `bun:",nullzero"`
+
+ // Database ID of the account's header MediaAttachment, if set.
+ HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
+
+ // MediaAttachment corresponding to HeaderMediaAttachmentID.
+ HeaderMediaAttachment *MediaAttachment `bun:"-"`
+
+ // URL of the header media.
+ //
+ // Null for local accounts.
+ HeaderRemoteURL string `bun:",nullzero"`
+
+ // Display name for this account, if set.
+ //
+ // Corresponds to the ActivityStreams `name` property.
+ //
+ // If null, fall back to username for display purposes.
+ DisplayName string `bun:",nullzero"`
+
+ // Database IDs of any emojis used in
+ // this account's bio, display name, etc
+ EmojiIDs []string `bun:"emojis,array"`
+
+ // Emojis corresponding to EmojiIDs.
+ Emojis []*Emoji `bun:"-"`
+
+ // A slice of of key/value fields that
+ // this account has added to their profile.
+ //
+ // Corresponds to schema.org PropertyValue types in `attachments`.
+ Fields []*Field `bun:",nullzero"`
+
+ // The raw (unparsed) content of fields that this
+ // account has added to their profile, before
+ // conversion to HTML.
+ //
+ // Only set for local accounts.
+ FieldsRaw []*Field `bun:",nullzero"`
+
+ // A note that this account has on their profile
+ // (ie., the account's bio/description of themselves).
+ //
+ // Corresponds to the ActivityStreams `summary` property.
+ Note string `bun:",nullzero"`
+
+ // The raw (unparsed) version of Note, before conversion to HTML.
+ //
+ // Only set for local accounts.
+ NoteRaw string `bun:",nullzero"`
+
+ // ActivityPub URI/IDs by which this account is also known.
+ //
+ // Corresponds to the ActivityStreams `alsoKnownAs` property.
+ AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
+
+ // Accounts matching AlsoKnownAsURIs.
+ AlsoKnownAs []*Account `bun:"-"`
+
+ // URI/ID to which the account has (or claims to have) moved.
+ //
+ // Corresponds to the ActivityStreams `movedTo` property.
+ //
+ // Even if this field is set the move may not yet have been
+ // processed. Check `move` for this.
+ MovedToURI string `bun:",nullzero"`
+
+ // Account matching MovedToURI.
+ MovedTo *Account `bun:"-"`
+
+ // ID of a Move in the database for this account.
+ // Only set if we received or created a Move activity
+ // for which this account URI was the origin.
+ MoveID string `bun:"type:CHAR(26),nullzero"`
+
+ // Move corresponding to MoveID, if set.
+ Move *Move `bun:"-"`
+
+ // True if account requires manual approval of Follows.
+ //
+ // Corresponds to AS `manuallyApprovesFollowers` prop.
+ Locked *bool `bun:",nullzero,notnull,default:true"`
+
+ // True if account has opted in to being shown in
+ // directories and exposed to search engines.
+ //
+ // Corresponds to the toot `discoverable` property.
+ Discoverable *bool `bun:",nullzero,notnull,default:false"`
+
+ // ActivityPub URI/ID for this account.
+ //
+ // Must be set, must be unique.
+ URI string `bun:",nullzero,notnull,unique"`
+
+ // URL at which a web representation of this
+ // account should be available, if set.
+ //
+ // Corresponds to ActivityStreams `url` prop.
+ URL string `bun:",nullzero"`
+
+ // URI of the actor's inbox.
+ //
+ // Corresponds to ActivityPub `inbox` property.
+ //
+ // According to AP this MUST be set, but some
+ // implementations don't set it for service actors.
+ InboxURI string `bun:",nullzero"`
+
+ // URI/ID of this account's sharedInbox, if set.
+ //
+ // Corresponds to ActivityPub `endpoints.sharedInbox`.
+ //
+ // Gotcha warning: this is a string pointer because
+ // it has three possible states:
+ //
+ // 1. null: We don't know (yet) if actor has a shared inbox.
+ // 2. empty: We know it doesn't have a shared inbox.
+ // 3. not empty: We know it does have a shared inbox.
+ SharedInboxURI *string `bun:""`
+
+ // URI/ID of the actor's outbox.
+ //
+ // Corresponds to ActivityPub `outbox` property.
+ //
+ // According to AP this MUST be set, but some
+ // implementations don't set it for service actors.
+ OutboxURI string `bun:",nullzero"`
+
+ // URI/ID of the actor's following collection.
+ //
+ // Corresponds to ActivityPub `following` property.
+ //
+ // According to AP this SHOULD be set.
+ FollowingURI string `bun:",nullzero"`
+
+ // URI/ID of the actor's followers collection.
+ //
+ // Corresponds to ActivityPub `followers` property.
+ //
+ // According to AP this SHOULD be set.
+ FollowersURI string `bun:",nullzero"`
+
+ // URI/ID of the actor's featured collection.
+ //
+ // Corresponds to the Toot `featured` property.
+ FeaturedCollectionURI string `bun:",nullzero"`
+
+ // ActivityStreams type of the actor.
+ //
+ // Application, Group, Organization, Person, or Service.
+ ActorType AccountActorType `bun:",nullzero,notnull"`
+
+ // Private key for signing http requests.
+ //
+ // Only defined for local accounts
+ PrivateKey *rsa.PrivateKey `bun:""`
+
+ // Public key for authorizing signed http requests.
+ //
+ // Defined for both local and remote accounts
+ PublicKey *rsa.PublicKey `bun:",notnull"`
+
+ // Dereferenceable location of this actor's public key.
+ //
+ // Corresponds to https://w3id.org/security/v1 `publicKey.id`.
+ PublicKeyURI string `bun:",nullzero,notnull,unique"`
+
+ // Datetime at which public key will expire/has expired,
+ // and should be fetched again as appropriate.
+ //
+ // Only ever set for remote accounts.
+ PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
+
+ // Datetime at which account was marked as a "memorial",
+ // ie., user owning the account has passed away.
+ MemorializedAt time.Time `bun:"type:timestamptz,nullzero"`
+
+ // Datetime at which account was set to
+ // have all its media shown as sensitive.
+ SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
+
+ // Datetime at which account was silenced.
+ SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
+
+ // Datetime at which account was suspended.
+ SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
+
+ // ID of the database entry that caused this account to
+ // be suspended. Can be an account ID or a domain block ID.
+ SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
+
+ // gtsmodel.AccountSettings for this account.
+ //
+ // Local, non-instance-actor accounts only.
+ Settings *AccountSettings `bun:"-"`
+
+ // gtsmodel.AccountStats for this account.
+ //
+ // Local accounts only.
+ Stats *AccountStats `bun:"-"`
}
// UsernameDomain returns account @username@domain (missing domain if local).
@@ -215,6 +405,59 @@ type Field struct {
VerifiedAt time.Time `bun:",nullzero"` // This field was verified at (optional).
}
+// AccountActorType is the ActivityStreams type of an actor.
+type AccountActorType enumType
+
+const (
+ AccountActorTypeUnknown AccountActorType = 0
+ AccountActorTypeApplication AccountActorType = 1 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
+ AccountActorTypeGroup AccountActorType = 2 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
+ AccountActorTypeOrganization AccountActorType = 3 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
+ AccountActorTypePerson AccountActorType = 4 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
+ AccountActorTypeService AccountActorType = 5 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
+)
+
+// String returns a stringified form of AccountActorType.
+func (t AccountActorType) String() string {
+ switch t {
+ case AccountActorTypeApplication:
+ return "Application"
+ case AccountActorTypeGroup:
+ return "Group"
+ case AccountActorTypeOrganization:
+ return "Organization"
+ case AccountActorTypePerson:
+ return "Person"
+ case AccountActorTypeService:
+ return "Service"
+ default:
+ panic("invalid notification type")
+ }
+}
+
+// ParseAccountActorType returns an
+// actor type from the given value.
+func ParseAccountActorType(in string) AccountActorType {
+ switch strings.ToLower(in) {
+ case "application":
+ return AccountActorTypeApplication
+ case "group":
+ return AccountActorTypeGroup
+ case "organization":
+ return AccountActorTypeOrganization
+ case "person":
+ return AccountActorTypePerson
+ case "service":
+ return AccountActorTypeService
+ default:
+ return AccountActorTypeUnknown
+ }
+}
+
+func (t AccountActorType) IsBot() bool {
+ return t == AccountActorTypeApplication || t == AccountActorTypeService
+}
+
// Relationship describes a requester's relationship with another account.
type Relationship struct {
ID string // The account id.
diff --git a/internal/processing/account/alias.go b/internal/processing/account/alias.go
index d7d4cf547..ca27a518c 100644
--- a/internal/processing/account/alias.go
+++ b/internal/processing/account/alias.go
@@ -107,9 +107,14 @@ func (p *Processor) Alias(
}
// Ensure we have account dereferenced.
+ //
+ // As this comes from user input, allow checking
+ // by URL to make things easier, not just to an
+ // exact AP URI (which a user might not even know).
targetAccount, _, err := p.federator.GetAccountByURI(ctx,
account.Username,
newAKA.uri,
+ true,
)
if err != nil {
err := fmt.Errorf(
diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go
index ab64c3270..2d3ef88de 100644
--- a/internal/processing/account/delete.go
+++ b/internal/processing/account/delete.go
@@ -528,7 +528,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
account.Fields = nil
account.Note = ""
account.NoteRaw = ""
- account.Memorial = util.Ptr(false)
+ account.MemorializedAt = never
account.AlsoKnownAsURIs = nil
account.MovedToURI = ""
account.Discoverable = util.Ptr(false)
@@ -546,7 +546,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
"fields",
"note",
"note_raw",
- "memorial",
+ "memorialized_at",
"also_known_as_uris",
"moved_to_uri",
"discoverable",
diff --git a/internal/processing/account/delete_test.go b/internal/processing/account/delete_test.go
index ee6fe1dfc..587071a11 100644
--- a/internal/processing/account/delete_test.go
+++ b/internal/processing/account/delete_test.go
@@ -64,7 +64,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
suite.Nil(updatedAccount.Fields)
suite.Zero(updatedAccount.Note)
suite.Zero(updatedAccount.NoteRaw)
- suite.False(*updatedAccount.Memorial)
+ suite.Zero(updatedAccount.MemorializedAt)
suite.Empty(updatedAccount.AlsoKnownAsURIs)
suite.False(*updatedAccount.Discoverable)
suite.WithinDuration(time.Now(), updatedAccount.SuspendedAt, 1*time.Minute)
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go
index eac0f0c3f..33eb4c101 100644
--- a/internal/processing/account/get.go
+++ b/internal/processing/account/get.go
@@ -66,10 +66,13 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
// Perform a last-minute fetch of target account to
// ensure remote account header / avatar is cached.
+ //
+ // Match by URI only.
latest, _, err := p.federator.GetAccountByURI(
gtscontext.SetFastFail(ctx),
requestingAccount.Username,
targetAccountURI,
+ false,
)
if err != nil {
log.Errorf(ctx, "error fetching latest target account: %v", err)
diff --git a/internal/processing/account/move.go b/internal/processing/account/move.go
index 1c5209e70..c8665cf04 100644
--- a/internal/processing/account/move.go
+++ b/internal/processing/account/move.go
@@ -119,11 +119,15 @@ func (p *Processor) MoveSelf(
unlock := p.state.ProcessingLocks.Lock(lockKey)
defer unlock()
- // Ensure we have a valid, up-to-date representation of the target account.
+ // Ensure we have a valid, up-to-date
+ // representation of the target account.
+ //
+ // Match by uri only.
targetAcct, targetAcctable, err = p.federator.GetAccountByURI(
ctx,
originAcct.Username,
targetAcctURI,
+ false,
)
if err != nil {
const text = "error dereferencing moved_to_uri"
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index a833d72c1..60d2cb8f6 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -78,8 +78,8 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
if form.Bot != nil {
- account.Bot = form.Bot
- acctColumns = append(acctColumns, "bot")
+ account.ActorType = gtsmodel.AccountActorTypeService
+ acctColumns = append(acctColumns, "actor_type")
}
if form.Locked != nil {
diff --git a/internal/processing/search/get.go b/internal/processing/search/get.go
index d1462cf53..7a65bed1d 100644
--- a/internal/processing/search/get.go
+++ b/internal/processing/search/get.go
@@ -490,7 +490,7 @@ func (p *Processor) byURI(
if includeAccounts(queryType) {
// Check if URI points to an account.
- foundAccount, err := p.accountByURI(ctx, requestingAccount, uri, resolve)
+ foundAccounts, err := p.accountsByURI(ctx, requestingAccount, uri, resolve)
if err != nil {
// Check for semi-expected error types.
// On one of these, we can continue.
@@ -508,7 +508,9 @@ func (p *Processor) byURI(
} else {
// Hit! Return early since it's extremely unlikely
// a status and an account will have the same URL.
- appendAccount(foundAccount)
+ for _, foundAccount := range foundAccounts {
+ appendAccount(foundAccount)
+ }
return nil
}
}
@@ -544,35 +546,42 @@ func (p *Processor) byURI(
return nil
}
-// accountByURI looks for one account with the given URI.
+// accountsByURI looks for one account with the given URI/ID,
+// then if nothing is found, multiple accounts with the given URL.
+//
// If resolve is false, it will only look in the database.
// If resolve is true, it will try to resolve the account
// from remote using the URI, if necessary.
//
// Will return either a hit, ErrNotRetrievable, ErrWrongType,
// or a real error that the caller should handle.
-func (p *Processor) accountByURI(
+func (p *Processor) accountsByURI(
ctx context.Context,
requestingAccount *gtsmodel.Account,
uri *url.URL,
resolve bool,
-) (*gtsmodel.Account, error) {
+) ([]*gtsmodel.Account, error) {
if resolve {
// We're allowed to resolve, leave the
// rest up to the dereferencer functions.
+ //
+ // Allow dereferencing by URL and not just URI;
+ // there are many cases where someone might
+ // paste a URL into the search bar.
account, _, err := p.federator.GetAccountByURI(
gtscontext.SetFastFail(ctx),
requestingAccount.Username,
uri,
+ true,
)
- return account, err
+ return []*gtsmodel.Account{account}, err
}
// We're not allowed to resolve; search database only.
uriStr := uri.String() // stringify uri just once
- // Search by ActivityPub URI.
+ // Search for single acct by ActivityPub URI.
account, err := p.state.DB.GetAccountByURI(ctx, uriStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = gtserror.Newf("error checking database for account using URI %s: %w", uriStr, err)
@@ -581,22 +590,22 @@ func (p *Processor) accountByURI(
if account != nil {
// We got a hit! No need to continue.
- return account, nil
+ return []*gtsmodel.Account{account}, nil
}
- // No hit yet. Fallback to try by URL.
- account, err = p.state.DB.GetAccountByURL(ctx, uriStr)
+ // No hit yet. Fallback to look for any accounts with URL.
+ accounts, err := p.state.DB.GetAccountsByURL(ctx, uriStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("error checking database for account using URL %s: %w", uriStr, err)
+ err = gtserror.Newf("error checking database for accounts using URL %s: %w", uriStr, err)
return nil, err
}
- if account != nil {
- // We got a hit! No need to continue.
- return account, nil
+ if len(accounts) != 0 {
+ // We got hits! No need to continue.
+ return accounts, nil
}
- err = fmt.Errorf("account %s could not be retrieved locally and we cannot resolve", uriStr)
+ err = fmt.Errorf("account(s) %s could not be retrieved locally and we cannot resolve", uriStr)
return nil, gtserror.SetUnretrievable(err)
}
diff --git a/internal/processing/workers/fromfediapi_move.go b/internal/processing/workers/fromfediapi_move.go
index d1e43c0c7..d2f06de5d 100644
--- a/internal/processing/workers/fromfediapi_move.go
+++ b/internal/processing/workers/fromfediapi_move.go
@@ -303,10 +303,13 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg *messages.FromFediAPI) e
}
// Account to which the Move is taking place.
+ //
+ // Match by uri only.
targetAcct, targetAcctable, err := p.federate.GetAccountByURI(
ctx,
fMsg.Receiving.Username,
targetAcctURI,
+ false,
)
if err != nil {
return gtserror.Newf(
diff --git a/internal/trans/import_test.go b/internal/trans/import_test.go
index 5177ec45b..12094f27e 100644
--- a/internal/trans/import_test.go
+++ b/internal/trans/import_test.go
@@ -103,8 +103,7 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() {
suite.Equal(testAccountBefore.DisplayName, testAccountAfter.DisplayName)
suite.Equal(testAccountBefore.Note, testAccountAfter.Note)
suite.Equal(testAccountBefore.NoteRaw, testAccountAfter.NoteRaw)
- suite.Equal(testAccountBefore.Memorial, testAccountAfter.Memorial)
- suite.Equal(testAccountBefore.Bot, testAccountAfter.Bot)
+ suite.Equal(testAccountBefore.MemorializedAt, testAccountAfter.MemorializedAt)
suite.Equal(testAccountBefore.Locked, testAccountAfter.Locked)
suite.Equal(testAccountBefore.URI, testAccountAfter.URI)
suite.Equal(testAccountBefore.URL, testAccountAfter.URL)
diff --git a/internal/trans/model/account.go b/internal/trans/model/account.go
index 097dea3a3..ffcb0d5ae 100644
--- a/internal/trans/model/account.go
+++ b/internal/trans/model/account.go
@@ -34,8 +34,6 @@ type Account struct {
DisplayName string `json:"displayName,omitempty" bun:",nullzero"`
Note string `json:"note,omitempty" bun:",nullzero"`
NoteRaw string `json:"noteRaw,omitempty" bun:",nullzero"`
- Memorial *bool `json:"memorial"`
- Bot *bool `json:"bot"`
Locked *bool `json:"locked"`
Discoverable *bool `json:"discoverable"`
URI string `json:"uri" bun:",nullzero"`
@@ -45,7 +43,7 @@ type Account struct {
FollowingURI string `json:"followingUri" bun:",nullzero"`
FollowersURI string `json:"followersUri" bun:",nullzero"`
FeaturedCollectionURI string `json:"featuredCollectionUri" bun:",nullzero"`
- ActorType string `json:"actorType" bun:",nullzero"`
+ ActorType int16 `json:"actorType" bun:",nullzero"`
PrivateKey *rsa.PrivateKey `json:"-" mapstructure:"-"`
PrivateKeyString string `json:"privateKey,omitempty" mapstructure:"privateKey" bun:"-"`
PublicKey *rsa.PublicKey `json:"-" mapstructure:"-"`
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index 741e1509e..80e1de378 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -70,19 +70,10 @@ func (c *Converter) ASRepresentationToAccount(
acct.URI = uri
// Check whether account is a usable actor type.
- switch acct.ActorType = accountable.GetTypeName(); acct.ActorType {
-
- // people, groups, and organizations aren't bots
- case ap.ActorPerson, ap.ActorGroup, ap.ActorOrganization:
- acct.Bot = util.Ptr(false)
-
- // apps and services are
- case ap.ActorApplication, ap.ActorService:
- acct.Bot = util.Ptr(true)
-
- // we don't know what this is!
- default:
- err := gtserror.Newf("unusable actor type for %s", uri)
+ actorTypeName := accountable.GetTypeName()
+ acct.ActorType = gtsmodel.ParseAccountActorType(actorTypeName)
+ if acct.ActorType == gtsmodel.AccountActorTypeUnknown {
+ err := gtserror.Newf("unusable actor type %s for %s", actorTypeName, uri)
return nil, gtserror.SetMalformed(err)
}
@@ -161,7 +152,7 @@ func (c *Converter) ASRepresentationToAccount(
acct.Note = ap.ExtractSummary(accountable)
// Assume not memorial (todo)
- acct.Memorial = util.Ptr(false)
+ acct.MemorializedAt = time.Time{}
// Extract 'manuallyApprovesFollowers' aka locked account (default = true).
manuallyApprovesFollowers := ap.GetManuallyApprovesFollowers(accountable)
diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go
index 67b7d75af..589a22df9 100644
--- a/internal/typeutils/astointernal_test.go
+++ b/internal/typeutils/astointernal_test.go
@@ -204,7 +204,6 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() {
suite.Equal("https://owncast.example.org/logo/external", acct.HeaderRemoteURL)
suite.Equal("Rob's Owncast Server", acct.DisplayName)
suite.Equal("linux audio stuff", acct.Note)
- suite.True(*acct.Bot)
suite.False(*acct.Locked)
suite.True(*acct.Discoverable)
suite.Equal("https://owncast.example.org/federation/user/rgh", acct.URI)
@@ -212,7 +211,7 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() {
suite.Equal("https://owncast.example.org/federation/user/rgh/inbox", acct.InboxURI)
suite.Equal("https://owncast.example.org/federation/user/rgh/outbox", acct.OutboxURI)
suite.Equal("https://owncast.example.org/federation/user/rgh/followers", acct.FollowersURI)
- suite.Equal("Service", acct.ActorType)
+ suite.Equal(gtsmodel.AccountActorTypeService, acct.ActorType)
suite.Equal("https://owncast.example.org/federation/user/rgh#main-key", acct.PublicKeyURI)
acct.ID = "01G42D57DTCJQE8XT9KD4K88RK"
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index 7d420de2c..4e6c6da77 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -36,7 +36,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/uris"
- "github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
)
@@ -49,7 +48,7 @@ func (c *Converter) AccountToAS(
// accountable is a service if this
// is a bot account, otherwise a person.
var accountable ap.Accountable
- if util.PtrOrZero(a.Bot) {
+ if a.ActorType.IsBot() {
accountable = streams.NewActivityStreamsService()
} else {
accountable = streams.NewActivityStreamsPerson()
@@ -393,7 +392,7 @@ func (c *Converter) AccountToASMinimal(
// accountable is a service if this
// is a bot account, otherwise a person.
var accountable ap.Accountable
- if util.PtrOrZero(a.Bot) {
+ if a.ActorType.IsBot() {
accountable = streams.NewActivityStreamsService()
} else {
accountable = streams.NewActivityStreamsPerson()
diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go
index 32f835da1..0db705ca7 100644
--- a/internal/typeutils/internaltoas_test.go
+++ b/internal/typeutils/internaltoas_test.go
@@ -27,7 +27,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -100,7 +99,7 @@ func (suite *InternalToASTestSuite) TestAccountToASBot() {
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
// Update zork to be a bot.
- testAccount.Bot = util.Ptr(true)
+ testAccount.ActorType = gtsmodel.AccountActorTypeService
if err := suite.state.DB.UpdateAccount(context.Background(), testAccount); err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 62a1ebc1e..7584e2b26 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -361,7 +361,6 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
var (
locked = util.PtrOrValue(a.Locked, true)
discoverable = util.PtrOrValue(a.Discoverable, false)
- bot = util.PtrOrValue(a.Bot, false)
)
// Remaining properties are simple and
@@ -374,7 +373,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
DisplayName: a.DisplayName,
Locked: locked,
Discoverable: discoverable,
- Bot: bot,
+ Bot: a.ActorType.IsBot(),
CreatedAt: util.FormatISO8601(a.CreatedAt),
Note: a.Note,
URL: a.URL,
@@ -518,7 +517,7 @@ func (c *Converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
ID: a.ID,
Username: a.Username,
Acct: acct,
- Bot: *a.Bot,
+ Bot: a.ActorType.IsBot(),
CreatedAt: util.FormatISO8601(a.CreatedAt),
URL: a.URL,
// Empty array (not nillable).
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index d70c210f3..da83e4e55 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -404,7 +404,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendPubl
"display_name": "",
"locked": false,
"discoverable": true,
- "bot": false,
+ "bot": true,
"created_at": "2020-05-17T13:10:59.000Z",
"note": "",
"url": "http://localhost:8080/@localhost:8080",
@@ -444,7 +444,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc
"display_name": "",
"locked": false,
"discoverable": false,
- "bot": false,
+ "bot": true,
"created_at": "2020-05-17T13:10:59.000Z",
"note": "",
"url": "http://localhost:8080/@localhost:8080",
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 08ca3b943..e12ac3405 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -292,104 +292,63 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
accounts := map[string]*gtsmodel.Account{
"instance_account": {
- ID: "01AY6P665V14JJR0AFVRT7311Y",
- Username: "localhost:8080",
- AvatarMediaAttachmentID: "",
- HeaderMediaAttachmentID: "",
- DisplayName: "",
- Fields: []*gtsmodel.Field{},
- Note: "",
- NoteRaw: "",
- Memorial: util.Ptr(false),
- MovedToURI: "",
- CreatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
- UpdatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
- Bot: util.Ptr(false),
- Locked: util.Ptr(false),
- Discoverable: util.Ptr(true),
- URI: "http://localhost:8080/users/localhost:8080",
- URL: "http://localhost:8080/@localhost:8080",
- PublicKeyURI: "http://localhost:8080/users/localhost:8080#main-key",
- FetchedAt: time.Time{},
- InboxURI: "http://localhost:8080/users/localhost:8080/inbox",
- OutboxURI: "http://localhost:8080/users/localhost:8080/outbox",
- FollowersURI: "http://localhost:8080/users/localhost:8080/followers",
- FollowingURI: "http://localhost:8080/users/localhost:8080/following",
- FeaturedCollectionURI: "http://localhost:8080/users/localhost:8080/collections/featured",
- ActorType: ap.ActorPerson,
- PrivateKey: &rsa.PrivateKey{},
- PublicKey: &rsa.PublicKey{},
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
+ ID: "01AY6P665V14JJR0AFVRT7311Y",
+ Username: "localhost:8080",
+ CreatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
+ UpdatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
+ Locked: util.Ptr(false),
+ Discoverable: util.Ptr(true),
+ URI: "http://localhost:8080/users/localhost:8080",
+ URL: "http://localhost:8080/@localhost:8080",
+ PublicKeyURI: "http://localhost:8080/users/localhost:8080#main-key",
+ InboxURI: "http://localhost:8080/users/localhost:8080/inbox",
+ OutboxURI: "http://localhost:8080/users/localhost:8080/outbox",
+ FollowersURI: "http://localhost:8080/users/localhost:8080/followers",
+ FollowingURI: "http://localhost:8080/users/localhost:8080/following",
+ FeaturedCollectionURI: "http://localhost:8080/users/localhost:8080/collections/featured",
+ ActorType: gtsmodel.AccountActorTypeService,
+ PrivateKey: &rsa.PrivateKey{},
+ PublicKey: &rsa.PublicKey{},
},
"unconfirmed_account": {
- ID: "01F8MH0BBE4FHXPH513MBVFHB0",
- Username: "weed_lord420",
- AvatarMediaAttachmentID: "",
- HeaderMediaAttachmentID: "",
- DisplayName: "",
- Fields: []*gtsmodel.Field{},
- Note: "",
- Memorial: util.Ptr(false),
- MovedToURI: "",
- CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
- UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
- Bot: util.Ptr(false),
- Locked: util.Ptr(false),
- Discoverable: util.Ptr(false),
- URI: "http://localhost:8080/users/weed_lord420",
- URL: "http://localhost:8080/@weed_lord420",
- FetchedAt: time.Time{},
- InboxURI: "http://localhost:8080/users/weed_lord420/inbox",
- OutboxURI: "http://localhost:8080/users/weed_lord420/outbox",
- FollowersURI: "http://localhost:8080/users/weed_lord420/followers",
- FollowingURI: "http://localhost:8080/users/weed_lord420/following",
- FeaturedCollectionURI: "http://localhost:8080/users/weed_lord420/collections/featured",
- ActorType: ap.ActorPerson,
- PrivateKey: &rsa.PrivateKey{},
- PublicKey: &rsa.PublicKey{},
- PublicKeyURI: "http://localhost:8080/users/weed_lord420#main-key",
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
- Settings: settings["unconfirmed_account"],
+ ID: "01F8MH0BBE4FHXPH513MBVFHB0",
+ Username: "weed_lord420",
+ CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
+ UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
+ Locked: util.Ptr(false),
+ Discoverable: util.Ptr(false),
+ URI: "http://localhost:8080/users/weed_lord420",
+ URL: "http://localhost:8080/@weed_lord420",
+ InboxURI: "http://localhost:8080/users/weed_lord420/inbox",
+ OutboxURI: "http://localhost:8080/users/weed_lord420/outbox",
+ FollowersURI: "http://localhost:8080/users/weed_lord420/followers",
+ FollowingURI: "http://localhost:8080/users/weed_lord420/following",
+ FeaturedCollectionURI: "http://localhost:8080/users/weed_lord420/collections/featured",
+ ActorType: gtsmodel.AccountActorTypePerson,
+ PrivateKey: &rsa.PrivateKey{},
+ PublicKey: &rsa.PublicKey{},
+ PublicKeyURI: "http://localhost:8080/users/weed_lord420#main-key",
+ Settings: settings["unconfirmed_account"],
},
"admin_account": {
- ID: "01F8MH17FWEB39HZJ76B6VXSKF",
- Username: "admin",
- AvatarMediaAttachmentID: "",
- HeaderMediaAttachmentID: "",
- DisplayName: "",
- Fields: []*gtsmodel.Field{},
- Note: "",
- NoteRaw: "",
- Memorial: util.Ptr(false),
- MovedToURI: "",
- CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
- UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
- Bot: util.Ptr(false),
- Locked: util.Ptr(false),
- Discoverable: util.Ptr(true),
- URI: "http://localhost:8080/users/admin",
- URL: "http://localhost:8080/@admin",
- PublicKeyURI: "http://localhost:8080/users/admin#main-key",
- FetchedAt: time.Time{},
- InboxURI: "http://localhost:8080/users/admin/inbox",
- OutboxURI: "http://localhost:8080/users/admin/outbox",
- FollowersURI: "http://localhost:8080/users/admin/followers",
- FollowingURI: "http://localhost:8080/users/admin/following",
- FeaturedCollectionURI: "http://localhost:8080/users/admin/collections/featured",
- ActorType: ap.ActorPerson,
- PrivateKey: &rsa.PrivateKey{},
- PublicKey: &rsa.PublicKey{},
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
- Settings: settings["admin_account"],
+ ID: "01F8MH17FWEB39HZJ76B6VXSKF",
+ Username: "admin",
+ CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
+ UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
+ Locked: util.Ptr(false),
+ Discoverable: util.Ptr(true),
+ URI: "http://localhost:8080/users/admin",
+ URL: "http://localhost:8080/@admin",
+ PublicKeyURI: "http://localhost:8080/users/admin#main-key",
+ InboxURI: "http://localhost:8080/users/admin/inbox",
+ OutboxURI: "http://localhost:8080/users/admin/outbox",
+ FollowersURI: "http://localhost:8080/users/admin/followers",
+ FollowingURI: "http://localhost:8080/users/admin/following",
+ FeaturedCollectionURI: "http://localhost:8080/users/admin/collections/featured",
+ ActorType: gtsmodel.AccountActorTypePerson,
+ PrivateKey: &rsa.PrivateKey{},
+ PublicKey: &rsa.PublicKey{},
+ Settings: settings["admin_account"],
},
"local_account_1": {
ID: "01F8MH1H7YV1Z7D2C8K2730QBF",
@@ -397,40 +356,29 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
AvatarMediaAttachmentID: "01F8MH58A357CV5K7R7TJMSH6S",
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3Q",
DisplayName: "original zork (he/they)",
- Fields: []*gtsmodel.Field{},
Note: "
hey yo this is my profile!
",
NoteRaw: "hey yo this is my profile!",
- Memorial: util.Ptr(false),
- MovedToURI: "",
CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
- Bot: util.Ptr(false),
Locked: util.Ptr(false),
Discoverable: util.Ptr(true),
URI: "http://localhost:8080/users/the_mighty_zork",
URL: "http://localhost:8080/@the_mighty_zork",
- FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/the_mighty_zork/inbox",
OutboxURI: "http://localhost:8080/users/the_mighty_zork/outbox",
FollowersURI: "http://localhost:8080/users/the_mighty_zork/followers",
FollowingURI: "http://localhost:8080/users/the_mighty_zork/following",
FeaturedCollectionURI: "http://localhost:8080/users/the_mighty_zork/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://localhost:8080/users/the_mighty_zork/main-key",
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
Settings: settings["local_account_1"],
},
"local_account_2": {
- ID: "01F8MH5NBDF2MV7CTC4Q5128HF",
- Username: "1happyturtle",
- AvatarMediaAttachmentID: "",
- HeaderMediaAttachmentID: "",
- DisplayName: "happy little turtle :3",
+ ID: "01F8MH5NBDF2MV7CTC4Q5128HF",
+ Username: "1happyturtle",
+ DisplayName: "happy little turtle :3",
Fields: []*gtsmodel.Field{
{
Name: "should you follow me?",
@@ -453,29 +401,21 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
},
Note: "i post about things that concern me
",
NoteRaw: "i post about things that concern me",
- Memorial: util.Ptr(false),
- MovedToURI: "",
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
- Bot: util.Ptr(false),
Locked: util.Ptr(true),
Discoverable: util.Ptr(false),
URI: "http://localhost:8080/users/1happyturtle",
URL: "http://localhost:8080/@1happyturtle",
- FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/1happyturtle/inbox",
OutboxURI: "http://localhost:8080/users/1happyturtle/outbox",
FollowersURI: "http://localhost:8080/users/1happyturtle/followers",
FollowingURI: "http://localhost:8080/users/1happyturtle/following",
FeaturedCollectionURI: "http://localhost:8080/users/1happyturtle/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://localhost:8080/users/1happyturtle#main-key",
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
Settings: settings["local_account_2"],
},
"local_account_3": {
@@ -483,7 +423,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Username: "media_mogul",
AvatarMediaAttachmentID: "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
HeaderMediaAttachmentID: "01JPHRB7F2RXPTEQFRYC85EPD9",
- DisplayName: "",
Fields: []*gtsmodel.Field{
{
Name: "I'm going to post a lot of",
@@ -506,29 +445,21 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
},
Note: "I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode
",
NoteRaw: "I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode",
- Memorial: util.Ptr(false),
- MovedToURI: "",
CreatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
UpdatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
- Bot: util.Ptr(false),
Locked: util.Ptr(false),
Discoverable: util.Ptr(false),
URI: "http://localhost:8080/users/media_mogul",
URL: "http://localhost:8080/@media_mogul",
- FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/media_mogul/inbox",
OutboxURI: "http://localhost:8080/users/media_mogul/outbox",
FollowersURI: "http://localhost:8080/users/media_mogul/followers",
FollowingURI: "http://localhost:8080/users/media_mogul/following",
FeaturedCollectionURI: "http://localhost:8080/users/media_mogul/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://localhost:8080/users/media_mogul#main-key",
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
Settings: settings["local_account_3"],
},
"remote_account_1": {
@@ -536,129 +467,92 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Username: "foss_satan",
Domain: "fossbros-anonymous.io",
DisplayName: "big gerald",
- Fields: []*gtsmodel.Field{},
Note: "i post about like, i dunno, stuff, or whatever!!!!",
- Memorial: util.Ptr(false),
- MovedToURI: "",
CreatedAt: TimeMustParse("2021-09-26T12:52:36+02:00"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
- Bot: util.Ptr(false),
Locked: util.Ptr(false),
Discoverable: util.Ptr(true),
URI: "http://fossbros-anonymous.io/users/foss_satan",
URL: "http://fossbros-anonymous.io/@foss_satan",
- FetchedAt: time.Time{},
InboxURI: "http://fossbros-anonymous.io/users/foss_satan/inbox",
SharedInboxURI: util.Ptr("http://fossbros-anonymous.io/inbox"),
OutboxURI: "http://fossbros-anonymous.io/users/foss_satan/outbox",
FollowersURI: "http://fossbros-anonymous.io/users/foss_satan/followers",
FollowingURI: "http://fossbros-anonymous.io/users/foss_satan/following",
FeaturedCollectionURI: "http://fossbros-anonymous.io/users/foss_satan/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://fossbros-anonymous.io/users/foss_satan/main-key",
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
},
"remote_account_2": {
ID: "01FHMQX3GAABWSM0S2VZEC2SWC",
Username: "Some_User",
Domain: "example.org",
DisplayName: "some user",
- Fields: []*gtsmodel.Field{},
Note: "i'm a real son of a gun",
- Memorial: util.Ptr(false),
- MovedToURI: "",
CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
- Bot: util.Ptr(false),
Locked: util.Ptr(true),
Discoverable: util.Ptr(true),
URI: "http://example.org/users/Some_User",
URL: "http://example.org/@Some_User",
- FetchedAt: time.Time{},
InboxURI: "http://example.org/users/Some_User/inbox",
SharedInboxURI: util.Ptr(""),
OutboxURI: "http://example.org/users/Some_User/outbox",
FollowersURI: "http://example.org/users/Some_User/followers",
FollowingURI: "http://example.org/users/Some_User/following",
FeaturedCollectionURI: "http://example.org/users/Some_User/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://example.org/users/Some_User#main-key",
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
},
"remote_account_3": {
ID: "062G5WYKY35KKD12EMSM3F8PJ8",
Username: "her_fuckin_maj",
Domain: "thequeenisstillalive.technology",
DisplayName: "lizzzieeeeeeeeeeee",
- Fields: []*gtsmodel.Field{},
Note: "if i die blame charles don't let that fuck become king",
- Memorial: util.Ptr(false),
- MovedToURI: "",
CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
- Bot: util.Ptr(false),
Locked: util.Ptr(true),
Discoverable: util.Ptr(true),
URI: "http://thequeenisstillalive.technology/users/her_fuckin_maj",
URL: "http://thequeenisstillalive.technology/@her_fuckin_maj",
- FetchedAt: time.Time{},
InboxURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/inbox",
SharedInboxURI: util.Ptr(""),
OutboxURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/outbox",
FollowersURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/followers",
FollowingURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/following",
FeaturedCollectionURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/collections/featured",
- ActorType: ap.ActorPerson,
+ ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj#main-key",
- SensitizedAt: time.Time{},
- SilencedAt: time.Time{},
- SuspendedAt: time.Time{},
- SuspensionOrigin: "",
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R",
},
"remote_account_4": {
- ID: "07GZRBAEMBNKGZ8Z9VSKSXKR98",
- Username: "üser",
- Domain: "xn--xample-ova.org",
- DisplayName: "",
- Note: "",
- Memorial: util.Ptr(false),
- MovedToURI: "",
- CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
- UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
- Bot: util.Ptr(false),
- Locked: util.Ptr(false),
- Discoverable: util.Ptr(false),
- 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: util.Ptr(""),
- 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,
- 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{},
- SuspensionOrigin: "",
- HeaderMediaAttachmentID: "",
+ ID: "07GZRBAEMBNKGZ8Z9VSKSXKR98",
+ Username: "üser",
+ Domain: "xn--xample-ova.org",
+ CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
+ UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
+ Locked: util.Ptr(false),
+ Discoverable: util.Ptr(false),
+ 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: util.Ptr(""),
+ 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: gtsmodel.AccountActorTypePerson,
+ PrivateKey: &rsa.PrivateKey{},
+ PublicKey: &rsa.PublicKey{},
+ PublicKeyURI: "https://xn--xample-ova.org/users/%C3%BCser#main-key",
},
}