diff options
Diffstat (limited to 'internal/typeutils')
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 226 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 40 |
2 files changed, 180 insertions, 86 deletions
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 74b061fb0..88646c311 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -19,6 +19,7 @@ package typeutils import ( "context" + "errors" "fmt" "math" "strconv" @@ -26,6 +27,7 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -83,99 +85,110 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode } func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { - // count followers + if err := c.db.PopulateAccount(ctx, a); err != nil { + log.Errorf(ctx, "error(s) populating account, will continue: %s", err) + } + + // Basic account stats: + // - Followers count + // - Following count + // - Statuses count + // - Last status time + followersCount, err := c.db.CountAccountFollowers(ctx, a.ID) - if err != nil { - return nil, fmt.Errorf("error counting followers: %s", err) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting followers: %w", err) } - // count following followingCount, err := c.db.CountAccountFollows(ctx, a.ID) - if err != nil { - return nil, fmt.Errorf("error counting following: %s", err) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting following: %w", err) } - // count statuses statusesCount, err := c.db.CountAccountStatuses(ctx, a.ID) - if err != nil { - return nil, fmt.Errorf("error counting statuses: %s", err) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err) } - // check when the last status was var lastStatusAt *string lastPosted, err := c.db.GetAccountLastPosted(ctx, a.ID, false) - if err == nil && !lastPosted.IsZero() { - lastStatusAtTemp := util.FormatISO8601(lastPosted) - lastStatusAt = &lastStatusAtTemp + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err) } - // set account avatar fields if available - var aviURL string - var aviURLStatic string - if a.AvatarMediaAttachmentID != "" { - if a.AvatarMediaAttachment == nil { - avi, err := c.db.GetAttachmentByID(ctx, a.AvatarMediaAttachmentID) - if err != nil { - log.Errorf(ctx, "error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err) - } - a.AvatarMediaAttachment = avi - } - if a.AvatarMediaAttachment != nil { - aviURL = a.AvatarMediaAttachment.URL - aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL - } + if !lastPosted.IsZero() { + lastStatusAt = func() *string { t := util.FormatISO8601(lastPosted); return &t }() } - // set account header fields if available - var headerURL string - var headerURLStatic string - if a.HeaderMediaAttachmentID != "" { - if a.HeaderMediaAttachment == nil { - avi, err := c.db.GetAttachmentByID(ctx, a.HeaderMediaAttachmentID) - if err != nil { - log.Errorf(ctx, "error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err) - } - a.HeaderMediaAttachment = avi - } - if a.HeaderMediaAttachment != nil { - headerURL = a.HeaderMediaAttachment.URL - headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL - } + // Profile media + nice extras: + // - Avatar + // - Header + // - Fields + // - Emojis + + var ( + aviURL string + aviURLStatic string + headerURL string + headerURLStatic string + fields = make([]apimodel.Field, len(a.Fields)) + ) + + if a.AvatarMediaAttachment != nil { + aviURL = a.AvatarMediaAttachment.URL + aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL } - // preallocate frontend fields slice - fields := make([]apimodel.Field, len(a.Fields)) + if a.HeaderMediaAttachment != nil { + headerURL = a.HeaderMediaAttachment.URL + headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL + } - // Convert account GTS model fields to frontend + // GTS model fields -> frontend. for i, field := range a.Fields { mField := apimodel.Field{ Name: field.Name, Value: field.Value, } + if !field.VerifiedAt.IsZero() { mField.VerifiedAt = util.FormatISO8601(field.VerifiedAt) } + fields[i] = mField } - // convert account gts model emojis to frontend api model emojis + // GTS model emojis -> frontend. apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, a.Emojis, a.EmojiIDs) if err != nil { log.Errorf(ctx, "error converting account emojis: %v", err) } - var acct string - var role *apimodel.AccountRole + // Bits that vary between remote + local accounts: + // - Account (acct) string. + // - Role. + + var ( + acct string + role *apimodel.AccountRole + ) + + if a.IsRemote() { + // Domain may be in Punycode, + // de-punify it just in case. + d, err := util.DePunify(a.Domain) + if err != nil { + return nil, fmt.Errorf("AccountToAPIAccountPublic: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err) + } - if a.Domain != "" { - // this is a remote user - acct = a.Username + "@" + a.Domain + acct = a.Username + "@" + d } else { - // this is a local user + // This is a local user. acct = a.Username + user, err := c.db.GetUserByAccountID(ctx, a.ID) if err != nil { - return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %s", a.ID, err) + return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %w", a.ID, err) } switch { @@ -188,10 +201,8 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A } } - var suspended bool - if !a.SuspendedAt.IsZero() { - suspended = true - } + // Remaining properties are simple and + // can be populated directly below. accountFrontend := &apimodel.Account{ ID: a.ID, @@ -214,12 +225,14 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A LastStatusAt: lastStatusAt, Emojis: apiEmojis, Fields: fields, - Suspended: suspended, + Suspended: !a.SuspendedAt.IsZero(), CustomCSS: a.CustomCSS, EnableRSS: *a.EnableRSS, Role: role, } + // Bodge default avatar + header in, + // if we didn't have one already. c.ensureAvatar(accountFrontend) c.ensureHeader(accountFrontend) @@ -227,18 +240,37 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A } func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { - var acct string - if a.Domain != "" { - // this is a remote user - acct = fmt.Sprintf("%s@%s", a.Username, a.Domain) + var ( + acct string + role *apimodel.AccountRole + ) + + if a.IsRemote() { + // Domain may be in Punycode, + // de-punify it just in case. + d, err := util.DePunify(a.Domain) + if err != nil { + return nil, fmt.Errorf("AccountToAPIAccountPublic: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err) + } + + acct = a.Username + "@" + d } else { - // this is a local user + // This is a local user. acct = a.Username - } - var suspended bool - if !a.SuspendedAt.IsZero() { - suspended = true + user, err := c.db.GetUserByAccountID(ctx, a.ID) + if err != nil { + return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %s", a.ID, err) + } + + switch { + case *user.Admin: + role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin} + case *user.Moderator: + role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator} + default: + role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser} + } } return &apimodel.Account{ @@ -249,7 +281,8 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel. Bot: *a.Bot, CreatedAt: util.FormatISO8601(a.CreatedAt), URL: a.URL, - Suspended: suspended, + Suspended: !a.SuspendedAt.IsZero(), + Role: role, }, nil } @@ -263,15 +296,20 @@ func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac inviteRequest *string approved bool disabled bool - silenced bool - suspended bool role = apimodel.AccountRole{Name: apimodel.AccountRoleUser} // assume user by default createdByApplicationID string ) // take user-level information if possible if a.IsRemote() { - domain = &a.Domain + // Domain may be in Punycode, + // de-punify it just in case. + d, err := util.DePunify(a.Domain) + if err != nil { + return nil, fmt.Errorf("AccountToAdminAPIAccount: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err) + } + + domain = &d } else { user, err := c.db.GetUserByAccountID(ctx, a.ID) if err != nil { @@ -303,9 +341,6 @@ func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac createdByApplicationID = user.CreatedByApplicationID } - silenced = !a.SilencedAt.IsZero() - suspended = !a.SuspendedAt.IsZero() - apiAccount, err := c.AccountToAPIAccountPublic(ctx, a) if err != nil { return nil, fmt.Errorf("AccountToAdminAPIAccount: error converting account to api account for account id %s: %w", a.ID, err) @@ -325,8 +360,8 @@ func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac Confirmed: confirmed, Approved: approved, Disabled: disabled, - Silenced: silenced, - Suspended: suspended, + Silenced: !a.SilencedAt.IsZero(), + Suspended: !a.SuspendedAt.IsZero(), Account: apiAccount, CreatedByApplicationID: createdByApplicationID, InvitedByAccountID: "", // not implemented (yet) @@ -428,16 +463,19 @@ func (c *converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention m.TargetAccount = targetAccount } - var local bool - if m.TargetAccount.Domain == "" { - local = true - } - var acct string - if local { + if m.TargetAccount.IsLocal() { acct = m.TargetAccount.Username } else { - acct = fmt.Sprintf("%s@%s", m.TargetAccount.Username, m.TargetAccount.Domain) + // Domain may be in Punycode, + // de-punify it just in case. + d, err := util.DePunify(m.TargetAccount.Domain) + if err != nil { + err = fmt.Errorf("MentionToAPIMention: error de-punifying domain %s for account id %s: %w", m.TargetAccount.Domain, m.TargetAccountID, err) + return apimodel.Mention{}, err + } + + acct = m.TargetAccount.Username + "@" + d } return apimodel.Mention{ @@ -476,6 +514,17 @@ func (c *converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) return nil, err } + if e.Domain != "" { + // Domain may be in Punycode, + // de-punify it just in case. + var err error + e.Domain, err = util.DePunify(e.Domain) + if err != nil { + err = fmt.Errorf("EmojiToAdminAPIEmoji: error de-punifying domain %s for emoji id %s: %w", e.Domain, e.ID, err) + return nil, err + } + } + return &apimodel.AdminEmoji{ Emoji: emoji, ID: e.ID, @@ -942,9 +991,16 @@ func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmod } func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) { + // Domain may be in Punycode, + // de-punify it just in case. + d, err := util.DePunify(b.Domain) + if err != nil { + return nil, fmt.Errorf("DomainBlockToAPIDomainBlock: error de-punifying domain %s: %w", b.Domain, err) + } + domainBlock := &apimodel.DomainBlock{ Domain: apimodel.Domain{ - Domain: b.Domain, + Domain: d, PublicComment: b.PublicComment, }, } diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 1fb601260..c8ab5a8e1 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -70,10 +70,12 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() { } func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() { - testAccount := suite.testAccounts["local_account_1"] // take zork for this test + testAccount := >smodel.Account{} + *testAccount = *suite.testAccounts["local_account_1"] // take zork for this test testEmoji := suite.testEmojis["rainbow"] testAccount.Emojis = []*gtsmodel.Emoji{testEmoji} + testAccount.EmojiIDs = []string{testEmoji.ID} apiAccount, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), testAccount) suite.NoError(err) @@ -210,6 +212,42 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { }`, string(b)) } +func (suite *InternalToFrontendTestSuite) TestAccountToFrontendPublicPunycode() { + testAccount := suite.testAccounts["remote_account_4"] + apiAccount, err := suite.typeconverter.AccountToAPIAccountPublic(context.Background(), testAccount) + suite.NoError(err) + suite.NotNil(apiAccount) + + b, err := json.MarshalIndent(apiAccount, "", " ") + suite.NoError(err) + + // Even though account domain is stored in + // punycode, it should be served in its + // unicode representation in the 'acct' field. + suite.Equal(`{ + "id": "07GZRBAEMBNKGZ8Z9VSKSXKR98", + "username": "üser", + "acct": "üser@ëxample.org", + "display_name": "", + "locked": false, + "discoverable": false, + "bot": false, + "created_at": "2020-08-10T12:13:28.000Z", + "note": "", + "url": "https://xn--xample-ova.org/users/@%C3%BCser", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 0, + "following_count": 0, + "statuses_count": 0, + "last_status_at": null, + "emojis": [], + "fields": [] +}`, string(b)) +} + func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { testStatus := suite.testStatuses["admin_account_status_1"] requestingAccount := suite.testAccounts["local_account_1"] |