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", }, }