[feature] Show + federate emojis in accounts (#837)

* Start adding account emoji

* get emojis serialized + deserialized nicely

* update tests

* set / retrieve emojis on accounts

* show account emojis in web view

* fetch emojis from db based on ids

* fix typo in test

* lint

* fix pg migration

* update tests

* update emoji checking logic

* update comment

* clarify comments + add some spacing

* tidy up loops a lil (thanks kim)
This commit is contained in:
tobi 2022-09-26 11:56:01 +02:00 committed by GitHub
parent 15a67b7bef
commit c4a08292ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 934 additions and 127 deletions

View file

@ -41,6 +41,7 @@ type Accountable interface {
WithFeatured WithFeatured
WithManuallyApprovesFollowers WithManuallyApprovesFollowers
WithEndpoints WithEndpoints
WithTag
} }
// Statusable represents the minimum activitypub interface for representing a 'status'. // Statusable represents the minimum activitypub interface for representing a 'status'.

View file

@ -200,7 +200,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwoFields() { func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwoFields() {
// set up the request // set up the request
// we're updating the note of zork, and setting locked to true // we're updating the note of zork, and setting locked to true
newBio := "this is my new bio read it and weep" newBio := "this is my new bio read it and weep :rainbow:"
requestBody, w, err := testrig.CreateMultipartFormData( requestBody, w, err := testrig.CreateMultipartFormData(
"", "", "", "",
map[string]string{ map[string]string{
@ -235,9 +235,19 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo
// check the returned api model account // check the returned api model account
// fields should be updated // fields should be updated
suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note) suite.Equal("<p>this is my new bio read it and weep :rainbow:</p>", apimodelAccount.Note)
suite.Equal(newBio, apimodelAccount.Source.Note) suite.Equal(newBio, apimodelAccount.Source.Note)
suite.True(apimodelAccount.Locked) suite.True(apimodelAccount.Locked)
suite.NotEmpty(apimodelAccount.Emojis)
suite.Equal(apimodelAccount.Emojis[0].Shortcode, "rainbow")
// check the account in the database
dbZork, err := suite.db.GetAccountByID(context.Background(), apimodelAccount.ID)
suite.NoError(err)
suite.Equal(newBio, dbZork.NoteRaw)
suite.Equal("<p>this is my new bio read it and weep :rainbow:</p>", dbZork.Note)
suite.True(*dbZork.Locked)
suite.NotEmpty(dbZork.EmojiIDs)
} }
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWithMedia() { func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWithMedia() {

View file

@ -237,6 +237,8 @@ func (suite *InboxPostTestSuite) TestPostUnblock() {
func (suite *InboxPostTestSuite) TestPostUpdate() { func (suite *InboxPostTestSuite) TestPostUpdate() {
updatedAccount := *suite.testAccounts["remote_account_1"] updatedAccount := *suite.testAccounts["remote_account_1"]
updatedAccount.DisplayName = "updated display name!" updatedAccount.DisplayName = "updated display name!"
testEmoji := testrig.NewTestEmojis()["rainbow"]
updatedAccount.Emojis = []*gtsmodel.Emoji{testEmoji}
asAccount, err := suite.tc.AccountToAS(context.Background(), &updatedAccount) asAccount, err := suite.tc.AccountToAS(context.Background(), &updatedAccount)
suite.NoError(err) suite.NoError(err)
@ -288,6 +290,15 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
emailSender := testrig.NewEmailSender("../../../../web/template/", nil) emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
if err := processor.Start(); err != nil {
panic(err)
}
defer func() {
if err := processor.Stop(); err != nil {
panic(err)
}
}()
userModule := user.New(processor).(*user.Module) userModule := user.New(processor).(*user.Module)
// setup request // setup request
@ -322,11 +333,21 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.Equal(http.StatusOK, result.StatusCode) suite.Equal(http.StatusOK, result.StatusCode)
// account should be changed in the database now // account should be changed in the database now
dbUpdatedAccount, err := suite.db.GetAccountByID(context.Background(), updatedAccount.ID) var dbUpdatedAccount *gtsmodel.Account
suite.NoError(err)
if !testrig.WaitFor(func() bool {
// displayName should be updated // displayName should be updated
suite.Equal("updated display name!", dbUpdatedAccount.DisplayName) dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), updatedAccount.ID)
return dbUpdatedAccount.DisplayName == "updated display name!"
}) {
suite.FailNow("timed out waiting for account update")
}
// emojis should be updated
suite.Contains(dbUpdatedAccount.EmojiIDs, testEmoji.ID)
// account should be freshly webfingered
suite.WithinDuration(time.Now(), dbUpdatedAccount.LastWebfingeredAt, 10*time.Second)
// everything else should be the same as it was before // everything else should be the same as it was before
suite.EqualValues(updatedAccount.Username, dbUpdatedAccount.Username) suite.EqualValues(updatedAccount.Username, dbUpdatedAccount.Username)
@ -350,7 +371,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.EqualValues(updatedAccount.Language, dbUpdatedAccount.Language) suite.EqualValues(updatedAccount.Language, dbUpdatedAccount.Language)
suite.EqualValues(updatedAccount.URI, dbUpdatedAccount.URI) suite.EqualValues(updatedAccount.URI, dbUpdatedAccount.URI)
suite.EqualValues(updatedAccount.URL, dbUpdatedAccount.URL) suite.EqualValues(updatedAccount.URL, dbUpdatedAccount.URL)
suite.EqualValues(updatedAccount.LastWebfingeredAt, dbUpdatedAccount.LastWebfingeredAt)
suite.EqualValues(updatedAccount.InboxURI, dbUpdatedAccount.InboxURI) suite.EqualValues(updatedAccount.InboxURI, dbUpdatedAccount.InboxURI)
suite.EqualValues(updatedAccount.OutboxURI, dbUpdatedAccount.OutboxURI) suite.EqualValues(updatedAccount.OutboxURI, dbUpdatedAccount.OutboxURI)
suite.EqualValues(updatedAccount.FollowingURI, dbUpdatedAccount.FollowingURI) suite.EqualValues(updatedAccount.FollowingURI, dbUpdatedAccount.FollowingURI)

View file

@ -116,6 +116,8 @@ func copyAccount(account *gtsmodel.Account) *gtsmodel.Account {
HeaderMediaAttachment: nil, HeaderMediaAttachment: nil,
HeaderRemoteURL: account.HeaderRemoteURL, HeaderRemoteURL: account.HeaderRemoteURL,
DisplayName: account.DisplayName, DisplayName: account.DisplayName,
EmojiIDs: account.EmojiIDs,
Emojis: nil,
Fields: account.Fields, Fields: account.Fields,
Note: account.Note, Note: account.Note,
NoteRaw: account.NoteRaw, NoteRaw: account.NoteRaw,

View file

@ -42,6 +42,9 @@ type Account interface {
// 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), or an error if something goes wrong.
GetAccountByPubkeyID(ctx context.Context, id string) (*gtsmodel.Account, Error) GetAccountByPubkeyID(ctx context.Context, id string) (*gtsmodel.Account, Error)
// PutAccount puts one account in the database.
PutAccount(ctx context.Context, account *gtsmodel.Account) (*gtsmodel.Account, Error)
// UpdateAccount updates one account by ID. // UpdateAccount updates one account by ID.
UpdateAccount(ctx context.Context, account *gtsmodel.Account) (*gtsmodel.Account, Error) UpdateAccount(ctx context.Context, account *gtsmodel.Account) (*gtsmodel.Account, Error)

View file

@ -45,7 +45,8 @@ func (a *accountDB) newAccountQ(account *gtsmodel.Account) *bun.SelectQuery {
NewSelect(). NewSelect().
Model(account). Model(account).
Relation("AvatarMediaAttachment"). Relation("AvatarMediaAttachment").
Relation("HeaderMediaAttachment") Relation("HeaderMediaAttachment").
Relation("Emojis")
} }
func (a *accountDB) GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, db.Error) { func (a *accountDB) GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, db.Error) {
@ -138,24 +139,61 @@ func (a *accountDB) getAccount(ctx context.Context, cacheGet func() (*gtsmodel.A
return account, nil return account, nil
} }
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) (*gtsmodel.Account, db.Error) {
if err := a.conn.RunInTx(ctx, func(tx bun.Tx) error {
// create links between this account and any emojis it uses
for _, i := range account.EmojiIDs {
if _, err := tx.NewInsert().Model(&gtsmodel.AccountToEmoji{
AccountID: account.ID,
EmojiID: i,
}).Exec(ctx); err != nil {
return err
}
}
// insert the account
_, err := tx.NewInsert().Model(account).Exec(ctx)
return err
}); err != nil {
return nil, a.conn.ProcessError(err)
}
a.cache.Put(account)
return account, nil
}
func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account) (*gtsmodel.Account, db.Error) { func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account) (*gtsmodel.Account, db.Error) {
// Update the account's last-updated // Update the account's last-updated
account.UpdatedAt = time.Now() account.UpdatedAt = time.Now()
// Update the account model in the DB if err := a.conn.RunInTx(ctx, func(tx bun.Tx) error {
_, err := a.conn. // create links between this account and any emojis it uses
NewUpdate(). // first clear out any old emoji links
Model(account). if _, err := tx.NewDelete().
WherePK(). Model(&[]*gtsmodel.AccountToEmoji{}).
Exec(ctx) Where("account_id = ?", account.ID).
if err != nil { Exec(ctx); err != nil {
return err
}
// now populate new emoji links
for _, i := range account.EmojiIDs {
if _, err := tx.NewInsert().Model(&gtsmodel.AccountToEmoji{
AccountID: account.ID,
EmojiID: i,
}).Exec(ctx); err != nil {
return err
}
}
// update the account
_, err := tx.NewUpdate().Model(account).WherePK().Exec(ctx)
return err
}); err != nil {
return nil, a.conn.ProcessError(err) return nil, a.conn.ProcessError(err)
} }
// Place updated account in cache
// (this will replace existing, i.e. invalidating)
a.cache.Put(account) a.cache.Put(account)
return account, nil return account, nil
} }

View file

@ -27,7 +27,9 @@ 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/bundb"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun"
) )
type AccountTestSuite struct { type AccountTestSuite struct {
@ -71,17 +73,70 @@ func (suite *AccountTestSuite) TestGetAccountByUsernameDomain() {
} }
func (suite *AccountTestSuite) TestUpdateAccount() { func (suite *AccountTestSuite) TestUpdateAccount() {
ctx := context.Background()
testAccount := suite.testAccounts["local_account_1"] testAccount := suite.testAccounts["local_account_1"]
testAccount.DisplayName = "new display name!" testAccount.DisplayName = "new display name!"
testAccount.EmojiIDs = []string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}
_, err := suite.db.UpdateAccount(context.Background(), testAccount) _, err := suite.db.UpdateAccount(ctx, testAccount)
suite.NoError(err) suite.NoError(err)
updated, err := suite.db.GetAccountByID(context.Background(), testAccount.ID) updated, err := suite.db.GetAccountByID(ctx, testAccount.ID)
suite.NoError(err) suite.NoError(err)
suite.Equal("new display name!", updated.DisplayName) suite.Equal("new display name!", updated.DisplayName)
suite.Equal([]string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}, updated.EmojiIDs)
suite.WithinDuration(time.Now(), updated.UpdatedAt, 5*time.Second) 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{}
err = dbService.GetConn().
NewSelect().
Model(noCache).
Where("account.id = ?", bun.Ident(testAccount.ID)).
Relation("AvatarMediaAttachment").
Relation("HeaderMediaAttachment").
Relation("Emojis").
Scan(ctx)
suite.NoError(err)
suite.Equal("new display name!", noCache.DisplayName)
suite.Equal([]string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}, noCache.EmojiIDs)
suite.WithinDuration(time.Now(), noCache.UpdatedAt, 5*time.Second)
suite.NotNil(noCache.AvatarMediaAttachment)
suite.NotNil(noCache.HeaderMediaAttachment)
// update again to remove emoji associations
testAccount.EmojiIDs = []string{}
_, err = suite.db.UpdateAccount(ctx, testAccount)
suite.NoError(err)
updated, err = suite.db.GetAccountByID(ctx, testAccount.ID)
suite.NoError(err)
suite.Equal("new display name!", updated.DisplayName)
suite.Empty(updated.EmojiIDs)
suite.WithinDuration(time.Now(), updated.UpdatedAt, 5*time.Second)
err = dbService.GetConn().
NewSelect().
Model(noCache).
Where("account.id = ?", bun.Ident(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() {

View file

@ -67,12 +67,13 @@ const (
) )
var registerTables = []interface{}{ var registerTables = []interface{}{
&gtsmodel.AccountToEmoji{},
&gtsmodel.StatusToEmoji{}, &gtsmodel.StatusToEmoji{},
&gtsmodel.StatusToTag{}, &gtsmodel.StatusToTag{},
} }
// bunDBService satisfies the DB interface // DBService satisfies the DB interface
type bunDBService struct { type DBService struct {
db.Account db.Account
db.Admin db.Admin
db.Basic db.Basic
@ -89,6 +90,12 @@ type bunDBService struct {
conn *DBConn conn *DBConn
} }
// GetConn returns the underlying bun connection.
// Should only be used in testing + exceptional circumstance.
func (dbService *DBService) GetConn() *DBConn {
return dbService.conn
}
func doMigration(ctx context.Context, db *bun.DB) error { func doMigration(ctx context.Context, db *bun.DB) error {
migrator := migrate.NewMigrator(db, migrations.Migrations) migrator := migrate.NewMigrator(db, migrations.Migrations)
@ -177,7 +184,7 @@ func NewBunDBService(ctx context.Context) (db.DB, error) {
// Prepare domain block cache // Prepare domain block cache
blockCache := cache.NewDomainBlockCache() blockCache := cache.NewDomainBlockCache()
ps := &bunDBService{ ps := &DBService{
Account: accounts, Account: accounts,
Admin: &adminDB{ Admin: &adminDB{
conn: conn, conn: conn,
@ -399,7 +406,7 @@ func tweakConnectionValues(sqldb *sql.DB) {
CONVERSION FUNCTIONS CONVERSION FUNCTIONS
*/ */
func (ps *bunDBService) TagStringsToTags(ctx context.Context, tags []string, originAccountID string) ([]*gtsmodel.Tag, error) { func (dbService *DBService) TagStringsToTags(ctx context.Context, tags []string, originAccountID string) ([]*gtsmodel.Tag, error) {
protocol := config.GetProtocol() protocol := config.GetProtocol()
host := config.GetHost() host := config.GetHost()
@ -408,7 +415,7 @@ func (ps *bunDBService) TagStringsToTags(ctx context.Context, tags []string, ori
tag := &gtsmodel.Tag{} tag := &gtsmodel.Tag{}
// we can use selectorinsert here to create the new tag if it doesn't exist already // we can use selectorinsert here to create the new tag if it doesn't exist already
// inserted will be true if this is a new tag we just created // inserted will be true if this is a new tag we just created
if err := ps.conn.NewSelect().Model(tag).Where("LOWER(?) = LOWER(?)", bun.Ident("name"), t).Scan(ctx); err != nil { if err := dbService.conn.NewSelect().Model(tag).Where("LOWER(?) = LOWER(?)", bun.Ident("name"), t).Scan(ctx); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
// tag doesn't exist yet so populate it // tag doesn't exist yet so populate it
newID, err := id.NewRandomULID() newID, err := id.NewRandomULID()

View file

@ -0,0 +1,69 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
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"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
q := tx.NewAddColumn().Model(&gtsmodel.Account{})
switch tx.Dialect().Name() {
case dialect.PG:
q = q.ColumnExpr("? VARCHAR[]", bun.Ident("emojis"))
case dialect.SQLite:
q = q.ColumnExpr("? VARCHAR", bun.Ident("emojis"))
default:
log.Panic("db dialect was neither pg nor sqlite")
}
if _, err := q.Exec(ctx); err != nil {
return err
}
if _, err := tx.
NewCreateTable().
Model(&gtsmodel.AccountToEmoji{}).
IfNotExists().
Exec(ctx); 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

@ -76,6 +76,11 @@ type GetRemoteAccountParams struct {
// quickly fetch a remote account from the database or fail, and don't want to cause // quickly fetch a remote account from the database or fail, and don't want to cause
// http requests to go flying around. // http requests to go flying around.
SkipResolve bool SkipResolve bool
// PartialAccount can be used if the GetRemoteAccount call results from a federated/ap
// account update. In this case, we will already have a partial representation of the account,
// derived from converting the AP representation to a gtsmodel representation. If this field
// is provided, then GetRemoteAccount will use this as a basis for building the full account.
PartialAccount *gtsmodel.Account
} }
// GetRemoteAccount completely dereferences a remote account, converts it to a GtS model account, // GetRemoteAccount completely dereferences a remote account, converts it to a GtS model account,
@ -107,8 +112,16 @@ func (d *deref) GetRemoteAccount(ctx context.Context, params GetRemoteAccountPar
skipResolve := params.SkipResolve skipResolve := params.SkipResolve
// this first step checks if we have the // this first step checks if we have the
// account in the database somewhere already // account in the database somewhere already,
// or if we've been provided it as a partial
switch { switch {
case params.PartialAccount != nil:
foundAccount = params.PartialAccount
if foundAccount.Domain == "" || foundAccount.Domain == config.GetHost() || foundAccount.Domain == config.GetAccountDomain() {
// this is actually a local account,
// make sure we don't try to resolve
skipResolve = true
}
case params.RemoteAccountID != nil: case params.RemoteAccountID != nil:
uri := params.RemoteAccountID uri := params.RemoteAccountID
host := uri.Host host := uri.Host
@ -163,7 +176,7 @@ func (d *deref) GetRemoteAccount(ctx context.Context, params GetRemoteAccountPar
params.RemoteAccountHost = params.RemoteAccountID.Host params.RemoteAccountHost = params.RemoteAccountID.Host
// ... but we still need the username so we can do a finger for the accountDomain // ... but we still need the username so we can do a finger for the accountDomain
// check if we had the account stored already and got it earlier // check if we got the account earlier
if foundAccount != nil { if foundAccount != nil {
params.RemoteAccountUsername = foundAccount.Username params.RemoteAccountUsername = foundAccount.Username
} else { } else {
@ -201,9 +214,10 @@ func (d *deref) GetRemoteAccount(ctx context.Context, params GetRemoteAccountPar
// to save on remote calls, only webfinger if: // to save on remote calls, only webfinger if:
// - we don't know the remote account ActivityPub ID yet OR // - we don't know the remote account ActivityPub ID yet OR
// - we haven't found the account yet in some other way OR // - we haven't found the account yet in some other way OR
// - we were passed a partial account in params OR
// - we haven't webfingered the account for two days AND the account isn't an instance account // - we haven't webfingered the account for two days AND the account isn't an instance account
var fingered time.Time var fingered time.Time
if params.RemoteAccountID == nil || foundAccount == nil || (foundAccount.LastWebfingeredAt.Before(time.Now().Add(webfingerInterval)) && !instanceAccount(foundAccount)) { if params.RemoteAccountID == nil || foundAccount == nil || params.PartialAccount != nil || (foundAccount.LastWebfingeredAt.Before(time.Now().Add(webfingerInterval)) && !instanceAccount(foundAccount)) {
accountDomain, params.RemoteAccountID, err = d.fingerRemoteAccount(ctx, params.RequestingUsername, params.RemoteAccountUsername, params.RemoteAccountHost) accountDomain, params.RemoteAccountID, err = d.fingerRemoteAccount(ctx, params.RequestingUsername, params.RemoteAccountUsername, params.RemoteAccountHost)
if err != nil { if err != nil {
err = fmt.Errorf("GetRemoteAccount: error while fingering: %s", err) err = fmt.Errorf("GetRemoteAccount: error while fingering: %s", err)
@ -263,7 +277,7 @@ func (d *deref) GetRemoteAccount(ctx context.Context, params GetRemoteAccountPar
foundAccount.LastWebfingeredAt = fingered foundAccount.LastWebfingeredAt = fingered
foundAccount.UpdatedAt = time.Now() foundAccount.UpdatedAt = time.Now()
err = d.db.Put(ctx, foundAccount) foundAccount, err = d.db.PutAccount(ctx, foundAccount)
if err != nil { if err != nil {
err = fmt.Errorf("GetRemoteAccount: error putting new account: %s", err) err = fmt.Errorf("GetRemoteAccount: error putting new account: %s", err)
return return
@ -273,13 +287,10 @@ func (d *deref) GetRemoteAccount(ctx context.Context, params GetRemoteAccountPar
} }
// we had the account already, but now we know the account domain, so update it if it's different // we had the account already, but now we know the account domain, so update it if it's different
var accountDomainChanged bool
if !strings.EqualFold(foundAccount.Domain, accountDomain) { if !strings.EqualFold(foundAccount.Domain, accountDomain) {
accountDomainChanged = true
foundAccount.Domain = accountDomain foundAccount.Domain = accountDomain
foundAccount, err = d.db.UpdateAccount(ctx, foundAccount)
if err != nil {
err = fmt.Errorf("GetRemoteAccount: error updating account: %s", err)
return
}
} }
// if SharedInboxURI is nil, that means we don't know yet if this account has // if SharedInboxURI is nil, that means we don't know yet if this account has
@ -327,8 +338,7 @@ func (d *deref) GetRemoteAccount(ctx context.Context, params GetRemoteAccountPar
foundAccount.LastWebfingeredAt = fingered foundAccount.LastWebfingeredAt = fingered
} }
if fieldsChanged || fingeredChanged || sharedInboxChanged { if accountDomainChanged || sharedInboxChanged || fieldsChanged || fingeredChanged {
foundAccount.UpdatedAt = time.Now()
foundAccount, err = d.db.UpdateAccount(ctx, foundAccount) foundAccount, err = d.db.UpdateAccount(ctx, foundAccount)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetRemoteAccount: error updating remoteAccount: %s", err) return nil, fmt.Errorf("GetRemoteAccount: error updating remoteAccount: %s", err)
@ -423,15 +433,20 @@ func (d *deref) populateAccountFields(ctx context.Context, account *gtsmodel.Acc
return false, fmt.Errorf("populateAccountFields: domain %s is blocked", accountURI.Host) return false, fmt.Errorf("populateAccountFields: domain %s is blocked", accountURI.Host)
} }
t, err := d.transportController.NewTransportForUsername(ctx, requestingUsername) var changed bool
if err != nil {
return false, fmt.Errorf("populateAccountFields: error getting transport for user: %s", err)
}
// fetch the header and avatar // fetch the header and avatar
changed, err := d.fetchRemoteAccountMedia(ctx, account, t, blocking) if mediaChanged, err := d.fetchRemoteAccountMedia(ctx, account, requestingUsername, blocking); err != nil {
if err != nil {
return false, fmt.Errorf("populateAccountFields: error fetching header/avi for account: %s", err) return false, fmt.Errorf("populateAccountFields: error fetching header/avi for account: %s", err)
} else if mediaChanged {
changed = mediaChanged
}
// fetch any emojis used in note, fields, display name, etc
if emojisChanged, err := d.fetchRemoteAccountEmojis(ctx, account, requestingUsername); err != nil {
return false, fmt.Errorf("populateAccountFields: error fetching emojis for account: %s", err)
} else if emojisChanged {
changed = emojisChanged
} }
return changed, nil return changed, nil
@ -449,17 +464,11 @@ func (d *deref) populateAccountFields(ctx context.Context, account *gtsmodel.Acc
// //
// If blocking is true, then the calls to the media manager made by this function will be blocking: // If blocking is true, then the calls to the media manager made by this function will be blocking:
// in other words, the function won't return until the header and the avatar have been fully processed. // in other words, the function won't return until the header and the avatar have been fully processed.
func (d *deref) fetchRemoteAccountMedia(ctx context.Context, targetAccount *gtsmodel.Account, t transport.Transport, blocking bool) (bool, error) { func (d *deref) fetchRemoteAccountMedia(ctx context.Context, targetAccount *gtsmodel.Account, requestingUsername string, blocking bool) (bool, error) {
changed := false var (
changed bool
accountURI, err := url.Parse(targetAccount.URI) t transport.Transport
if err != nil { )
return changed, fmt.Errorf("fetchRemoteAccountMedia: couldn't parse account URI %s: %s", targetAccount.URI, err)
}
if blocked, err := d.db.IsDomainBlocked(ctx, accountURI.Host); blocked || err != nil {
return changed, fmt.Errorf("fetchRemoteAccountMedia: domain %s is blocked", accountURI.Host)
}
if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "") { if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "") {
var processingMedia *media.ProcessingMedia var processingMedia *media.ProcessingMedia
@ -479,6 +488,14 @@ func (d *deref) fetchRemoteAccountMedia(ctx context.Context, targetAccount *gtsm
return changed, err return changed, err
} }
if t == nil {
var err error
t, err = d.transportController.NewTransportForUsername(ctx, requestingUsername)
if err != nil {
return false, fmt.Errorf("fetchRemoteAccountMedia: error getting transport for user: %s", err)
}
}
data := func(innerCtx context.Context) (io.Reader, int, error) { data := func(innerCtx context.Context) (io.Reader, int, error) {
return t.DereferenceMedia(innerCtx, avatarIRI) return t.DereferenceMedia(innerCtx, avatarIRI)
} }
@ -537,6 +554,14 @@ func (d *deref) fetchRemoteAccountMedia(ctx context.Context, targetAccount *gtsm
return changed, err return changed, err
} }
if t == nil {
var err error
t, err = d.transportController.NewTransportForUsername(ctx, requestingUsername)
if err != nil {
return false, fmt.Errorf("fetchRemoteAccountMedia: error getting transport for user: %s", err)
}
}
data := func(innerCtx context.Context) (io.Reader, int, error) { data := func(innerCtx context.Context) (io.Reader, int, error) {
return t.DereferenceMedia(innerCtx, headerIRI) return t.DereferenceMedia(innerCtx, headerIRI)
} }
@ -580,6 +605,118 @@ func (d *deref) fetchRemoteAccountMedia(ctx context.Context, targetAccount *gtsm
return changed, nil return changed, nil
} }
func (d *deref) fetchRemoteAccountEmojis(ctx context.Context, targetAccount *gtsmodel.Account, requestingUsername string) (bool, error) {
maybeEmojis := targetAccount.Emojis
maybeEmojiIDs := targetAccount.EmojiIDs
// It's possible that the account had emoji IDs set on it, but not Emojis
// themselves, depending on how it was fetched before being passed to us.
//
// If we only have IDs, fetch the emojis from the db. We know they're in
// there or else they wouldn't have IDs.
if len(maybeEmojiIDs) > len(maybeEmojis) {
maybeEmojis = []*gtsmodel.Emoji{}
for _, emojiID := range maybeEmojiIDs {
maybeEmoji, err := d.db.GetEmojiByID(ctx, emojiID)
if err != nil {
return false, err
}
maybeEmojis = append(maybeEmojis, maybeEmoji)
}
}
// For all the maybe emojis we have, we either fetch them from the database
// (if we haven't already), or dereference them from the remote instance.
gotEmojis, err := d.populateEmojis(ctx, maybeEmojis, requestingUsername)
if err != nil {
return false, err
}
// Extract the ID of each fetched or dereferenced emoji, so we can attach
// this to the account if necessary.
gotEmojiIDs := make([]string, 0, len(gotEmojis))
for _, e := range gotEmojis {
gotEmojiIDs = append(gotEmojiIDs, e.ID)
}
var (
changed = false // have the emojis for this account changed?
maybeLen = len(maybeEmojis)
gotLen = len(gotEmojis)
)
// if the length of everything is zero, this is simple:
// nothing has changed and there's nothing to do
if maybeLen == 0 && gotLen == 0 {
return changed, nil
}
// if the *amount* of emojis on the account has changed, then the got emojis
// are definitely different from the previous ones (if there were any) --
// the account has either more or fewer emojis set on it now, so take the
// discovered emojis as the new correct ones.
if maybeLen != gotLen {
changed = true
targetAccount.Emojis = gotEmojis
targetAccount.EmojiIDs = gotEmojiIDs
return changed, nil
}
// if the lengths are the same but not all of the slices are
// zero, something *might* have changed, so we have to check
// 1. did we have emojis before that we don't have now?
for _, maybeEmoji := range maybeEmojis {
var stillPresent bool
for _, gotEmoji := range gotEmojis {
if maybeEmoji.URI == gotEmoji.URI {
// the emoji we maybe had is still present now,
// so we can stop checking gotEmojis
stillPresent = true
break
}
}
if !stillPresent {
// at least one maybeEmoji is no longer present in
// the got emojis, so we can stop checking now
changed = true
targetAccount.Emojis = gotEmojis
targetAccount.EmojiIDs = gotEmojiIDs
return changed, nil
}
}
// 2. do we have emojis now that we didn't have before?
for _, gotEmoji := range gotEmojis {
var wasPresent bool
for _, maybeEmoji := range maybeEmojis {
// check emoji IDs here as well, because unreferenced
// maybe emojis we didn't already have would not have
// had IDs set on them yet
if gotEmoji.URI == maybeEmoji.URI && gotEmoji.ID == maybeEmoji.ID {
// this got emoji was present already in the maybeEmoji,
// so we can stop checking through maybeEmojis
wasPresent = true
break
}
}
if !wasPresent {
// at least one gotEmojis was not present in
// the maybeEmojis, so we can stop checking now
changed = true
targetAccount.Emojis = gotEmojis
targetAccount.EmojiIDs = gotEmojiIDs
return changed, nil
}
}
return changed, nil
}
func lockAndLoad(ctx context.Context, lock *sync.Mutex, processing *media.ProcessingMedia, processingMap map[string]*media.ProcessingMedia, accountID string) error { func lockAndLoad(ctx context.Context, lock *sync.Mutex, processing *media.ProcessingMedia, processingMap map[string]*media.ProcessingMedia, accountID string) error {
// whatever happens, remove the in-process media from the map // whatever happens, remove the in-process media from the map
defer func() { defer func() {

View file

@ -27,6 +27,7 @@ import (
"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/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -195,6 +196,205 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() {
suite.Nil(fetchedAccount) suite.Nil(fetchedAccount)
} }
func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithPartial() {
fetchingAccount := suite.testAccounts["local_account_1"]
remoteAccount := suite.testAccounts["remote_account_1"]
remoteAccountPartial := &gtsmodel.Account{
ID: remoteAccount.ID,
ActorType: remoteAccount.ActorType,
Language: remoteAccount.Language,
CreatedAt: remoteAccount.CreatedAt,
UpdatedAt: remoteAccount.UpdatedAt,
Username: remoteAccount.Username,
Domain: remoteAccount.Domain,
DisplayName: remoteAccount.DisplayName,
URI: remoteAccount.URI,
InboxURI: remoteAccount.URI,
SharedInboxURI: remoteAccount.SharedInboxURI,
PublicKeyURI: remoteAccount.PublicKeyURI,
URL: remoteAccount.URL,
FollowingURI: remoteAccount.FollowingURI,
FollowersURI: remoteAccount.FollowersURI,
OutboxURI: remoteAccount.OutboxURI,
FeaturedCollectionURI: remoteAccount.FeaturedCollectionURI,
Emojis: []*gtsmodel.Emoji{
// dereference an emoji we don't have stored yet
{
URI: "http://fossbros-anonymous.io/emoji/01GD5HCC2YECT012TK8PAGX4D1",
Shortcode: "kip_van_den_bos",
UpdatedAt: testrig.TimeMustParse("2022-09-13T12:13:12+02:00"),
ImageRemoteURL: "http://fossbros-anonymous.io/emoji/kip.gif",
Disabled: testrig.FalseBool(),
VisibleInPicker: testrig.FalseBool(),
Domain: "fossbros-anonymous.io",
},
},
}
fetchedAccount, err := suite.dereferencer.GetRemoteAccount(context.Background(), dereferencing.GetRemoteAccountParams{
RequestingUsername: fetchingAccount.Username,
RemoteAccountID: testrig.URLMustParse(remoteAccount.URI),
RemoteAccountHost: remoteAccount.Domain,
RemoteAccountUsername: remoteAccount.Username,
PartialAccount: remoteAccountPartial,
Blocking: true,
})
suite.NoError(err)
suite.NotNil(fetchedAccount)
suite.NotNil(fetchedAccount.EmojiIDs)
suite.NotNil(fetchedAccount.Emojis)
}
func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithPartial2() {
fetchingAccount := suite.testAccounts["local_account_1"]
knownEmoji := suite.testEmojis["yell"]
remoteAccount := suite.testAccounts["remote_account_1"]
remoteAccountPartial := &gtsmodel.Account{
ID: remoteAccount.ID,
ActorType: remoteAccount.ActorType,
Language: remoteAccount.Language,
CreatedAt: remoteAccount.CreatedAt,
UpdatedAt: remoteAccount.UpdatedAt,
Username: remoteAccount.Username,
Domain: remoteAccount.Domain,
DisplayName: remoteAccount.DisplayName,
URI: remoteAccount.URI,
InboxURI: remoteAccount.URI,
SharedInboxURI: remoteAccount.SharedInboxURI,
PublicKeyURI: remoteAccount.PublicKeyURI,
URL: remoteAccount.URL,
FollowingURI: remoteAccount.FollowingURI,
FollowersURI: remoteAccount.FollowersURI,
OutboxURI: remoteAccount.OutboxURI,
FeaturedCollectionURI: remoteAccount.FeaturedCollectionURI,
Emojis: []*gtsmodel.Emoji{
// an emoji we already have
{
URI: knownEmoji.URI,
Shortcode: knownEmoji.Shortcode,
UpdatedAt: knownEmoji.CreatedAt,
ImageRemoteURL: knownEmoji.ImageRemoteURL,
Disabled: knownEmoji.Disabled,
VisibleInPicker: knownEmoji.VisibleInPicker,
},
},
}
fetchedAccount, err := suite.dereferencer.GetRemoteAccount(context.Background(), dereferencing.GetRemoteAccountParams{
RequestingUsername: fetchingAccount.Username,
RemoteAccountID: testrig.URLMustParse(remoteAccount.URI),
RemoteAccountHost: remoteAccount.Domain,
RemoteAccountUsername: remoteAccount.Username,
PartialAccount: remoteAccountPartial,
Blocking: true,
})
suite.NoError(err)
suite.NotNil(fetchedAccount)
suite.NotNil(fetchedAccount.EmojiIDs)
suite.NotNil(fetchedAccount.Emojis)
}
func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithPartial3() {
fetchingAccount := suite.testAccounts["local_account_1"]
knownEmoji := suite.testEmojis["yell"]
remoteAccount := suite.testAccounts["remote_account_1"]
remoteAccountPartial := &gtsmodel.Account{
ID: remoteAccount.ID,
ActorType: remoteAccount.ActorType,
Language: remoteAccount.Language,
CreatedAt: remoteAccount.CreatedAt,
UpdatedAt: remoteAccount.UpdatedAt,
Username: remoteAccount.Username,
Domain: remoteAccount.Domain,
DisplayName: remoteAccount.DisplayName,
URI: remoteAccount.URI,
InboxURI: remoteAccount.URI,
SharedInboxURI: remoteAccount.SharedInboxURI,
PublicKeyURI: remoteAccount.PublicKeyURI,
URL: remoteAccount.URL,
FollowingURI: remoteAccount.FollowingURI,
FollowersURI: remoteAccount.FollowersURI,
OutboxURI: remoteAccount.OutboxURI,
FeaturedCollectionURI: remoteAccount.FeaturedCollectionURI,
Emojis: []*gtsmodel.Emoji{
// an emoji we already have
{
URI: knownEmoji.URI,
Shortcode: knownEmoji.Shortcode,
UpdatedAt: knownEmoji.CreatedAt,
ImageRemoteURL: knownEmoji.ImageRemoteURL,
Disabled: knownEmoji.Disabled,
VisibleInPicker: knownEmoji.VisibleInPicker,
},
},
}
fetchedAccount, err := suite.dereferencer.GetRemoteAccount(context.Background(), dereferencing.GetRemoteAccountParams{
RequestingUsername: fetchingAccount.Username,
RemoteAccountID: testrig.URLMustParse(remoteAccount.URI),
RemoteAccountHost: remoteAccount.Domain,
RemoteAccountUsername: remoteAccount.Username,
PartialAccount: remoteAccountPartial,
Blocking: true,
})
suite.NoError(err)
suite.NotNil(fetchedAccount)
suite.NotNil(fetchedAccount.EmojiIDs)
suite.NotNil(fetchedAccount.Emojis)
suite.Equal(knownEmoji.URI, fetchedAccount.Emojis[0].URI)
remoteAccountPartial2 := &gtsmodel.Account{
ID: remoteAccount.ID,
ActorType: remoteAccount.ActorType,
Language: remoteAccount.Language,
CreatedAt: remoteAccount.CreatedAt,
UpdatedAt: remoteAccount.UpdatedAt,
Username: remoteAccount.Username,
Domain: remoteAccount.Domain,
DisplayName: remoteAccount.DisplayName,
URI: remoteAccount.URI,
InboxURI: remoteAccount.URI,
SharedInboxURI: remoteAccount.SharedInboxURI,
PublicKeyURI: remoteAccount.PublicKeyURI,
URL: remoteAccount.URL,
FollowingURI: remoteAccount.FollowingURI,
FollowersURI: remoteAccount.FollowersURI,
OutboxURI: remoteAccount.OutboxURI,
FeaturedCollectionURI: remoteAccount.FeaturedCollectionURI,
Emojis: []*gtsmodel.Emoji{
// dereference an emoji we don't have stored yet
{
URI: "http://fossbros-anonymous.io/emoji/01GD5HCC2YECT012TK8PAGX4D1",
Shortcode: "kip_van_den_bos",
UpdatedAt: testrig.TimeMustParse("2022-09-13T12:13:12+02:00"),
ImageRemoteURL: "http://fossbros-anonymous.io/emoji/kip.gif",
Disabled: testrig.FalseBool(),
VisibleInPicker: testrig.FalseBool(),
Domain: "fossbros-anonymous.io",
},
},
}
fetchedAccount2, err := suite.dereferencer.GetRemoteAccount(context.Background(), dereferencing.GetRemoteAccountParams{
RequestingUsername: fetchingAccount.Username,
RemoteAccountID: testrig.URLMustParse(remoteAccount.URI),
RemoteAccountHost: remoteAccount.Domain,
RemoteAccountUsername: remoteAccount.Username,
PartialAccount: remoteAccountPartial2,
Blocking: true,
})
suite.NoError(err)
suite.NotNil(fetchedAccount2)
suite.NotNil(fetchedAccount2.EmojiIDs)
suite.NotNil(fetchedAccount2.Emojis)
suite.Equal("http://fossbros-anonymous.io/emoji/01GD5HCC2YECT012TK8PAGX4D1", fetchedAccount2.Emojis[0].URI)
}
func TestAccountTestSuite(t *testing.T) { func TestAccountTestSuite(t *testing.T) {
suite.Run(t, new(AccountTestSuite)) suite.Run(t, new(AccountTestSuite))
} }

View file

@ -41,6 +41,7 @@ type DereferencerStandardTestSuite struct {
testRemoteServices map[string]vocab.ActivityStreamsService testRemoteServices map[string]vocab.ActivityStreamsService
testRemoteAttachments map[string]testrig.RemoteAttachmentFile testRemoteAttachments map[string]testrig.RemoteAttachmentFile
testAccounts map[string]*gtsmodel.Account testAccounts map[string]*gtsmodel.Account
testEmojis map[string]*gtsmodel.Emoji
dereferencer dereferencing.Dereferencer dereferencer dereferencing.Dereferencer
} }
@ -55,6 +56,7 @@ func (suite *DereferencerStandardTestSuite) SetupTest() {
suite.testRemoteGroups = testrig.NewTestFediGroups() suite.testRemoteGroups = testrig.NewTestFediGroups()
suite.testRemoteServices = testrig.NewTestFediServices() suite.testRemoteServices = testrig.NewTestFediServices()
suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media") suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media")
suite.testEmojis = testrig.NewTestEmojis()
suite.db = testrig.NewTestDB() suite.db = testrig.NewTestDB()
suite.storage = testrig.NewInMemoryStorage() suite.storage = testrig.NewInMemoryStorage()

View file

@ -24,6 +24,10 @@ import (
"io" "io"
"net/url" "net/url"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/media"
) )
@ -49,3 +53,57 @@ func (d *deref) GetRemoteEmoji(ctx context.Context, requestingUsername string, r
return processingMedia, nil return processingMedia, nil
} }
func (d *deref) populateEmojis(ctx context.Context, rawEmojis []*gtsmodel.Emoji, requestingUsername string) ([]*gtsmodel.Emoji, error) {
// At this point we should know:
// * the AP uri of the emoji
// * the domain of the emoji
// * the shortcode of the emoji
// * the remote URL of the image
// This should be enough to dereference the emoji
gotEmojis := make([]*gtsmodel.Emoji, 0, len(rawEmojis))
for _, e := range rawEmojis {
var gotEmoji *gtsmodel.Emoji
var err error
// check if we've already got this emoji in the db
if gotEmoji, err = d.db.GetEmojiByURI(ctx, e.URI); err != nil && err != db.ErrNoEntries {
log.Errorf("populateEmojis: error checking database for emoji %s: %s", e.URI, err)
continue
}
if gotEmoji == nil {
// it's new! go get it!
newEmojiID, err := id.NewRandomULID()
if err != nil {
log.Errorf("populateEmojis: error generating id for remote emoji %s: %s", e.URI, err)
continue
}
processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, newEmojiID, e.URI, &media.AdditionalEmojiInfo{
Domain: &e.Domain,
ImageRemoteURL: &e.ImageRemoteURL,
ImageStaticRemoteURL: &e.ImageRemoteURL,
Disabled: e.Disabled,
VisibleInPicker: e.VisibleInPicker,
})
if err != nil {
log.Errorf("populateEmojis: couldn't get remote emoji %s: %s", e.URI, err)
continue
}
if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
log.Errorf("populateEmojis: couldn't load remote emoji %s: %s", e.URI, err)
continue
}
}
// if we get here, we either had the emoji already or we successfully fetched it
gotEmojis = append(gotEmojis, gotEmoji)
}
return gotEmojis, nil
}

View file

@ -406,58 +406,17 @@ func (d *deref) populateStatusAttachments(ctx context.Context, status *gtsmodel.
} }
func (d *deref) populateStatusEmojis(ctx context.Context, status *gtsmodel.Status, requestingUsername string) error { func (d *deref) populateStatusEmojis(ctx context.Context, status *gtsmodel.Status, requestingUsername string) error {
// At this point we should know: emojis, err := d.populateEmojis(ctx, status.Emojis, requestingUsername)
// * the AP uri of the emoji
// * the domain of the emoji
// * the shortcode of the emoji
// * the remote URL of the image
// This should be enough to dereference the emoji
gotEmojis := make([]*gtsmodel.Emoji, 0, len(status.Emojis))
emojiIDs := make([]string, 0, len(status.Emojis))
for _, e := range status.Emojis {
var gotEmoji *gtsmodel.Emoji
var err error
// check if we've already got this emoji in the db
if gotEmoji, err = d.db.GetEmojiByURI(ctx, e.URI); err != nil && err != db.ErrNoEntries {
log.Errorf("populateStatusEmojis: error checking database for emoji %s: %s", e.URI, err)
continue
}
if gotEmoji == nil {
// it's new! go get it!
newEmojiID, err := id.NewRandomULID()
if err != nil { if err != nil {
log.Errorf("populateStatusEmojis: error generating id for remote emoji %s: %s", e.URI, err) return err
continue
} }
processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, newEmojiID, e.URI, &media.AdditionalEmojiInfo{ emojiIDs := make([]string, 0, len(emojis))
Domain: &e.Domain, for _, e := range emojis {
ImageRemoteURL: &e.ImageRemoteURL, emojiIDs = append(emojiIDs, e.ID)
ImageStaticRemoteURL: &e.ImageRemoteURL,
Disabled: e.Disabled,
VisibleInPicker: e.VisibleInPicker,
})
if err != nil {
log.Errorf("populateStatusEmojis: couldn't get remote emoji %s: %s", e.URI, err)
continue
} }
if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil { status.Emojis = emojis
log.Errorf("populateStatusEmojis: couldn't load remote emoji %s: %s", e.URI, err)
continue
}
}
// if we get here, we either had the emoji already or we successfully fetched it
gotEmojis = append(gotEmojis, gotEmoji)
emojiIDs = append(emojiIDs, gotEmoji.ID)
}
status.Emojis = gotEmojis
status.EmojiIDs = emojiIDs status.EmojiIDs = emojiIDs
return nil return nil
} }

View file

@ -121,7 +121,7 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
return fmt.Errorf("UPDATE: error converting to account: %s", err) return fmt.Errorf("UPDATE: error converting to account: %s", err)
} }
if updatedAcct.Domain == config.GetHost() { if updatedAcct.Domain == config.GetHost() || updatedAcct.Domain == config.GetAccountDomain() {
// no need to update local accounts // no need to update local accounts
// in fact, if we do this will break the shit out of things so do NOT // in fact, if we do this will break the shit out of things so do NOT
return nil return nil
@ -136,13 +136,8 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
updatedAcct.ID = requestingAcct.ID updatedAcct.ID = requestingAcct.ID
updatedAcct.Language = requestingAcct.Language updatedAcct.Language = requestingAcct.Language
// do the update // pass to the processor for further updating of eg., avatar/header, emojis
updatedAcct, err = f.db.UpdateAccount(ctx, updatedAcct) // the actual db insert/update will take place a bit later
if err != nil {
return fmt.Errorf("UPDATE: database error inserting updated account: %s", err)
}
// pass to the processor for further processing of eg., avatar/header
f.fedWorker.Queue(messages.FromFederator{ f.fedWorker.Queue(messages.FromFederator{
APObjectType: ap.ObjectProfile, APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityUpdate, APActivityType: ap.ActivityUpdate,

View file

@ -41,6 +41,8 @@ type Account struct {
HeaderMediaAttachment *MediaAttachment `validate:"-" bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID HeaderMediaAttachment *MediaAttachment `validate:"-" bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID
HeaderRemoteURL string `validate:"omitempty,url" bun:",nullzero"` // For a non-local account, where can the header be fetched? HeaderRemoteURL string `validate:"omitempty,url" bun:",nullzero"` // For a non-local account, where can the header be fetched?
DisplayName string `validate:"-" bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. DisplayName string `validate:"-" bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
EmojiIDs []string `validate:"dive,ulid" bun:"emojis,array"` // Database IDs of any emojis used in this account's bio, display name, etc
Emojis []*Emoji `validate:"-" bun:"attached_emojis,m2m:account_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
Fields []Field `validate:"-"` // a key/value map of fields that this account has added to their profile Fields []Field `validate:"-"` // a key/value map of fields that this account has added to their profile
Note string `validate:"-" bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves) Note string `validate:"-" bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves)
NoteRaw string `validate:"-" bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target NoteRaw string `validate:"-" bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target
@ -76,6 +78,14 @@ type Account struct {
SuspensionOrigin string `validate:"omitempty,ulid" 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 SuspensionOrigin string `validate:"omitempty,ulid" 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
} }
// AccountToEmoji is an intermediate struct to facilitate the many2many relationship between an account and one or more emojis.
type AccountToEmoji struct {
AccountID string `validate:"ulid,required" bun:"type:CHAR(26),unique:accountemoji,nullzero,notnull"`
Account *Account `validate:"-" bun:"rel:belongs-to"`
EmojiID string `validate:"ulid,required" bun:"type:CHAR(26),unique:accountemoji,nullzero,notnull"`
Emoji *Emoji `validate:"-" bun:"rel:belongs-to"`
}
// Field represents a key value field on an account, for things like pronouns, website, etc. // Field represents a key value field on an account, for things like pronouns, website, etc.
// VerifiedAt is optional, to be used only if Value is a URL to a webpage that contains the // VerifiedAt is optional, to be used only if Value is a URL to a webpage that contains the
// username of the user. // username of the user.

View file

@ -259,6 +259,8 @@ selectStatusesLoop:
account.HeaderMediaAttachmentID = "" account.HeaderMediaAttachmentID = ""
account.HeaderRemoteURL = "" account.HeaderRemoteURL = ""
account.Reason = "" account.Reason = ""
account.Emojis = []*gtsmodel.Emoji{}
account.EmojiIDs = []string{}
account.Fields = []gtsmodel.Field{} account.Fields = []gtsmodel.Field{}
hideCollections := true hideCollections := true
account.HideCollections = &hideCollections account.HideCollections = &hideCollections

View file

@ -27,6 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/log"
@ -46,11 +47,14 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
account.Bot = form.Bot account.Bot = form.Bot
} }
var updateEmojis bool
if form.DisplayName != nil { if form.DisplayName != nil {
if err := validate.DisplayName(*form.DisplayName); err != nil { if err := validate.DisplayName(*form.DisplayName); err != nil {
return nil, gtserror.NewErrorBadRequest(err) return nil, gtserror.NewErrorBadRequest(err)
} }
account.DisplayName = text.SanitizePlaintext(*form.DisplayName) account.DisplayName = text.SanitizePlaintext(*form.DisplayName)
updateEmojis = true
} }
if form.Note != nil { if form.Note != nil {
@ -69,6 +73,30 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
// Set updated HTML-ified note // Set updated HTML-ified note
account.Note = note account.Note = note
updateEmojis = true
}
if updateEmojis {
// account emojis -- treat the sanitized display name and raw
// note like one long text for the purposes of deriving emojis
accountEmojiShortcodes := util.DeriveEmojisFromText(account.DisplayName + "\n\n" + account.NoteRaw)
account.Emojis = make([]*gtsmodel.Emoji, 0, len(accountEmojiShortcodes))
account.EmojiIDs = make([]string, 0, len(accountEmojiShortcodes))
for _, shortcode := range accountEmojiShortcodes {
emoji, err := p.db.GetEmojiByShortcodeDomain(ctx, shortcode, "")
if err != nil {
if err != db.ErrNoEntries {
log.Errorf("error getting local emoji with shortcode %s: %s", shortcode, err)
}
continue
}
if *emoji.VisibleInPicker && !*emoji.Disabled {
account.Emojis = append(account.Emojis, emoji)
account.EmojiIDs = append(account.EmojiIDs, emoji.ID)
}
}
} }
if form.Avatar != nil && form.Avatar.Size != 0 { if form.Avatar != nil && form.Avatar.Size != 0 {

View file

@ -369,9 +369,13 @@ func (p *processor) processUpdateAccountFromFederator(ctx context.Context, feder
return err return err
} }
// further database updates occur inside getremoteaccount
if _, err := p.federator.GetRemoteAccount(ctx, dereferencing.GetRemoteAccountParams{ if _, err := p.federator.GetRemoteAccount(ctx, dereferencing.GetRemoteAccountParams{
RequestingUsername: federatorMsg.ReceivingAccount.Username, RequestingUsername: federatorMsg.ReceivingAccount.Username,
RemoteAccountID: incomingAccountURL, RemoteAccountID: incomingAccountURL,
RemoteAccountHost: incomingAccount.Domain,
RemoteAccountUsername: incomingAccount.Username,
PartialAccount: incomingAccount,
Blocking: true, Blocking: true,
}); err != nil { }); err != nil {
return fmt.Errorf("error enriching updated account from federator: %s", err) return fmt.Errorf("error enriching updated account from federator: %s", err)

View file

@ -88,6 +88,13 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a
acct.DisplayName = displayName acct.DisplayName = displayName
} }
// account emojis (used in bio, display name, fields)
if emojis, err := ap.ExtractEmojis(accountable); err != nil {
log.Infof("ASRepresentationToAccount: error extracting account emojis: %s", err)
} else {
acct.Emojis = emojis
}
// TODO: fields aka attachment array // TODO: fields aka attachment array
// note aka summary // note aka summary

View file

@ -473,6 +473,7 @@ type TypeUtilsTestSuite struct {
testAccounts map[string]*gtsmodel.Account testAccounts map[string]*gtsmodel.Account
testStatuses map[string]*gtsmodel.Status testStatuses map[string]*gtsmodel.Status
testPeople map[string]vocab.ActivityStreamsPerson testPeople map[string]vocab.ActivityStreamsPerson
testEmojis map[string]*gtsmodel.Emoji
typeconverter typeutils.TypeConverter typeconverter typeutils.TypeConverter
} }
@ -485,6 +486,7 @@ func (suite *TypeUtilsTestSuite) SetupSuite() {
suite.testAccounts = testrig.NewTestAccounts() suite.testAccounts = testrig.NewTestAccounts()
suite.testStatuses = testrig.NewTestStatuses() suite.testStatuses = testrig.NewTestStatuses()
suite.testPeople = testrig.NewTestFediPeople() suite.testPeople = testrig.NewTestFediPeople()
suite.testEmojis = testrig.NewTestEmojis()
suite.typeconverter = typeutils.NewConverter(suite.db) suite.typeconverter = typeutils.NewConverter(suite.db)
} }

View file

@ -216,8 +216,33 @@ func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab
// set the public key property on the Person // set the public key property on the Person
person.SetW3IDSecurityV1PublicKey(publicKeyProp) person.SetW3IDSecurityV1PublicKey(publicKeyProp)
// tag // tags
// TODO: Any tags used in the summary of this profile tagProp := streams.NewActivityStreamsTagProperty()
// tag -- emojis
emojis := a.Emojis
if len(a.EmojiIDs) > len(emojis) {
emojis = []*gtsmodel.Emoji{}
for _, emojiID := range a.EmojiIDs {
emoji, err := c.db.GetEmojiByID(ctx, emojiID)
if err != nil {
return nil, fmt.Errorf("AccountToAS: error getting emoji %s from database: %s", emojiID, err)
}
emojis = append(emojis, emoji)
}
}
for _, emoji := range emojis {
asEmoji, err := c.EmojiToAS(ctx, emoji)
if err != nil {
return nil, fmt.Errorf("AccountToAS: error converting emoji to AS emoji: %s", err)
}
tagProp.AppendTootEmoji(asEmoji)
}
// tag -- hashtags
// TODO
person.SetActivityStreamsTag(tagProp)
// attachment // attachment
// Used for profile fields. // Used for profile fields.
@ -477,11 +502,11 @@ func (c *converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.A
} }
} }
for _, emoji := range emojis { for _, emoji := range emojis {
asMention, err := c.EmojiToAS(ctx, emoji) asEmoji, err := c.EmojiToAS(ctx, emoji)
if err != nil { if err != nil {
return nil, fmt.Errorf("StatusToAS: error converting emoji to AS emoji: %s", err) return nil, fmt.Errorf("StatusToAS: error converting emoji to AS emoji: %s", err)
} }
tagProp.AppendTootEmoji(asMention) tagProp.AppendTootEmoji(asEmoji)
} }
// tag -- hashtags // tag -- hashtags

View file

@ -26,6 +26,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -34,7 +35,8 @@ type InternalToASTestSuite struct {
} }
func (suite *InternalToASTestSuite) TestAccountToAS() { func (suite *InternalToASTestSuite) TestAccountToAS() {
testAccount := suite.testAccounts["local_account_1"] // take zork for this test testAccount := &gtsmodel.Account{}
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
suite.NoError(err) suite.NoError(err)
@ -49,11 +51,33 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
// this is necessary because the order of multiple 'context' entries is not determinate // this is necessary because the order of multiple 'context' entries is not determinate
trimmed := strings.Split(string(bytes), "\"discoverable\"")[1] trimmed := strings.Split(string(bytes), "\"discoverable\"")[1]
suite.Equal(`:true,"featured":"http://localhost:8080/users/the_mighty_zork/collections/featured","followers":"http://localhost:8080/users/the_mighty_zork/followers","following":"http://localhost:8080/users/the_mighty_zork/following","icon":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"},"id":"http://localhost:8080/users/the_mighty_zork","image":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"},"inbox":"http://localhost:8080/users/the_mighty_zork/inbox","manuallyApprovesFollowers":false,"name":"original zork (he/they)","outbox":"http://localhost:8080/users/the_mighty_zork/outbox","preferredUsername":"the_mighty_zork","publicKey":{"id":"http://localhost:8080/users/the_mighty_zork/main-key","owner":"http://localhost:8080/users/the_mighty_zork","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"},"summary":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","type":"Person","url":"http://localhost:8080/@the_mighty_zork"}`, trimmed) suite.Equal(`:true,"featured":"http://localhost:8080/users/the_mighty_zork/collections/featured","followers":"http://localhost:8080/users/the_mighty_zork/followers","following":"http://localhost:8080/users/the_mighty_zork/following","icon":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"},"id":"http://localhost:8080/users/the_mighty_zork","image":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"},"inbox":"http://localhost:8080/users/the_mighty_zork/inbox","manuallyApprovesFollowers":false,"name":"original zork (he/they)","outbox":"http://localhost:8080/users/the_mighty_zork/outbox","preferredUsername":"the_mighty_zork","publicKey":{"id":"http://localhost:8080/users/the_mighty_zork/main-key","owner":"http://localhost:8080/users/the_mighty_zork","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"},"summary":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","tag":[],"type":"Person","url":"http://localhost:8080/@the_mighty_zork"}`, trimmed)
}
func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
testAccount := &gtsmodel.Account{}
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
testAccount.Emojis = []*gtsmodel.Emoji{suite.testEmojis["rainbow"]}
asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
suite.NoError(err)
ser, err := streams.Serialize(asPerson)
suite.NoError(err)
bytes, err := json.Marshal(ser)
suite.NoError(err)
// trim off everything up to 'discoverable';
// this is necessary because the order of multiple 'context' entries is not determinate
trimmed := strings.Split(string(bytes), "\"discoverable\"")[1]
suite.Equal(`:true,"featured":"http://localhost:8080/users/the_mighty_zork/collections/featured","followers":"http://localhost:8080/users/the_mighty_zork/followers","following":"http://localhost:8080/users/the_mighty_zork/following","icon":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"},"id":"http://localhost:8080/users/the_mighty_zork","image":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"},"inbox":"http://localhost:8080/users/the_mighty_zork/inbox","manuallyApprovesFollowers":false,"name":"original zork (he/they)","outbox":"http://localhost:8080/users/the_mighty_zork/outbox","preferredUsername":"the_mighty_zork","publicKey":{"id":"http://localhost:8080/users/the_mighty_zork/main-key","owner":"http://localhost:8080/users/the_mighty_zork","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"},"summary":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","tag":{"icon":{"mediaType":"image/png","type":"Image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"},"id":"http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ","name":":rainbow:","type":"Emoji"},"type":"Person","url":"http://localhost:8080/@the_mighty_zork"}`, trimmed)
} }
func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() { func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
testAccount := suite.testAccounts["local_account_1"] // take zork for this test testAccount := &gtsmodel.Account{}
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
sharedInbox := "http://localhost:8080/sharedInbox" sharedInbox := "http://localhost:8080/sharedInbox"
testAccount.SharedInboxURI = &sharedInbox testAccount.SharedInboxURI = &sharedInbox
@ -70,7 +94,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
// this is necessary because the order of multiple 'context' entries is not determinate // this is necessary because the order of multiple 'context' entries is not determinate
trimmed := strings.Split(string(bytes), "\"discoverable\"")[1] trimmed := strings.Split(string(bytes), "\"discoverable\"")[1]
suite.Equal(`:true,"endpoints":{"sharedInbox":"http://localhost:8080/sharedInbox"},"featured":"http://localhost:8080/users/the_mighty_zork/collections/featured","followers":"http://localhost:8080/users/the_mighty_zork/followers","following":"http://localhost:8080/users/the_mighty_zork/following","icon":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"},"id":"http://localhost:8080/users/the_mighty_zork","image":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"},"inbox":"http://localhost:8080/users/the_mighty_zork/inbox","manuallyApprovesFollowers":false,"name":"original zork (he/they)","outbox":"http://localhost:8080/users/the_mighty_zork/outbox","preferredUsername":"the_mighty_zork","publicKey":{"id":"http://localhost:8080/users/the_mighty_zork/main-key","owner":"http://localhost:8080/users/the_mighty_zork","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"},"summary":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","type":"Person","url":"http://localhost:8080/@the_mighty_zork"}`, trimmed) suite.Equal(`:true,"endpoints":{"sharedInbox":"http://localhost:8080/sharedInbox"},"featured":"http://localhost:8080/users/the_mighty_zork/collections/featured","followers":"http://localhost:8080/users/the_mighty_zork/followers","following":"http://localhost:8080/users/the_mighty_zork/following","icon":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"},"id":"http://localhost:8080/users/the_mighty_zork","image":{"mediaType":"image/jpeg","type":"Image","url":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"},"inbox":"http://localhost:8080/users/the_mighty_zork/inbox","manuallyApprovesFollowers":false,"name":"original zork (he/they)","outbox":"http://localhost:8080/users/the_mighty_zork/outbox","preferredUsername":"the_mighty_zork","publicKey":{"id":"http://localhost:8080/users/the_mighty_zork/main-key","owner":"http://localhost:8080/users/the_mighty_zork","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"},"summary":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","tag":[],"type":"Person","url":"http://localhost:8080/@the_mighty_zork"}`, trimmed)
} }
func (suite *InternalToASTestSuite) TestOutboxToASCollection() { func (suite *InternalToASTestSuite) TestOutboxToASCollection() {

View file

@ -159,8 +159,29 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
fields = append(fields, mField) fields = append(fields, mField)
} }
// account emojis
emojis := []model.Emoji{} emojis := []model.Emoji{}
// TODO: account emojis gtsEmojis := a.Emojis
if len(a.EmojiIDs) > len(gtsEmojis) {
gtsEmojis = []*gtsmodel.Emoji{}
for _, emojiID := range a.EmojiIDs {
emoji, err := c.db.GetEmojiByID(ctx, emojiID)
if err != nil {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting emoji %s from database: %s", emojiID, err)
}
gtsEmojis = append(gtsEmojis, emoji)
}
}
for _, emoji := range gtsEmojis {
if *emoji.Disabled {
continue
}
apiEmoji, err := c.EmojiToAPIEmoji(ctx, emoji)
if err != nil {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error converting emoji to api emoji: %s", err)
}
emojis = append(emojis, apiEmoji)
}
var acct string var acct string
if a.Domain != "" { if a.Domain != "" {
@ -194,7 +215,7 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
FollowingCount: followingCount, FollowingCount: followingCount,
StatusesCount: statusesCount, StatusesCount: statusesCount,
LastStatusAt: lastStatusAt, LastStatusAt: lastStatusAt,
Emojis: emojis, // TODO: implement this Emojis: emojis,
Fields: fields, Fields: fields,
Suspended: suspended, Suspended: suspended,
CustomCSS: a.CustomCSS, CustomCSS: a.CustomCSS,

View file

@ -43,6 +43,36 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.000Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.000Z","emojis":[],"fields":[]}`, string(b)) suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.000Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.000Z","emojis":[],"fields":[]}`, string(b))
} }
func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() {
testAccount := suite.testAccounts["local_account_1"] // take zork for this test
testEmoji := suite.testEmojis["rainbow"]
testAccount.Emojis = []*gtsmodel.Emoji{testEmoji}
apiAccount, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), testAccount)
suite.NoError(err)
suite.NotNil(apiAccount)
b, err := json.Marshal(apiAccount)
suite.NoError(err)
suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.000Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.000Z","emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true}],"fields":[]}`, string(b))
}
func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() {
testAccount := suite.testAccounts["local_account_1"] // take zork for this test
testEmoji := suite.testEmojis["rainbow"]
testAccount.EmojiIDs = []string{testEmoji.ID}
apiAccount, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), testAccount)
suite.NoError(err)
suite.NotNil(apiAccount)
b, err := json.Marshal(apiAccount)
suite.NoError(err)
suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.000Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.000Z","emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true}],"fields":[]}`, string(b))
}
func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
testAccount := suite.testAccounts["local_account_1"] // take zork for this test testAccount := suite.testAccounts["local_account_1"] // take zork for this test
apiAccount, err := suite.typeconverter.AccountToAPIAccountSensitive(context.Background(), testAccount) apiAccount, err := suite.typeconverter.AccountToAPIAccountSensitive(context.Background(), testAccount)

View file

@ -32,6 +32,7 @@ import (
var testModels = []interface{}{ var testModels = []interface{}{
&gtsmodel.Account{}, &gtsmodel.Account{},
&gtsmodel.AccountToEmoji{},
&gtsmodel.Application{}, &gtsmodel.Application{},
&gtsmodel.Block{}, &gtsmodel.Block{},
&gtsmodel.DomainBlock{}, &gtsmodel.DomainBlock{},

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -952,6 +952,28 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
VisibleInPicker: TrueBool(), VisibleInPicker: TrueBool(),
CategoryID: "", CategoryID: "",
}, },
"yell": {
ID: "01GD5KP5CQEE1R3X43Y1EHS2CW",
Shortcode: "yell",
Domain: "fossbros-anonymous.io",
CreatedAt: TimeMustParse("2020-03-18T13:12:00+01:00"),
UpdatedAt: TimeMustParse("2020-03-18T13:12:00+01:00"),
ImageRemoteURL: "http://fossbros-anonymous.io/emoji/yell.gif",
ImageStaticRemoteURL: "",
ImageURL: "http://localhost:8080/fileserver/01GD5KR15NHTY8FZ01CD4D08XP/emoji/original/01GD5KP5CQEE1R3X43Y1EHS2CW.png",
ImagePath: "/tmp/gotosocial/01GD5KR15NHTY8FZ01CD4D08XP/emoji/original/01GD5KP5CQEE1R3X43Y1EHS2CW.png",
ImageStaticURL: "http://localhost:8080/fileserver/01GD5KR15NHTY8FZ01CD4D08XP/emoji/static/01GD5KP5CQEE1R3X43Y1EHS2CW.png",
ImageStaticPath: "/tmp/gotosocial/01GD5KR15NHTY8FZ01CD4D08XP/emoji/static/01GD5KP5CQEE1R3X43Y1EHS2CW.png",
ImageContentType: "image/png",
ImageStaticContentType: "image/png",
ImageFileSize: 10889,
ImageStaticFileSize: 10808,
ImageUpdatedAt: TimeMustParse("2020-03-18T13:12:00+01:00"),
Disabled: FalseBool(),
URI: "http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW",
VisibleInPicker: FalseBool(),
CategoryID: "",
},
} }
} }
@ -1045,6 +1067,10 @@ func newTestStoredEmoji() map[string]filenames {
Original: "rainbow-original.png", Original: "rainbow-original.png",
Static: "rainbow-static.png", Static: "rainbow-static.png",
}, },
"yell": {
Original: "yell-original.png",
Static: "yell-static.png",
},
} }
} }
@ -1941,6 +1967,22 @@ func NewTestFediServices() map[string]vocab.ActivityStreamsService {
} }
} }
func NewTestFediEmojis() map[string]vocab.TootEmoji {
return map[string]vocab.TootEmoji{
"http://fossbros-anonymous.io/emoji/01GD5HCC2YECT012TK8PAGX4D1": newAPEmoji(
URLMustParse("http://fossbros-anonymous.io/emoji/01GD5HCC2YECT012TK8PAGX4D1"),
"kip_van_den_bos",
TimeMustParse("2022-09-13T12:13:12+02:00"),
newAPImage(
URLMustParse("http://fossbros-anonymous.io/emoji/kip.gif"),
"image/gif",
"",
"",
),
),
}
}
// RemoteAttachmentFile mimics a remote (federated) attachment // RemoteAttachmentFile mimics a remote (federated) attachment
type RemoteAttachmentFile struct { type RemoteAttachmentFile struct {
Data []byte Data []byte
@ -1968,6 +2010,16 @@ func NewTestFediAttachments(relativePath string) map[string]RemoteAttachmentFile
panic(err) panic(err)
} }
kipBytes, err := os.ReadFile(fmt.Sprintf("%s/kip-original.gif", relativePath))
if err != nil {
panic(err)
}
yellBytes, err := os.ReadFile(fmt.Sprintf("%s/yell-original.png", relativePath))
if err != nil {
panic(err)
}
return map[string]RemoteAttachmentFile{ return map[string]RemoteAttachmentFile{
"https://s3-us-west-2.amazonaws.com/plushcity/media_attachments/files/106/867/380/219/163/828/original/88e8758c5f011439.jpg": { "https://s3-us-west-2.amazonaws.com/plushcity/media_attachments/files/106/867/380/219/163/828/original/88e8758c5f011439.jpg": {
Data: beeBytes, Data: beeBytes,
@ -1985,6 +2037,14 @@ func NewTestFediAttachments(relativePath string) map[string]RemoteAttachmentFile
Data: peglinBytes, Data: peglinBytes,
ContentType: "image/gif", ContentType: "image/gif",
}, },
"http://fossbros-anonymous.io/emoji/kip.gif": {
Data: kipBytes,
ContentType: "image/gif",
},
"http://fossbros-anonymous.io/emoji/yell.gif": {
Data: yellBytes,
ContentType: "image/png",
},
} }
} }
@ -2857,6 +2917,28 @@ func newAPImage(url *url.URL, mediaType string, imageDescription string, blurhas
return image return image
} }
func newAPEmoji(id *url.URL, name string, updated time.Time, image vocab.ActivityStreamsImage) vocab.TootEmoji {
emoji := streams.NewTootEmoji()
idProp := streams.NewJSONLDIdProperty()
idProp.SetIRI(id)
emoji.SetJSONLDId(idProp)
nameProp := streams.NewActivityStreamsNameProperty()
nameProp.AppendXMLSchemaString(`:` + strings.Trim(name, ":") + `:`)
emoji.SetActivityStreamsName(nameProp)
updatedProp := streams.NewActivityStreamsUpdatedProperty()
updatedProp.Set(updated)
emoji.SetActivityStreamsUpdated(updatedProp)
iconProp := streams.NewActivityStreamsIconProperty()
iconProp.AppendActivityStreamsImage(image)
emoji.SetActivityStreamsIcon(iconProp)
return emoji
}
// NewAPNote returns a new activity streams note for the given parameters // NewAPNote returns a new activity streams note for the given parameters
func NewAPNote( func NewAPNote(
noteID *url.URL, noteID *url.URL,

View file

@ -64,6 +64,7 @@ type MockHTTPClient struct {
testRemoteGroups map[string]vocab.ActivityStreamsGroup testRemoteGroups map[string]vocab.ActivityStreamsGroup
testRemoteServices map[string]vocab.ActivityStreamsService testRemoteServices map[string]vocab.ActivityStreamsService
testRemoteAttachments map[string]RemoteAttachmentFile testRemoteAttachments map[string]RemoteAttachmentFile
testRemoteEmojis map[string]vocab.TootEmoji
SentMessages sync.Map SentMessages sync.Map
} }
@ -90,6 +91,7 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
mockHTTPClient.testRemoteGroups = NewTestFediGroups() mockHTTPClient.testRemoteGroups = NewTestFediGroups()
mockHTTPClient.testRemoteServices = NewTestFediServices() mockHTTPClient.testRemoteServices = NewTestFediServices()
mockHTTPClient.testRemoteAttachments = NewTestFediAttachments(relativeMediaPath) mockHTTPClient.testRemoteAttachments = NewTestFediAttachments(relativeMediaPath)
mockHTTPClient.testRemoteEmojis = NewTestFediEmojis()
mockHTTPClient.do = func(req *http.Request) (*http.Response, error) { mockHTTPClient.do = func(req *http.Request) (*http.Response, error) {
responseCode := http.StatusNotFound responseCode := http.StatusNotFound
@ -173,6 +175,19 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
responseBytes = serviceJSON responseBytes = serviceJSON
responseContentType = applicationActivityJSON responseContentType = applicationActivityJSON
responseContentLength = len(serviceJSON) responseContentLength = len(serviceJSON)
} else if emoji, ok := mockHTTPClient.testRemoteEmojis[req.URL.String()]; ok {
emojiI, err := streams.Serialize(emoji)
if err != nil {
panic(err)
}
emojiJSON, err := json.Marshal(emojiI)
if err != nil {
panic(err)
}
responseCode = http.StatusOK
responseBytes = emojiJSON
responseContentType = applicationActivityJSON
responseContentLength = len(emojiJSON)
} else if attachment, ok := mockHTTPClient.testRemoteAttachments[req.URL.String()]; ok { } else if attachment, ok := mockHTTPClient.testRemoteAttachments[req.URL.String()]; ok {
responseCode = http.StatusOK responseCode = http.StatusOK
responseBytes = attachment.Data responseBytes = attachment.Data

View file

@ -12,12 +12,12 @@
<div class="basic"> <div class="basic">
<div id="profile-basic-filler2"></div> <div id="profile-basic-filler2"></div>
<a href="{{.account.Avatar}}" class="avatar"><img src="{{.account.Avatar}}" alt="{{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}'s avatar"></a> <a href="{{.account.Avatar}}" class="avatar"><img src="{{.account.Avatar}}" alt="{{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}'s avatar"></a>
<div class="displayname">{{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}</div> <div class="displayname">{{if .account.DisplayName}}{{emojify .account.Emojis (escape .account.DisplayName)}}{{else}}{{.account.Username}}{{end}}</div>
<div class="username"><span>@{{.account.Username}}</span><span>@{{.instance.AccountDomain}}</span></div> <div class="username"><span>@{{.account.Username}}</span><span>@{{.instance.AccountDomain}}</span></div>
</div> </div>
<div class="detailed"> <div class="detailed">
<div class="bio"> <div class="bio">
{{ if .account.Note }}{{ .account.Note | noescape }}{{else}}This GoToSocial user hasn't written a bio yet!{{end}} {{ if .account.Note }}{{emojify .account.Emojis (noescape .account.Note)}}{{else}}This GoToSocial user hasn't written a bio yet!{{end}}
</div> </div>
</div> </div>
<div class="accountstats"> <div class="accountstats">

View file

@ -1,6 +1,6 @@
<div class="contentgrid"> <div class="contentgrid">
<a href="{{.Account.URL}}" class="avatar"><img src="{{.Account.Avatar}}" alt=""></a> <a href="{{.Account.URL}}" class="avatar"><img src="{{.Account.Avatar}}" alt=""></a>
<a href="{{.Account.URL}}" class="displayname">{{if .Account.DisplayName}}{{.Account.DisplayName}}{{else}}{{.Account.Username}}{{end}}</a> <a href="{{.Account.URL}}" class="displayname">{{if .Account.DisplayName}}{{emojify .Account.Emojis (escape .Account.DisplayName)}}{{else}}{{.Account.Username}}{{end}}</a>
<a href="{{.Account.URL}}" class="username">@{{.Account.Acct}}</a> <a href="{{.Account.URL}}" class="username">@{{.Account.Acct}}</a>
<div class="text"> <div class="text">
{{if .SpoilerText}} {{if .SpoilerText}}