[chore] Migrate accounts to new table, relax uniqueness constraint of actor url and collections (#3928)

* [chore] Migrate accounts to new table, relax uniqueness constraint of actor url and collections

* fiddle with it! (that's what she said)

* remove unused cache fields

* sillyness

* fix tiny whoopsie
This commit is contained in:
tobi
2025-04-06 14:39:40 +02:00
committed by GitHub
parent 650be1e8d0
commit 8ae2440da3
43 changed files with 1298 additions and 566 deletions

View File

@ -376,10 +376,9 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.EqualValues(requestingAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment) suite.EqualValues(requestingAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment)
suite.EqualValues(requestingAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL) suite.EqualValues(requestingAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL)
suite.EqualValues(requestingAccount.Note, dbUpdatedAccount.Note) 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.AlsoKnownAsURIs, dbUpdatedAccount.AlsoKnownAsURIs)
suite.EqualValues(requestingAccount.MovedToURI, dbUpdatedAccount.MovedToURI) suite.EqualValues(requestingAccount.MovedToURI, dbUpdatedAccount.MovedToURI)
suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot)
suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked) suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked)
suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable) suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable)
suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI) suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI)

View File

@ -88,7 +88,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
suite.Equal(testAccount.Username, apimodelAccount.Acct) suite.Equal(testAccount.Username, apimodelAccount.Acct)
suite.Equal(testAccount.DisplayName, apimodelAccount.DisplayName) suite.Equal(testAccount.DisplayName, apimodelAccount.DisplayName)
suite.Equal(*testAccount.Locked, apimodelAccount.Locked) 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.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(testAccount.URL, apimodelAccount.URL)
suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", apimodelAccount.Avatar) suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", apimodelAccount.Avatar)

View File

@ -204,7 +204,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
"display_name": "", "display_name": "",
"locked": false, "locked": false,
"discoverable": true, "discoverable": true,
"bot": false, "bot": true,
"created_at": "2020-05-17T13:10:59.000Z", "created_at": "2020-05-17T13:10:59.000Z",
"note": "", "note": "",
"url": "http://localhost:8080/@localhost:8080", "url": "http://localhost:8080/@localhost:8080",

View File

@ -31,7 +31,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api/client/search" "github.com/superseriousbusiness/gotosocial/internal/api/client/search"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
@ -1402,7 +1401,7 @@ func (suite *SearchGetTestSuite) TestSearchRemoteInstanceAccountPartial() {
FollowersURI: "http://" + theirDomain + "/users/" + theirDomain + "/followers", FollowersURI: "http://" + theirDomain + "/users/" + theirDomain + "/followers",
FollowingURI: "http://" + theirDomain + "/users/" + theirDomain + "/following", FollowingURI: "http://" + theirDomain + "/users/" + theirDomain + "/following",
FeaturedCollectionURI: "http://" + theirDomain + "/users/" + theirDomain + "/collections/featured", FeaturedCollectionURI: "http://" + theirDomain + "/users/" + theirDomain + "/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: key, PrivateKey: key,
PublicKey: &key.PublicKey, PublicKey: &key.PublicKey,
}); err != nil { }); err != nil {

View File

@ -30,7 +30,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
"github.com/superseriousbusiness/gotosocial/internal/cleaner" "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", FollowingURI: "http://" + host + "/users/new_account_domain_user/following",
FollowersURI: "http://" + host + "/users/new_account_domain_user/followers", FollowersURI: "http://" + host + "/users/new_account_domain_user/followers",
FeaturedCollectionURI: "http://" + host + "/users/new_account_domain_user/collections/featured", FeaturedCollectionURI: "http://" + host + "/users/new_account_domain_user/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: privateKey, PrivateKey: privateKey,
PublicKey: publicKey, PublicKey: publicKey,
PublicKeyURI: "http://" + host + "/users/new_account_domain_user/main-key", PublicKeyURI: "http://" + host + "/users/new_account_domain_user/main-key",

View File

@ -306,13 +306,8 @@ func (c *Caches) initAccount() {
Indices: []structr.IndexConfig{ Indices: []structr.IndexConfig{
{Fields: "ID"}, {Fields: "ID"},
{Fields: "URI"}, {Fields: "URI"},
{Fields: "URL"},
{Fields: "Username,Domain", AllowZero: true},
{Fields: "PublicKeyURI"}, {Fields: "PublicKeyURI"},
{Fields: "InboxURI"}, {Fields: "Username,Domain", AllowZero: true},
{Fields: "OutboxURI"},
{Fields: "FollowersURI"},
{Fields: "FollowingURI"},
}, },
MaxSize: cap, MaxSize: cap,
IgnoreErr: ignoreErrors, IgnoreErr: ignoreErrors,

View File

@ -240,13 +240,12 @@ func sizeofAccount() uintptr {
DisplayName: exampleUsername, DisplayName: exampleUsername,
Note: exampleText, Note: exampleText,
NoteRaw: exampleText, NoteRaw: exampleText,
Memorial: func() *bool { ok := false; return &ok }(), MemorializedAt: exampleTime,
CreatedAt: exampleTime, CreatedAt: exampleTime,
UpdatedAt: exampleTime, UpdatedAt: exampleTime,
FetchedAt: exampleTime, FetchedAt: exampleTime,
Bot: func() *bool { ok := true; return &ok }(), Locked: util.Ptr(true),
Locked: func() *bool { ok := true; return &ok }(), Discoverable: util.Ptr(false),
Discoverable: func() *bool { ok := false; return &ok }(),
URI: exampleURI, URI: exampleURI,
URL: exampleURI, URL: exampleURI,
InboxURI: exampleURI, InboxURI: exampleURI,
@ -254,7 +253,7 @@ func sizeofAccount() uintptr {
FollowersURI: exampleURI, FollowersURI: exampleURI,
FollowingURI: exampleURI, FollowingURI: exampleURI,
FeaturedCollectionURI: exampleURI, FeaturedCollectionURI: exampleURI,
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: exampleURI, PublicKeyURI: exampleURI,

View File

@ -27,37 +27,37 @@ import (
// Account contains functions related to account getting/setting/creation. // Account contains functions related to account getting/setting/creation.
type Account interface { 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) GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, error)
// GetAccountsByIDs returns accounts corresponding to given IDs. // GetAccountsByIDs returns accounts corresponding to given IDs.
GetAccountsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Account, error) 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) GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
// GetAccountByURL returns one account with the given URL, or an error if something goes wrong. // GetOneAccountByURL returns *one* account with the given ActivityStreams URL.
GetAccountByURL(ctx context.Context, uri string) (*gtsmodel.Account, error) // 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) 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) 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. // GetOneAccountByInboxURI returns one account with the given inbox_uri.
GetAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error) // 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. // GetOneAccountByOutboxURI returns one account with the given outbox_uri.
GetAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error) // 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. // GetAccountsByMovedToURI returns any accounts with given moved_to_uri set.
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(ctx context.Context, uri string) ([]*gtsmodel.Account, error) GetAccountsByMovedToURI(ctx context.Context, uri string) ([]*gtsmodel.Account, error)
// GetAccounts returns accounts // GetAccounts returns accounts

View File

@ -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) { func (a *accountDB) GetOneAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, error) {
return a.getAccount( // Select IDs of all
ctx, // accounts with this url.
"URL", var ids []string
func(account *gtsmodel.Account) error { if err := a.db.NewSelect().
return a.db.NewSelect(). TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
Model(account). Column("account.id").
Where("? = ?", bun.Ident("account.url"), url). Where("? = ?", bun.Ident("account.url"), url).
Scan(ctx) Scan(ctx, &ids); err != nil {
}, return nil, err
url, }
)
// 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) { 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) { func (a *accountDB) GetOneAccountByInboxURI(ctx context.Context, inboxURI string) (*gtsmodel.Account, error) {
return a.getAccount( // Select IDs of all accounts
ctx, // with this inbox_uri.
"InboxURI", var ids []string
func(account *gtsmodel.Account) error { if err := a.db.NewSelect().
return a.db.NewSelect(). TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
Model(account). Column("account.id").
Where("? = ?", bun.Ident("account.inbox_uri"), uri). Where("? = ?", bun.Ident("account.inbox_uri"), inboxURI).
Scan(ctx) Scan(ctx, &ids); err != nil {
}, return nil, err
uri, }
)
// 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) { func (a *accountDB) GetOneAccountByOutboxURI(ctx context.Context, outboxURI string) (*gtsmodel.Account, error) {
return a.getAccount( // Select IDs of all accounts
ctx, // with this outbox_uri.
"OutboxURI", var ids []string
func(account *gtsmodel.Account) error { if err := a.db.NewSelect().
return a.db.NewSelect(). TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
Model(account). Column("account.id").
Where("? = ?", bun.Ident("account.outbox_uri"), uri). Where("? = ?", bun.Ident("account.outbox_uri"), outboxURI).
Scan(ctx) Scan(ctx, &ids); err != nil {
}, return nil, err
uri, }
)
}
func (a *accountDB) GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error) { // Ensure exactly one account.
return a.getAccount( if len(ids) == 0 {
ctx, return nil, db.ErrNoEntries
"FollowersURI", }
func(account *gtsmodel.Account) error { if len(ids) > 1 {
return a.db.NewSelect(). return nil, db.ErrMultipleEntries
Model(account). }
Where("? = ?", bun.Ident("account.followers_uri"), uri).
Scan(ctx)
},
uri,
)
}
func (a *accountDB) GetAccountByFollowingURI(ctx context.Context, uri string) (*gtsmodel.Account, error) { return a.GetAccountByID(ctx, ids[0])
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,
)
} }
func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gtsmodel.Account, error) { 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) 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 // Fetch account from database cache with loader callback
account, err := a.state.Caches.DB.Account.LoadOne(lookup, func() (*gtsmodel.Account, error) { account, err := a.state.Caches.DB.Account.LoadOne(lookup, func() (*gtsmodel.Account, error) {
var account gtsmodel.Account var account gtsmodel.Account

View File

@ -32,11 +32,10 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db" "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/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
) )
type AccountTestSuite struct { type AccountTestSuite struct {
@ -255,7 +254,20 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
if account.URL == "" { if account.URL == "" {
return nil, sentinelErr 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) { "username@domain": func() (*gtsmodel.Account, error) {
@ -281,28 +293,14 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
if account.InboxURI == "" { if account.InboxURI == "" {
return nil, sentinelErr return nil, sentinelErr
} }
return suite.db.GetAccountByInboxURI(ctx, account.InboxURI) return suite.db.GetOneAccountByInboxURI(ctx, account.InboxURI)
}, },
"outbox_uri": func() (*gtsmodel.Account, error) { "outbox_uri": func() (*gtsmodel.Account, error) {
if account.OutboxURI == "" { if account.OutboxURI == "" {
return nil, sentinelErr return nil, sentinelErr
} }
return suite.db.GetAccountByOutboxURI(ctx, account.OutboxURI) return suite.db.GetOneAccountByOutboxURI(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)
}, },
} { } {
@ -345,71 +343,37 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
} }
} }
func (suite *AccountTestSuite) TestUpdateAccount() { func (suite *AccountTestSuite) TestGetAccountsByURLMulti() {
ctx := context.Background() ctx := context.Background()
testAccount := suite.testAccounts["local_account_1"] // Update admin account to have the same url as zork.
testAccount1 := suite.testAccounts["local_account_1"]
testAccount.DisplayName = "new display name!" testAccount2 := new(gtsmodel.Account)
testAccount.EmojiIDs = []string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"} *testAccount2 = *suite.testAccounts["admin_account"]
testAccount2.URL = testAccount1.URL
err := suite.db.UpdateAccount(ctx, testAccount) if err := suite.state.DB.UpdateAccount(ctx, testAccount2, "url"); err != nil {
suite.NoError(err) suite.FailNow(err.Error())
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")
} }
noCache := &gtsmodel.Account{} // Select all accounts with that URL.
err = dbService.DB(). // Should return 2.
NewSelect(). accounts, err := suite.state.DB.GetAccountsByURL(
Model(noCache). gtscontext.SetBarebones(ctx),
Where("? = ?", bun.Ident("account.id"), testAccount.ID). testAccount1.URL,
Relation("AvatarMediaAttachment"). )
Relation("HeaderMediaAttachment"). if err != nil {
Relation("Emojis"). suite.FailNow(err.Error())
Scan(ctx) }
suite.Len(accounts, 2)
suite.NoError(err) // Try to select one account with that URL.
suite.Equal("new display name!", noCache.DisplayName) // Should error.
suite.Equal([]string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}, noCache.EmojiIDs) account, err := suite.state.DB.GetOneAccountByURL(
suite.WithinDuration(time.Now(), noCache.UpdatedAt, 5*time.Second) gtscontext.SetBarebones(ctx),
suite.NotNil(noCache.AvatarMediaAttachment) testAccount1.URL,
suite.NotNil(noCache.HeaderMediaAttachment) )
suite.Nil(account)
// update again to remove emoji associations suite.ErrorIs(err, db.ErrMultipleEntries)
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)
} }
func (suite *AccountTestSuite) TestInsertAccountWithDefaults() { func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
@ -422,7 +386,7 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
Domain: "example.org", Domain: "example.org",
URI: "https://example.org/users/test_service", URI: "https://example.org/users/test_service",
URL: "https://example.org/@test_service", URL: "https://example.org/@test_service",
ActorType: ap.ActorService, ActorType: gtsmodel.AccountActorTypeService,
PublicKey: &key.PublicKey, PublicKey: &key.PublicKey,
PublicKeyURI: "https://example.org/users/test_service#main-key", 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.CreatedAt, 30*time.Second)
suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second) suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second)
suite.True(*newAccount.Locked) suite.True(*newAccount.Locked)
suite.False(*newAccount.Bot)
suite.False(*newAccount.Discoverable) suite.False(*newAccount.Discoverable)
} }

View File

@ -28,7 +28,6 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
@ -131,7 +130,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
FollowingURI: uris.FollowingURI, FollowingURI: uris.FollowingURI,
FollowersURI: uris.FollowersURI, FollowersURI: uris.FollowersURI,
FeaturedCollectionURI: uris.FeaturedCollectionURI, FeaturedCollectionURI: uris.FeaturedCollectionURI,
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: privKey, PrivateKey: privKey,
PublicKey: &privKey.PublicKey, PublicKey: &privKey.PublicKey,
PublicKeyURI: uris.PublicKeyURI, PublicKeyURI: uris.PublicKeyURI,
@ -283,7 +282,7 @@ func (a *adminDB) CreateInstanceAccount(ctx context.Context) error {
PrivateKey: key, PrivateKey: key,
PublicKey: &key.PublicKey, PublicKey: &key.PublicKey,
PublicKeyURI: newAccountURIs.PublicKeyURI, PublicKeyURI: newAccountURIs.PublicKeyURI,
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypeService,
URI: newAccountURIs.UserURI, URI: newAccountURIs.UserURI,
InboxURI: newAccountURIs.InboxURI, InboxURI: newAccountURIs.InboxURI,
OutboxURI: newAccountURIs.OutboxURI, OutboxURI: newAccountURIs.OutboxURI,

View File

@ -55,7 +55,7 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
URL: "https://example.org/@test", URL: "https://example.org/@test",
InboxURI: "https://example.org/users/test/inbox", InboxURI: "https://example.org/users/test/inbox",
OutboxURI: "https://example.org/users/test/outbox", OutboxURI: "https://example.org/users/test/outbox",
ActorType: "Person", ActorType: gtsmodel.AccountActorTypePerson,
PublicKeyURI: "https://example.org/test#main-key", PublicKeyURI: "https://example.org/test#main-key",
PublicKey: &key.PublicKey, PublicKey: &key.PublicKey,
} }
@ -87,7 +87,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
suite.Empty(a.NoteRaw) suite.Empty(a.NoteRaw)
suite.Empty(a.AlsoKnownAsURIs) suite.Empty(a.AlsoKnownAsURIs)
suite.Empty(a.MovedToURI) suite.Empty(a.MovedToURI)
suite.False(*a.Bot)
// Locked is especially important, since it's a bool that defaults // 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 // to true, which is why we use pointers for bools in the first place
suite.True(*a.Locked) suite.True(*a.Locked)

View File

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
package common
import "time"
type Field struct {
Name string
Value string
VerifiedAt time.Time `bun:",nullzero"`
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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"`
}

View File

@ -29,4 +29,8 @@ var (
// ErrAlreadyExists is returned when a conflict was encountered in the db when doing an insert. // ErrAlreadyExists is returned when a conflict was encountered in the db when doing an insert.
ErrAlreadyExists = errors.New("already exists") 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")
) )

View File

@ -199,9 +199,11 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
} }
// Dereference the account located at owner URI. // Dereference the account located at owner URI.
// Use exact URI match, not URL match.
pubKeyAuth.Owner, _, err = f.GetAccountByURI(ctx, pubKeyAuth.Owner, _, err = f.GetAccountByURI(ctx,
requestedUsername, requestedUsername,
pubKeyAuth.OwnerURI, pubKeyAuth.OwnerURI,
false,
) )
if err != nil { if err != nil {
if gtserror.StatusCode(err) == http.StatusGone { if gtserror.StatusCode(err) == http.StatusGone {

View File

@ -24,6 +24,7 @@ import (
"net/url" "net/url"
"time" "time"
errorsv2 "codeberg.org/gruf/go-errors/v2"
"codeberg.org/superseriousbusiness/activity/pub" "codeberg.org/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
@ -88,14 +89,30 @@ func accountFresh(
return !time.Now().After(staleAt) 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 // GetAccountByURI will attempt to fetch an accounts by its
// whose last_fetched date is beyond a certain interval, the account will be dereferenced. In the case of dereferencing, some low-priority account information // URI, first checking the database. In the case of a newly-met
// may be enqueued for asynchronous fetching, e.g. featured account statuses (pins). An ActivityPub object indicates the account was dereferenced. // remote model, or a remote model whose last_fetched date is
func (d *Dereferencer) GetAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, ap.Accountable, error) { // 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. // Fetch and dereference account if necessary.
account, accountable, err := d.getAccountByURI(ctx, account, accountable, err := d.getAccountByURI(ctx,
requestUser, requestUser,
uri, uri,
tryURL,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -117,8 +134,15 @@ func (d *Dereferencer) GetAccountByURI(ctx context.Context, requestUser string,
return account, accountable, nil return account, accountable, nil
} }
// getAccountByURI is a package internal form of .GetAccountByURI() that doesn't bother dereferencing featured posts on update. // getAccountByURI is a package internal form of
func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, ap.Accountable, error) { // .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 ( var (
account *gtsmodel.Account account *gtsmodel.Account
uriStr = uri.String() uriStr = uri.String()
@ -126,9 +150,8 @@ func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string,
) )
// Search the database for existing account with URI. // 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( 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), gtscontext.SetBarebones(ctx),
uriStr, 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) return nil, nil, gtserror.Newf("error checking database for account %s by uri: %w", uriStr, err)
} }
if account == nil { if account == nil && tryURL {
// Else, search the database for existing by URL. // Else if we're permitted, search the database for *ONE*
account, err = d.state.DB.GetAccountByURL( // 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), gtscontext.SetBarebones(ctx),
uriStr, 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) return nil, nil, gtserror.Newf("error checking database for account %s by url: %w", uriStr, err)
} }
} }

View File

@ -54,6 +54,7 @@ func (suite *AccountTestSuite) TestDereferenceGroup() {
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
groupURL, groupURL,
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(group) suite.NotNil(group)
@ -67,7 +68,7 @@ func (suite *AccountTestSuite) TestDereferenceGroup() {
dbGroup, err := suite.db.GetAccountByURI(context.Background(), group.URI) dbGroup, err := suite.db.GetAccountByURI(context.Background(), group.URI)
suite.NoError(err) suite.NoError(err)
suite.Equal(group.ID, dbGroup.ID) suite.Equal(group.ID, dbGroup.ID)
suite.Equal(ap.ActorGroup, dbGroup.ActorType) suite.Equal(ap.ActorGroup, dbGroup.ActorType.String())
} }
func (suite *AccountTestSuite) TestDereferenceService() { func (suite *AccountTestSuite) TestDereferenceService() {
@ -78,6 +79,7 @@ func (suite *AccountTestSuite) TestDereferenceService() {
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
serviceURL, serviceURL,
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(service) suite.NotNil(service)
@ -91,7 +93,7 @@ func (suite *AccountTestSuite) TestDereferenceService() {
dbService, err := suite.db.GetAccountByURI(context.Background(), service.URI) dbService, err := suite.db.GetAccountByURI(context.Background(), service.URI)
suite.NoError(err) suite.NoError(err)
suite.Equal(service.ID, dbService.ID) 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) suite.Equal("example.org", dbService.Domain)
} }
@ -110,6 +112,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURL() {
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI), testrig.URLMustParse(targetAccount.URI),
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(fetchedAccount) suite.NotNil(fetchedAccount)
@ -129,6 +132,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURLNoSharedInb
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI), testrig.URLMustParse(targetAccount.URI),
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(fetchedAccount) suite.NotNil(fetchedAccount)
@ -143,6 +147,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsername() {
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI), testrig.URLMustParse(targetAccount.URI),
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(fetchedAccount) suite.NotNil(fetchedAccount)
@ -157,6 +162,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsernameDomain() {
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI), testrig.URLMustParse(targetAccount.URI),
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(fetchedAccount) suite.NotNil(fetchedAccount)
@ -213,6 +219,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() {
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
testrig.URLMustParse("http://localhost:8080/users/thisaccountdoesnotexist"), testrig.URLMustParse("http://localhost:8080/users/thisaccountdoesnotexist"),
false,
) )
suite.True(gtserror.IsUnretrievable(err)) suite.True(gtserror.IsUnretrievable(err))
suite.EqualError(err, db.ErrNoEntries.Error()) suite.EqualError(err, db.ErrNoEntries.Error())
@ -265,7 +272,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountByRedirect() {
uri := testrig.URLMustParse("https://this-will-be-redirected.butts/") 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. // 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.NoError(err)
suite.Nil(accountable) suite.Nil(accountable)
suite.NotNil(account) 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. // 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.NotNil(err)
suite.Nil(account) suite.Nil(account)
suite.Nil(accountable) suite.Nil(accountable)
@ -341,6 +348,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithNonMatchingURI()
context.Background(), context.Background(),
fetchingAccount.Username, fetchingAccount.Username,
testrig.URLMustParse(remoteAltURI), testrig.URLMustParse(remoteAltURI),
false,
) )
suite.Equal(err.Error(), fmt.Sprintf("enrichAccount: account uri %s does not match %s", remoteURI, remoteAltURI)) suite.Equal(err.Error(), fmt.Sprintf("enrichAccount: account uri %s does not match %s", remoteURI, remoteAltURI))
suite.Nil(fetchedAccount) suite.Nil(fetchedAccount)
@ -357,6 +365,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithUnexpectedKeyChan
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx, remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
fetchingAcc.Username, fetchingAcc.Username,
testrig.URLMustParse(remoteURI), testrig.URLMustParse(remoteURI),
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(remoteAcc) suite.NotNil(remoteAcc)
@ -395,6 +404,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithExpectedKeyChange
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx, remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
fetchingAcc.Username, fetchingAcc.Username,
testrig.URLMustParse(remoteURI), testrig.URLMustParse(remoteURI),
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(remoteAcc) suite.NotNil(remoteAcc)
@ -436,6 +446,7 @@ func (suite *AccountTestSuite) TestRefreshFederatedRemoteAccountWithKeyChange()
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx, remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
fetchingAcc.Username, fetchingAcc.Username,
testrig.URLMustParse(remoteURI), testrig.URLMustParse(remoteURI),
false,
) )
suite.NoError(err) suite.NoError(err)
suite.NotNil(remoteAcc) suite.NotNil(remoteAcc)

View File

@ -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 // 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. // (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. // 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. // 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, // Search existing status for a mention already stored,
// else ensure new mention's target account is populated. // else ensure new mention's target account is populated.
mention, alreadyExists, err = d.getPopulatedMention(ctx, mention, alreadyExists, err = d.populateMentionTarget(ctx,
requestUser, requestUser,
existing, existing,
mention, mention,
@ -1290,7 +1291,7 @@ func (d *Dereferencer) handleStatusEdit(
return cols, nil return cols, nil
} }
// getPopulatedMention tries to populate the given // populateMentionTarget tries to populate the given
// mention with the correct TargetAccount and (if not // mention with the correct TargetAccount and (if not
// yet set) TargetAccountURI, returning the populated // yet set) TargetAccountURI, returning the populated
// mention. // mention.
@ -1302,7 +1303,13 @@ func (d *Dereferencer) handleStatusEdit(
// Otherwise, this function will try to parse first // Otherwise, this function will try to parse first
// the Href of the mention, and then the namestring, // the Href of the mention, and then the namestring,
// to see who it targets, and go fetch that account. // 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, ctx context.Context,
requestUser string, requestUser string,
existing *gtsmodel.Status, existing *gtsmodel.Status,
@ -1312,8 +1319,9 @@ func (d *Dereferencer) getPopulatedMention(
bool, // True if mention already exists in the DB. bool, // True if mention already exists in the DB.
error, error,
) { ) {
// Mentions can be created using Name or Href. // Mentions can be created using `name` or `href`.
// Prefer Href (TargetAccountURI), fall back to Name. //
// Prefer `href` (TargetAccountURI), fall back to Name.
if mention.TargetAccountURI != "" { if mention.TargetAccountURI != "" {
// Look for existing mention with target account's URI, if so use this. // 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. // Ensure that mention account URI is parseable.
accountURI, err := url.Parse(mention.TargetAccountURI) targetAccountURI, err := url.Parse(mention.TargetAccountURI)
if err != nil { if err != nil {
err := gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err) err := gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
return nil, false, 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, mention.TargetAccount, _, err = d.getAccountByURI(ctx,
requestUser, requestUser,
accountURI, targetAccountURI,
false,
) )
if err != nil { 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 return nil, false, err
} }
} else { } else {
@ -1353,17 +1366,32 @@ func (d *Dereferencer) getPopulatedMention(
return existingMention, true, nil 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, mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx,
requestUser, requestUser,
username, username,
domain, domain,
) )
if err != nil { if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err) err := gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
return nil, false, 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. // Look for existing mention with target account's URI, if so use this.
existingMention, ok = existing.GetMentionByTargetURI(mention.TargetAccountURI) existingMention, ok = existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" { if ok && existingMention.ID != "" {

View File

@ -33,7 +33,7 @@ import (
// //
// The library makes this call only after acquiring a lock first. // 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -32,7 +32,7 @@ import (
// //
// The library makes this call only after acquiring a lock first. // 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -46,12 +46,12 @@ func (f *federatingDB) SetOutbox(ctx context.Context, outbox vocab.ActivityStrea
return nil 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. // actor's inbox IRI.
// //
// The library makes this call only after acquiring a lock first. // 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -28,7 +28,6 @@ import (
"codeberg.org/superseriousbusiness/activity/streams/vocab" "codeberg.org/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "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)) 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. // 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) { 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 { if err != nil {
return nil, err return nil, err
} }
return url.Parse(acct.URI) 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. // 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) { 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 { if err != nil {
return nil, err return nil, err
} }
return url.Parse(acct.URI) 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. // 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() collection := streams.NewActivityStreamsCollection()
items := streams.NewActivityStreamsItemsProperty() items := streams.NewActivityStreamsItemsProperty()
for _, i := range iris { for _, i := range iris {

View File

@ -31,57 +31,247 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/log"
) )
// Account represents either a local or a remote fediverse // Account represents either a local or a remote ActivityPub actor.
// account, gotosocial or otherwise (mastodon, pleroma, etc). // https://www.w3.org/TR/activitypub/#actor-objects
type Account struct { type Account struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database // Database ID of the account.
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created. ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
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. // Datetime when the account was created.
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 // Corresponds to ActivityStreams `published` prop.
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. CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
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 // Datetime when was the account was last updated,
AvatarRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched? // ie., when the actor last sent out an Update
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present // activity, or if never, when it was `published`.
HeaderMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
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. // Datetime when the account was last fetched /
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this account's bio, display name, etc // dereferenced by this GoToSocial instance.
Emojis []*Emoji `bun:"attached_emojis,m2m:account_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
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 // Username of the account.
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 // Corresponds to AS `preferredUsername` prop, which gives
Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away? // no uniqueness guarantee. However, we do enforce uniqueness
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs. // for it as, in practice, it always is and we rely on this.
AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db). Username string `bun:",nullzero,notnull,unique:accounts_username_domain_uniq"`
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). // Domain of the account, discovered via webfinger.
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. // Null if this is a local account, otherwise
Bot *bool `bun:",default:false"` // Does this account identify itself as a bot? // something like `example.org`.
Locked *bool `bun:",default:true"` // Does this account need an approval for new followers? Domain string `bun:",nullzero,unique:accounts_username_domain_uniq"`
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. // Database ID of the account's avatar MediaAttachment, if set.
URL string `bun:",nullzero,unique"` // Web URL for this account's profile AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
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. // MediaAttachment corresponding to AvatarMediaAttachmentID.
OutboxURI string `bun:",nullzero,unique"` // Address of this account's activitypub outbox AvatarMediaAttachment *MediaAttachment `bun:"-"`
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 // URL of the avatar media.
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? // Null for local accounts.
PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts AvatarRemoteURL string `bun:",nullzero"`
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 // Database ID of the account's header MediaAttachment, if set.
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. HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
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)? // MediaAttachment corresponding to HeaderMediaAttachmentID.
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) HeaderMediaAttachment *MediaAttachment `bun:"-"`
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. // URL of the header media.
Stats *AccountStats `bun:"-"` // gtsmodel.AccountStats for this account. //
// 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). // 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). 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. // Relationship describes a requester's relationship with another account.
type Relationship struct { type Relationship struct {
ID string // The account id. ID string // The account id.

View File

@ -107,9 +107,14 @@ func (p *Processor) Alias(
} }
// Ensure we have account dereferenced. // 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, targetAccount, _, err := p.federator.GetAccountByURI(ctx,
account.Username, account.Username,
newAKA.uri, newAKA.uri,
true,
) )
if err != nil { if err != nil {
err := fmt.Errorf( err := fmt.Errorf(

View File

@ -528,7 +528,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
account.Fields = nil account.Fields = nil
account.Note = "" account.Note = ""
account.NoteRaw = "" account.NoteRaw = ""
account.Memorial = util.Ptr(false) account.MemorializedAt = never
account.AlsoKnownAsURIs = nil account.AlsoKnownAsURIs = nil
account.MovedToURI = "" account.MovedToURI = ""
account.Discoverable = util.Ptr(false) account.Discoverable = util.Ptr(false)
@ -546,7 +546,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
"fields", "fields",
"note", "note",
"note_raw", "note_raw",
"memorial", "memorialized_at",
"also_known_as_uris", "also_known_as_uris",
"moved_to_uri", "moved_to_uri",
"discoverable", "discoverable",

View File

@ -64,7 +64,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
suite.Nil(updatedAccount.Fields) suite.Nil(updatedAccount.Fields)
suite.Zero(updatedAccount.Note) suite.Zero(updatedAccount.Note)
suite.Zero(updatedAccount.NoteRaw) suite.Zero(updatedAccount.NoteRaw)
suite.False(*updatedAccount.Memorial) suite.Zero(updatedAccount.MemorializedAt)
suite.Empty(updatedAccount.AlsoKnownAsURIs) suite.Empty(updatedAccount.AlsoKnownAsURIs)
suite.False(*updatedAccount.Discoverable) suite.False(*updatedAccount.Discoverable)
suite.WithinDuration(time.Now(), updatedAccount.SuspendedAt, 1*time.Minute) suite.WithinDuration(time.Now(), updatedAccount.SuspendedAt, 1*time.Minute)

View File

@ -66,10 +66,13 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
// Perform a last-minute fetch of target account to // Perform a last-minute fetch of target account to
// ensure remote account header / avatar is cached. // ensure remote account header / avatar is cached.
//
// Match by URI only.
latest, _, err := p.federator.GetAccountByURI( latest, _, err := p.federator.GetAccountByURI(
gtscontext.SetFastFail(ctx), gtscontext.SetFastFail(ctx),
requestingAccount.Username, requestingAccount.Username,
targetAccountURI, targetAccountURI,
false,
) )
if err != nil { if err != nil {
log.Errorf(ctx, "error fetching latest target account: %v", err) log.Errorf(ctx, "error fetching latest target account: %v", err)

View File

@ -119,11 +119,15 @@ func (p *Processor) MoveSelf(
unlock := p.state.ProcessingLocks.Lock(lockKey) unlock := p.state.ProcessingLocks.Lock(lockKey)
defer unlock() 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( targetAcct, targetAcctable, err = p.federator.GetAccountByURI(
ctx, ctx,
originAcct.Username, originAcct.Username,
targetAcctURI, targetAcctURI,
false,
) )
if err != nil { if err != nil {
const text = "error dereferencing moved_to_uri" const text = "error dereferencing moved_to_uri"

View File

@ -78,8 +78,8 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
} }
if form.Bot != nil { if form.Bot != nil {
account.Bot = form.Bot account.ActorType = gtsmodel.AccountActorTypeService
acctColumns = append(acctColumns, "bot") acctColumns = append(acctColumns, "actor_type")
} }
if form.Locked != nil { if form.Locked != nil {

View File

@ -490,7 +490,7 @@ func (p *Processor) byURI(
if includeAccounts(queryType) { if includeAccounts(queryType) {
// Check if URI points to an account. // 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 { if err != nil {
// Check for semi-expected error types. // Check for semi-expected error types.
// On one of these, we can continue. // On one of these, we can continue.
@ -508,7 +508,9 @@ func (p *Processor) byURI(
} else { } else {
// Hit! Return early since it's extremely unlikely // Hit! Return early since it's extremely unlikely
// a status and an account will have the same URL. // a status and an account will have the same URL.
for _, foundAccount := range foundAccounts {
appendAccount(foundAccount) appendAccount(foundAccount)
}
return nil return nil
} }
} }
@ -544,35 +546,42 @@ func (p *Processor) byURI(
return nil 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 false, it will only look in the database.
// If resolve is true, it will try to resolve the account // If resolve is true, it will try to resolve the account
// from remote using the URI, if necessary. // from remote using the URI, if necessary.
// //
// Will return either a hit, ErrNotRetrievable, ErrWrongType, // Will return either a hit, ErrNotRetrievable, ErrWrongType,
// or a real error that the caller should handle. // or a real error that the caller should handle.
func (p *Processor) accountByURI( func (p *Processor) accountsByURI(
ctx context.Context, ctx context.Context,
requestingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account,
uri *url.URL, uri *url.URL,
resolve bool, resolve bool,
) (*gtsmodel.Account, error) { ) ([]*gtsmodel.Account, error) {
if resolve { if resolve {
// We're allowed to resolve, leave the // We're allowed to resolve, leave the
// rest up to the dereferencer functions. // 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( account, _, err := p.federator.GetAccountByURI(
gtscontext.SetFastFail(ctx), gtscontext.SetFastFail(ctx),
requestingAccount.Username, requestingAccount.Username,
uri, uri,
true,
) )
return account, err return []*gtsmodel.Account{account}, err
} }
// We're not allowed to resolve; search database only. // We're not allowed to resolve; search database only.
uriStr := uri.String() // stringify uri just once 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) account, err := p.state.DB.GetAccountByURI(ctx, uriStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = gtserror.Newf("error checking database for account using URI %s: %w", uriStr, err) 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 { if account != nil {
// We got a hit! No need to continue. // We got a hit! No need to continue.
return account, nil return []*gtsmodel.Account{account}, nil
} }
// No hit yet. Fallback to try by URL. // No hit yet. Fallback to look for any accounts with URL.
account, err = p.state.DB.GetAccountByURL(ctx, uriStr) accounts, err := p.state.DB.GetAccountsByURL(ctx, uriStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) { 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 return nil, err
} }
if account != nil { if len(accounts) != 0 {
// We got a hit! No need to continue. // We got hits! No need to continue.
return account, nil 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) return nil, gtserror.SetUnretrievable(err)
} }

View File

@ -303,10 +303,13 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg *messages.FromFediAPI) e
} }
// Account to which the Move is taking place. // Account to which the Move is taking place.
//
// Match by uri only.
targetAcct, targetAcctable, err := p.federate.GetAccountByURI( targetAcct, targetAcctable, err := p.federate.GetAccountByURI(
ctx, ctx,
fMsg.Receiving.Username, fMsg.Receiving.Username,
targetAcctURI, targetAcctURI,
false,
) )
if err != nil { if err != nil {
return gtserror.Newf( return gtserror.Newf(

View File

@ -103,8 +103,7 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() {
suite.Equal(testAccountBefore.DisplayName, testAccountAfter.DisplayName) suite.Equal(testAccountBefore.DisplayName, testAccountAfter.DisplayName)
suite.Equal(testAccountBefore.Note, testAccountAfter.Note) suite.Equal(testAccountBefore.Note, testAccountAfter.Note)
suite.Equal(testAccountBefore.NoteRaw, testAccountAfter.NoteRaw) suite.Equal(testAccountBefore.NoteRaw, testAccountAfter.NoteRaw)
suite.Equal(testAccountBefore.Memorial, testAccountAfter.Memorial) suite.Equal(testAccountBefore.MemorializedAt, testAccountAfter.MemorializedAt)
suite.Equal(testAccountBefore.Bot, testAccountAfter.Bot)
suite.Equal(testAccountBefore.Locked, testAccountAfter.Locked) suite.Equal(testAccountBefore.Locked, testAccountAfter.Locked)
suite.Equal(testAccountBefore.URI, testAccountAfter.URI) suite.Equal(testAccountBefore.URI, testAccountAfter.URI)
suite.Equal(testAccountBefore.URL, testAccountAfter.URL) suite.Equal(testAccountBefore.URL, testAccountAfter.URL)

View File

@ -34,8 +34,6 @@ type Account struct {
DisplayName string `json:"displayName,omitempty" bun:",nullzero"` DisplayName string `json:"displayName,omitempty" bun:",nullzero"`
Note string `json:"note,omitempty" bun:",nullzero"` Note string `json:"note,omitempty" bun:",nullzero"`
NoteRaw string `json:"noteRaw,omitempty" bun:",nullzero"` NoteRaw string `json:"noteRaw,omitempty" bun:",nullzero"`
Memorial *bool `json:"memorial"`
Bot *bool `json:"bot"`
Locked *bool `json:"locked"` Locked *bool `json:"locked"`
Discoverable *bool `json:"discoverable"` Discoverable *bool `json:"discoverable"`
URI string `json:"uri" bun:",nullzero"` URI string `json:"uri" bun:",nullzero"`
@ -45,7 +43,7 @@ type Account struct {
FollowingURI string `json:"followingUri" bun:",nullzero"` FollowingURI string `json:"followingUri" bun:",nullzero"`
FollowersURI string `json:"followersUri" bun:",nullzero"` FollowersURI string `json:"followersUri" bun:",nullzero"`
FeaturedCollectionURI string `json:"featuredCollectionUri" 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:"-"` PrivateKey *rsa.PrivateKey `json:"-" mapstructure:"-"`
PrivateKeyString string `json:"privateKey,omitempty" mapstructure:"privateKey" bun:"-"` PrivateKeyString string `json:"privateKey,omitempty" mapstructure:"privateKey" bun:"-"`
PublicKey *rsa.PublicKey `json:"-" mapstructure:"-"` PublicKey *rsa.PublicKey `json:"-" mapstructure:"-"`

View File

@ -70,19 +70,10 @@ func (c *Converter) ASRepresentationToAccount(
acct.URI = uri acct.URI = uri
// Check whether account is a usable actor type. // Check whether account is a usable actor type.
switch acct.ActorType = accountable.GetTypeName(); acct.ActorType { actorTypeName := accountable.GetTypeName()
acct.ActorType = gtsmodel.ParseAccountActorType(actorTypeName)
// people, groups, and organizations aren't bots if acct.ActorType == gtsmodel.AccountActorTypeUnknown {
case ap.ActorPerson, ap.ActorGroup, ap.ActorOrganization: err := gtserror.Newf("unusable actor type %s for %s", actorTypeName, uri)
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)
return nil, gtserror.SetMalformed(err) return nil, gtserror.SetMalformed(err)
} }
@ -161,7 +152,7 @@ func (c *Converter) ASRepresentationToAccount(
acct.Note = ap.ExtractSummary(accountable) acct.Note = ap.ExtractSummary(accountable)
// Assume not memorial (todo) // Assume not memorial (todo)
acct.Memorial = util.Ptr(false) acct.MemorializedAt = time.Time{}
// Extract 'manuallyApprovesFollowers' aka locked account (default = true). // Extract 'manuallyApprovesFollowers' aka locked account (default = true).
manuallyApprovesFollowers := ap.GetManuallyApprovesFollowers(accountable) manuallyApprovesFollowers := ap.GetManuallyApprovesFollowers(accountable)

View File

@ -204,7 +204,6 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() {
suite.Equal("https://owncast.example.org/logo/external", acct.HeaderRemoteURL) suite.Equal("https://owncast.example.org/logo/external", acct.HeaderRemoteURL)
suite.Equal("Rob's Owncast Server", acct.DisplayName) suite.Equal("Rob's Owncast Server", acct.DisplayName)
suite.Equal("linux audio stuff", acct.Note) suite.Equal("linux audio stuff", acct.Note)
suite.True(*acct.Bot)
suite.False(*acct.Locked) suite.False(*acct.Locked)
suite.True(*acct.Discoverable) suite.True(*acct.Discoverable)
suite.Equal("https://owncast.example.org/federation/user/rgh", acct.URI) 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/inbox", acct.InboxURI)
suite.Equal("https://owncast.example.org/federation/user/rgh/outbox", acct.OutboxURI) 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("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) suite.Equal("https://owncast.example.org/federation/user/rgh#main-key", acct.PublicKeyURI)
acct.ID = "01G42D57DTCJQE8XT9KD4K88RK" acct.ID = "01G42D57DTCJQE8XT9KD4K88RK"

View File

@ -36,7 +36,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/uris" "github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/internal/util/xslices" "github.com/superseriousbusiness/gotosocial/internal/util/xslices"
) )
@ -49,7 +48,7 @@ func (c *Converter) AccountToAS(
// accountable is a service if this // accountable is a service if this
// is a bot account, otherwise a person. // is a bot account, otherwise a person.
var accountable ap.Accountable var accountable ap.Accountable
if util.PtrOrZero(a.Bot) { if a.ActorType.IsBot() {
accountable = streams.NewActivityStreamsService() accountable = streams.NewActivityStreamsService()
} else { } else {
accountable = streams.NewActivityStreamsPerson() accountable = streams.NewActivityStreamsPerson()
@ -393,7 +392,7 @@ func (c *Converter) AccountToASMinimal(
// accountable is a service if this // accountable is a service if this
// is a bot account, otherwise a person. // is a bot account, otherwise a person.
var accountable ap.Accountable var accountable ap.Accountable
if util.PtrOrZero(a.Bot) { if a.ActorType.IsBot() {
accountable = streams.NewActivityStreamsService() accountable = streams.NewActivityStreamsService()
} else { } else {
accountable = streams.NewActivityStreamsPerson() accountable = streams.NewActivityStreamsPerson()

View File

@ -27,7 +27,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -100,7 +99,7 @@ func (suite *InternalToASTestSuite) TestAccountToASBot() {
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test *testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
// Update zork to be a bot. // 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 { if err := suite.state.DB.UpdateAccount(context.Background(), testAccount); err != nil {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }

View File

@ -361,7 +361,6 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
var ( var (
locked = util.PtrOrValue(a.Locked, true) locked = util.PtrOrValue(a.Locked, true)
discoverable = util.PtrOrValue(a.Discoverable, false) discoverable = util.PtrOrValue(a.Discoverable, false)
bot = util.PtrOrValue(a.Bot, false)
) )
// Remaining properties are simple and // Remaining properties are simple and
@ -374,7 +373,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
DisplayName: a.DisplayName, DisplayName: a.DisplayName,
Locked: locked, Locked: locked,
Discoverable: discoverable, Discoverable: discoverable,
Bot: bot, Bot: a.ActorType.IsBot(),
CreatedAt: util.FormatISO8601(a.CreatedAt), CreatedAt: util.FormatISO8601(a.CreatedAt),
Note: a.Note, Note: a.Note,
URL: a.URL, URL: a.URL,
@ -518,7 +517,7 @@ func (c *Converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
ID: a.ID, ID: a.ID,
Username: a.Username, Username: a.Username,
Acct: acct, Acct: acct,
Bot: *a.Bot, Bot: a.ActorType.IsBot(),
CreatedAt: util.FormatISO8601(a.CreatedAt), CreatedAt: util.FormatISO8601(a.CreatedAt),
URL: a.URL, URL: a.URL,
// Empty array (not nillable). // Empty array (not nillable).

View File

@ -404,7 +404,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendPubl
"display_name": "", "display_name": "",
"locked": false, "locked": false,
"discoverable": true, "discoverable": true,
"bot": false, "bot": true,
"created_at": "2020-05-17T13:10:59.000Z", "created_at": "2020-05-17T13:10:59.000Z",
"note": "", "note": "",
"url": "http://localhost:8080/@localhost:8080", "url": "http://localhost:8080/@localhost:8080",
@ -444,7 +444,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc
"display_name": "", "display_name": "",
"locked": false, "locked": false,
"discoverable": false, "discoverable": false,
"bot": false, "bot": true,
"created_at": "2020-05-17T13:10:59.000Z", "created_at": "2020-05-17T13:10:59.000Z",
"note": "", "note": "",
"url": "http://localhost:8080/@localhost:8080", "url": "http://localhost:8080/@localhost:8080",

View File

@ -294,101 +294,60 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
"instance_account": { "instance_account": {
ID: "01AY6P665V14JJR0AFVRT7311Y", ID: "01AY6P665V14JJR0AFVRT7311Y",
Username: "localhost:8080", Username: "localhost:8080",
AvatarMediaAttachmentID: "",
HeaderMediaAttachmentID: "",
DisplayName: "",
Fields: []*gtsmodel.Field{},
Note: "",
NoteRaw: "",
Memorial: util.Ptr(false),
MovedToURI: "",
CreatedAt: TimeMustParse("2020-05-17T13:10:59Z"), CreatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
UpdatedAt: TimeMustParse("2020-05-17T13:10:59Z"), UpdatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(false), Locked: util.Ptr(false),
Discoverable: util.Ptr(true), Discoverable: util.Ptr(true),
URI: "http://localhost:8080/users/localhost:8080", URI: "http://localhost:8080/users/localhost:8080",
URL: "http://localhost:8080/@localhost:8080", URL: "http://localhost:8080/@localhost:8080",
PublicKeyURI: "http://localhost:8080/users/localhost:8080#main-key", PublicKeyURI: "http://localhost:8080/users/localhost:8080#main-key",
FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/localhost:8080/inbox", InboxURI: "http://localhost:8080/users/localhost:8080/inbox",
OutboxURI: "http://localhost:8080/users/localhost:8080/outbox", OutboxURI: "http://localhost:8080/users/localhost:8080/outbox",
FollowersURI: "http://localhost:8080/users/localhost:8080/followers", FollowersURI: "http://localhost:8080/users/localhost:8080/followers",
FollowingURI: "http://localhost:8080/users/localhost:8080/following", FollowingURI: "http://localhost:8080/users/localhost:8080/following",
FeaturedCollectionURI: "http://localhost:8080/users/localhost:8080/collections/featured", FeaturedCollectionURI: "http://localhost:8080/users/localhost:8080/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypeService,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
}, },
"unconfirmed_account": { "unconfirmed_account": {
ID: "01F8MH0BBE4FHXPH513MBVFHB0", ID: "01F8MH0BBE4FHXPH513MBVFHB0",
Username: "weed_lord420", Username: "weed_lord420",
AvatarMediaAttachmentID: "",
HeaderMediaAttachmentID: "",
DisplayName: "",
Fields: []*gtsmodel.Field{},
Note: "",
Memorial: util.Ptr(false),
MovedToURI: "",
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"), CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(false), Locked: util.Ptr(false),
Discoverable: util.Ptr(false), Discoverable: util.Ptr(false),
URI: "http://localhost:8080/users/weed_lord420", URI: "http://localhost:8080/users/weed_lord420",
URL: "http://localhost:8080/@weed_lord420", URL: "http://localhost:8080/@weed_lord420",
FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/weed_lord420/inbox", InboxURI: "http://localhost:8080/users/weed_lord420/inbox",
OutboxURI: "http://localhost:8080/users/weed_lord420/outbox", OutboxURI: "http://localhost:8080/users/weed_lord420/outbox",
FollowersURI: "http://localhost:8080/users/weed_lord420/followers", FollowersURI: "http://localhost:8080/users/weed_lord420/followers",
FollowingURI: "http://localhost:8080/users/weed_lord420/following", FollowingURI: "http://localhost:8080/users/weed_lord420/following",
FeaturedCollectionURI: "http://localhost:8080/users/weed_lord420/collections/featured", FeaturedCollectionURI: "http://localhost:8080/users/weed_lord420/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://localhost:8080/users/weed_lord420#main-key", PublicKeyURI: "http://localhost:8080/users/weed_lord420#main-key",
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
Settings: settings["unconfirmed_account"], Settings: settings["unconfirmed_account"],
}, },
"admin_account": { "admin_account": {
ID: "01F8MH17FWEB39HZJ76B6VXSKF", ID: "01F8MH17FWEB39HZJ76B6VXSKF",
Username: "admin", Username: "admin",
AvatarMediaAttachmentID: "",
HeaderMediaAttachmentID: "",
DisplayName: "",
Fields: []*gtsmodel.Field{},
Note: "",
NoteRaw: "",
Memorial: util.Ptr(false),
MovedToURI: "",
CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"), CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"), UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(false), Locked: util.Ptr(false),
Discoverable: util.Ptr(true), Discoverable: util.Ptr(true),
URI: "http://localhost:8080/users/admin", URI: "http://localhost:8080/users/admin",
URL: "http://localhost:8080/@admin", URL: "http://localhost:8080/@admin",
PublicKeyURI: "http://localhost:8080/users/admin#main-key", PublicKeyURI: "http://localhost:8080/users/admin#main-key",
FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/admin/inbox", InboxURI: "http://localhost:8080/users/admin/inbox",
OutboxURI: "http://localhost:8080/users/admin/outbox", OutboxURI: "http://localhost:8080/users/admin/outbox",
FollowersURI: "http://localhost:8080/users/admin/followers", FollowersURI: "http://localhost:8080/users/admin/followers",
FollowingURI: "http://localhost:8080/users/admin/following", FollowingURI: "http://localhost:8080/users/admin/following",
FeaturedCollectionURI: "http://localhost:8080/users/admin/collections/featured", FeaturedCollectionURI: "http://localhost:8080/users/admin/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
Settings: settings["admin_account"], Settings: settings["admin_account"],
}, },
"local_account_1": { "local_account_1": {
@ -397,39 +356,28 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
AvatarMediaAttachmentID: "01F8MH58A357CV5K7R7TJMSH6S", AvatarMediaAttachmentID: "01F8MH58A357CV5K7R7TJMSH6S",
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3Q", HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3Q",
DisplayName: "original zork (he/they)", DisplayName: "original zork (he/they)",
Fields: []*gtsmodel.Field{},
Note: "<p>hey yo this is my profile!</p>", Note: "<p>hey yo this is my profile!</p>",
NoteRaw: "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"), CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"), UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(false), Locked: util.Ptr(false),
Discoverable: util.Ptr(true), Discoverable: util.Ptr(true),
URI: "http://localhost:8080/users/the_mighty_zork", URI: "http://localhost:8080/users/the_mighty_zork",
URL: "http://localhost:8080/@the_mighty_zork", URL: "http://localhost:8080/@the_mighty_zork",
FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/the_mighty_zork/inbox", InboxURI: "http://localhost:8080/users/the_mighty_zork/inbox",
OutboxURI: "http://localhost:8080/users/the_mighty_zork/outbox", OutboxURI: "http://localhost:8080/users/the_mighty_zork/outbox",
FollowersURI: "http://localhost:8080/users/the_mighty_zork/followers", FollowersURI: "http://localhost:8080/users/the_mighty_zork/followers",
FollowingURI: "http://localhost:8080/users/the_mighty_zork/following", FollowingURI: "http://localhost:8080/users/the_mighty_zork/following",
FeaturedCollectionURI: "http://localhost:8080/users/the_mighty_zork/collections/featured", FeaturedCollectionURI: "http://localhost:8080/users/the_mighty_zork/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://localhost:8080/users/the_mighty_zork/main-key", 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"], Settings: settings["local_account_1"],
}, },
"local_account_2": { "local_account_2": {
ID: "01F8MH5NBDF2MV7CTC4Q5128HF", ID: "01F8MH5NBDF2MV7CTC4Q5128HF",
Username: "1happyturtle", Username: "1happyturtle",
AvatarMediaAttachmentID: "",
HeaderMediaAttachmentID: "",
DisplayName: "happy little turtle :3", DisplayName: "happy little turtle :3",
Fields: []*gtsmodel.Field{ Fields: []*gtsmodel.Field{
{ {
@ -453,29 +401,21 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
}, },
Note: "<p>i post about things that concern me</p>", Note: "<p>i post about things that concern me</p>",
NoteRaw: "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"), CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(true), Locked: util.Ptr(true),
Discoverable: util.Ptr(false), Discoverable: util.Ptr(false),
URI: "http://localhost:8080/users/1happyturtle", URI: "http://localhost:8080/users/1happyturtle",
URL: "http://localhost:8080/@1happyturtle", URL: "http://localhost:8080/@1happyturtle",
FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/1happyturtle/inbox", InboxURI: "http://localhost:8080/users/1happyturtle/inbox",
OutboxURI: "http://localhost:8080/users/1happyturtle/outbox", OutboxURI: "http://localhost:8080/users/1happyturtle/outbox",
FollowersURI: "http://localhost:8080/users/1happyturtle/followers", FollowersURI: "http://localhost:8080/users/1happyturtle/followers",
FollowingURI: "http://localhost:8080/users/1happyturtle/following", FollowingURI: "http://localhost:8080/users/1happyturtle/following",
FeaturedCollectionURI: "http://localhost:8080/users/1happyturtle/collections/featured", FeaturedCollectionURI: "http://localhost:8080/users/1happyturtle/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://localhost:8080/users/1happyturtle#main-key", PublicKeyURI: "http://localhost:8080/users/1happyturtle#main-key",
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
Settings: settings["local_account_2"], Settings: settings["local_account_2"],
}, },
"local_account_3": { "local_account_3": {
@ -483,7 +423,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Username: "media_mogul", Username: "media_mogul",
AvatarMediaAttachmentID: "01JPHQZ0ZHC2AXJK1JQNXRXQZN", AvatarMediaAttachmentID: "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
HeaderMediaAttachmentID: "01JPHRB7F2RXPTEQFRYC85EPD9", HeaderMediaAttachmentID: "01JPHRB7F2RXPTEQFRYC85EPD9",
DisplayName: "",
Fields: []*gtsmodel.Field{ Fields: []*gtsmodel.Field{
{ {
Name: "I'm going to post a lot of", Name: "I'm going to post a lot of",
@ -506,29 +445,21 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
}, },
Note: "<p>I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode</p>", Note: "<p>I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode</p>",
NoteRaw: "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"), CreatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
UpdatedAt: TimeMustParse("2025-03-15T11:08:00Z"), UpdatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(false), Locked: util.Ptr(false),
Discoverable: util.Ptr(false), Discoverable: util.Ptr(false),
URI: "http://localhost:8080/users/media_mogul", URI: "http://localhost:8080/users/media_mogul",
URL: "http://localhost:8080/@media_mogul", URL: "http://localhost:8080/@media_mogul",
FetchedAt: time.Time{},
InboxURI: "http://localhost:8080/users/media_mogul/inbox", InboxURI: "http://localhost:8080/users/media_mogul/inbox",
OutboxURI: "http://localhost:8080/users/media_mogul/outbox", OutboxURI: "http://localhost:8080/users/media_mogul/outbox",
FollowersURI: "http://localhost:8080/users/media_mogul/followers", FollowersURI: "http://localhost:8080/users/media_mogul/followers",
FollowingURI: "http://localhost:8080/users/media_mogul/following", FollowingURI: "http://localhost:8080/users/media_mogul/following",
FeaturedCollectionURI: "http://localhost:8080/users/media_mogul/collections/featured", FeaturedCollectionURI: "http://localhost:8080/users/media_mogul/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://localhost:8080/users/media_mogul#main-key", PublicKeyURI: "http://localhost:8080/users/media_mogul#main-key",
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
Settings: settings["local_account_3"], Settings: settings["local_account_3"],
}, },
"remote_account_1": { "remote_account_1": {
@ -536,109 +467,77 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Username: "foss_satan", Username: "foss_satan",
Domain: "fossbros-anonymous.io", Domain: "fossbros-anonymous.io",
DisplayName: "big gerald", DisplayName: "big gerald",
Fields: []*gtsmodel.Field{},
Note: "i post about like, i dunno, stuff, or whatever!!!!", Note: "i post about like, i dunno, stuff, or whatever!!!!",
Memorial: util.Ptr(false),
MovedToURI: "",
CreatedAt: TimeMustParse("2021-09-26T12:52:36+02:00"), CreatedAt: TimeMustParse("2021-09-26T12:52:36+02:00"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(false), Locked: util.Ptr(false),
Discoverable: util.Ptr(true), Discoverable: util.Ptr(true),
URI: "http://fossbros-anonymous.io/users/foss_satan", URI: "http://fossbros-anonymous.io/users/foss_satan",
URL: "http://fossbros-anonymous.io/@foss_satan", URL: "http://fossbros-anonymous.io/@foss_satan",
FetchedAt: time.Time{},
InboxURI: "http://fossbros-anonymous.io/users/foss_satan/inbox", InboxURI: "http://fossbros-anonymous.io/users/foss_satan/inbox",
SharedInboxURI: util.Ptr("http://fossbros-anonymous.io/inbox"), SharedInboxURI: util.Ptr("http://fossbros-anonymous.io/inbox"),
OutboxURI: "http://fossbros-anonymous.io/users/foss_satan/outbox", OutboxURI: "http://fossbros-anonymous.io/users/foss_satan/outbox",
FollowersURI: "http://fossbros-anonymous.io/users/foss_satan/followers", FollowersURI: "http://fossbros-anonymous.io/users/foss_satan/followers",
FollowingURI: "http://fossbros-anonymous.io/users/foss_satan/following", FollowingURI: "http://fossbros-anonymous.io/users/foss_satan/following",
FeaturedCollectionURI: "http://fossbros-anonymous.io/users/foss_satan/collections/featured", FeaturedCollectionURI: "http://fossbros-anonymous.io/users/foss_satan/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://fossbros-anonymous.io/users/foss_satan/main-key", PublicKeyURI: "http://fossbros-anonymous.io/users/foss_satan/main-key",
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
}, },
"remote_account_2": { "remote_account_2": {
ID: "01FHMQX3GAABWSM0S2VZEC2SWC", ID: "01FHMQX3GAABWSM0S2VZEC2SWC",
Username: "Some_User", Username: "Some_User",
Domain: "example.org", Domain: "example.org",
DisplayName: "some user", DisplayName: "some user",
Fields: []*gtsmodel.Field{},
Note: "i'm a real son of a gun", Note: "i'm a real son of a gun",
Memorial: util.Ptr(false),
MovedToURI: "",
CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"), CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(true), Locked: util.Ptr(true),
Discoverable: util.Ptr(true), Discoverable: util.Ptr(true),
URI: "http://example.org/users/Some_User", URI: "http://example.org/users/Some_User",
URL: "http://example.org/@Some_User", URL: "http://example.org/@Some_User",
FetchedAt: time.Time{},
InboxURI: "http://example.org/users/Some_User/inbox", InboxURI: "http://example.org/users/Some_User/inbox",
SharedInboxURI: util.Ptr(""), SharedInboxURI: util.Ptr(""),
OutboxURI: "http://example.org/users/Some_User/outbox", OutboxURI: "http://example.org/users/Some_User/outbox",
FollowersURI: "http://example.org/users/Some_User/followers", FollowersURI: "http://example.org/users/Some_User/followers",
FollowingURI: "http://example.org/users/Some_User/following", FollowingURI: "http://example.org/users/Some_User/following",
FeaturedCollectionURI: "http://example.org/users/Some_User/collections/featured", FeaturedCollectionURI: "http://example.org/users/Some_User/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://example.org/users/Some_User#main-key", PublicKeyURI: "http://example.org/users/Some_User#main-key",
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
}, },
"remote_account_3": { "remote_account_3": {
ID: "062G5WYKY35KKD12EMSM3F8PJ8", ID: "062G5WYKY35KKD12EMSM3F8PJ8",
Username: "her_fuckin_maj", Username: "her_fuckin_maj",
Domain: "thequeenisstillalive.technology", Domain: "thequeenisstillalive.technology",
DisplayName: "lizzzieeeeeeeeeeee", DisplayName: "lizzzieeeeeeeeeeee",
Fields: []*gtsmodel.Field{},
Note: "if i die blame charles don't let that fuck become king", 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"), CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(true), Locked: util.Ptr(true),
Discoverable: util.Ptr(true), Discoverable: util.Ptr(true),
URI: "http://thequeenisstillalive.technology/users/her_fuckin_maj", URI: "http://thequeenisstillalive.technology/users/her_fuckin_maj",
URL: "http://thequeenisstillalive.technology/@her_fuckin_maj", URL: "http://thequeenisstillalive.technology/@her_fuckin_maj",
FetchedAt: time.Time{},
InboxURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/inbox", InboxURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/inbox",
SharedInboxURI: util.Ptr(""), SharedInboxURI: util.Ptr(""),
OutboxURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/outbox", OutboxURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/outbox",
FollowersURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/followers", FollowersURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/followers",
FollowingURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/following", FollowingURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/following",
FeaturedCollectionURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/collections/featured", FeaturedCollectionURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj#main-key", PublicKeyURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj#main-key",
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R", HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R",
}, },
"remote_account_4": { "remote_account_4": {
ID: "07GZRBAEMBNKGZ8Z9VSKSXKR98", ID: "07GZRBAEMBNKGZ8Z9VSKSXKR98",
Username: "üser", Username: "üser",
Domain: "xn--xample-ova.org", Domain: "xn--xample-ova.org",
DisplayName: "",
Note: "",
Memorial: util.Ptr(false),
MovedToURI: "",
CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"), CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Locked: util.Ptr(false), Locked: util.Ptr(false),
Discoverable: util.Ptr(false), Discoverable: util.Ptr(false),
URI: "https://xn--xample-ova.org/users/%C3%BCser", URI: "https://xn--xample-ova.org/users/%C3%BCser",
@ -650,15 +549,10 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
FollowersURI: "https://xn--xample-ova.org/users/%C3%BCser/followers", FollowersURI: "https://xn--xample-ova.org/users/%C3%BCser/followers",
FollowingURI: "https://xn--xample-ova.org/users/%C3%BCser/following", FollowingURI: "https://xn--xample-ova.org/users/%C3%BCser/following",
FeaturedCollectionURI: "https://xn--xample-ova.org/users/%C3%BCser/collections/featured", FeaturedCollectionURI: "https://xn--xample-ova.org/users/%C3%BCser/collections/featured",
ActorType: ap.ActorPerson, ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: &rsa.PrivateKey{}, PrivateKey: &rsa.PrivateKey{},
PublicKey: &rsa.PublicKey{}, PublicKey: &rsa.PublicKey{},
PublicKeyURI: "https://xn--xample-ova.org/users/%C3%BCser#main-key", PublicKeyURI: "https://xn--xample-ova.org/users/%C3%BCser#main-key",
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
SuspensionOrigin: "",
HeaderMediaAttachmentID: "",
}, },
} }