summaryrefslogtreecommitdiff
path: root/internal/typeutils/internaltofrontend.go
diff options
context:
space:
mode:
authorLibravatar kim <grufwub@gmail.com>2025-11-03 21:16:42 +0100
committerLibravatar tobi <tobi.smethurst@protonmail.com>2025-11-17 14:12:22 +0100
commit29547ce8aba63454e84d277b0597fb2764e8cb23 (patch)
treee80d0d74eea123dbc6dd1dcffadff0cb11f82faa /internal/typeutils/internaltofrontend.go
parent[bugfix] Fix invalid period parameter when generating the TOTP URL (#4536) (diff)
downloadgotosocial-29547ce8aba63454e84d277b0597fb2764e8cb23.tar.xz
[bugfix] more RSS validation issues (#4517)
Fixes some validation issues relating to author information often expected to be valid email addresses, which our @displayname@username is not. There's still a few more validation issues, but honestly if we're going have better support I think it might be worth dropping gorilla/feeds for our own tagged XML / JSON structs. Also does a bunch of housekeeping in the typeutils package removing error returns where never used / only ever logged, removing unused contexts etc. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4517 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
Diffstat (limited to 'internal/typeutils/internaltofrontend.go')
-rw-r--r--internal/typeutils/internaltofrontend.go641
1 files changed, 300 insertions, 341 deletions
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 72b7a0126..73faaa2c2 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -195,11 +195,16 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
// AccountToAPIAccount functions instead.
func (c *Converter) AccountToWebAccount(
ctx context.Context,
- a *gtsmodel.Account,
+ account *gtsmodel.Account,
+ apiAccount *apimodel.Account,
) (*apimodel.WebAccount, error) {
- apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
- if err != nil {
- return nil, err
+ if apiAccount == nil {
+ var err error
+
+ apiAccount, err = c.AccountToAPIAccountPublic(ctx, account)
+ if err != nil {
+ return nil, err
+ }
}
webAccount := &apimodel.WebAccount{
@@ -208,35 +213,23 @@ func (c *Converter) AccountToWebAccount(
// Set additional avatar information for
// serving the avatar in a nice <picture>.
- if ogAvi := a.AvatarMediaAttachment; ogAvi != nil {
- avatarAttachment, err := c.AttachmentToAPIAttachment(ctx, ogAvi)
- if err != nil {
- // This is just extra data so just
- // log but don't return any error.
- log.Errorf(ctx, "error converting account avatar attachment: %v", err)
- } else {
- webAccount.AvatarAttachment = &apimodel.WebAttachment{
- Attachment: &avatarAttachment,
- MIMEType: ogAvi.File.ContentType,
- PreviewMIMEType: ogAvi.Thumbnail.ContentType,
- }
+ if avatar := account.AvatarMediaAttachment; avatar != nil {
+ apiAttachment := AttachmentToAPIAttachment(avatar)
+ webAccount.AvatarAttachment = &apimodel.WebAttachment{
+ Attachment: &apiAttachment,
+ MIMEType: avatar.File.ContentType,
+ PreviewMIMEType: avatar.Thumbnail.ContentType,
}
}
// Set additional header information for
// serving the header in a nice <picture>.
- if ogHeader := a.HeaderMediaAttachment; ogHeader != nil {
- headerAttachment, err := c.AttachmentToAPIAttachment(ctx, ogHeader)
- if err != nil {
- // This is just extra data so just
- // log but don't return any error.
- log.Errorf(ctx, "error converting account header attachment: %v", err)
- } else {
- webAccount.HeaderAttachment = &apimodel.WebAttachment{
- Attachment: &headerAttachment,
- MIMEType: ogHeader.File.ContentType,
- PreviewMIMEType: ogHeader.Thumbnail.ContentType,
- }
+ if header := account.HeaderMediaAttachment; header != nil {
+ apiAttachment := AttachmentToAPIAttachment(header)
+ webAccount.HeaderAttachment = &apimodel.WebAttachment{
+ Attachment: &apiAttachment,
+ MIMEType: header.File.ContentType,
+ PreviewMIMEType: header.Thumbnail.ContentType,
}
}
@@ -244,8 +237,8 @@ func (c *Converter) AccountToWebAccount(
// populating settings-specific thingies,
// as instance account doesn't store a
// settings struct.
- if a.Settings != nil {
- webAccount.WebLayout = a.Settings.WebLayout.String()
+ if account.Settings != nil {
+ webAccount.WebLayout = account.Settings.WebLayout.String()
}
return webAccount, nil
@@ -325,10 +318,10 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
fields := c.fieldsToAPIFields(a.Fields)
// 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)
- }
+ apiEmojis := c.emojisToAPI(ctx,
+ a.Emojis,
+ a.EmojiIDs,
+ )
// Bits that vary between remote + local accounts:
// - Account (acct) string.
@@ -670,15 +663,15 @@ func (c *Converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Applic
// AppToAPIAppPublic takes a db model application as a param, and returns a populated apitype application, or an error
// if something goes wrong. The returned application should be ready to serialize on an API level, and has sensitive
// fields sanitized so that it can be served to non-authorized accounts without revealing any private information.
-func (c *Converter) AppToAPIAppPublic(ctx context.Context, a *gtsmodel.Application) (*apimodel.Application, error) {
+func AppToAPIAppPublic(app *gtsmodel.Application) *apimodel.Application {
return &apimodel.Application{
- Name: a.Name,
- Website: a.Website,
- }, nil
+ Name: app.Name,
+ Website: app.Website,
+ }
}
-// AttachmentToAPIAttachment converts a gts model media attacahment into its api representation for serialization on the API.
-func (c *Converter) AttachmentToAPIAttachment(ctx context.Context, media *gtsmodel.MediaAttachment) (apimodel.Attachment, error) {
+// AttachmentToAPIAttachment converts a gts model media attacahment into its api representation.
+func AttachmentToAPIAttachment(media *gtsmodel.MediaAttachment) apimodel.Attachment {
var api apimodel.Attachment
api.Type = media.Type.String()
api.ID = media.ID
@@ -730,111 +723,110 @@ func (c *Converter) AttachmentToAPIAttachment(ctx context.Context, media *gtsmod
api.PreviewRemoteURL = util.PtrIf(media.Thumbnail.RemoteURL)
api.Description = util.PtrIf(media.Description)
- return api, nil
+ return api
}
// MentionToAPIMention converts a gts model mention into its api (frontend) representation for serialization on the API.
-func (c *Converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention) (apimodel.Mention, error) {
- if m.TargetAccount == nil {
- targetAccount, err := c.state.DB.GetAccountByID(ctx, m.TargetAccountID)
+func (c *Converter) MentionToAPIMention(ctx context.Context, mention *gtsmodel.Mention) (apimodel.Mention, error) {
+ if mention.TargetAccount == nil {
+ var err error
+
+ mention.TargetAccount, err = c.state.DB.GetAccountByID(ctx, mention.TargetAccountID)
if err != nil {
- return apimodel.Mention{}, err
+ return apimodel.Mention{}, gtserror.Newf("db error getting mention target: %w", err)
}
- m.TargetAccount = targetAccount
}
var acct string
- if m.TargetAccount.IsLocal() {
- acct = m.TargetAccount.Username
+ if mention.TargetAccount.IsLocal() {
+ acct = mention.TargetAccount.Username
} else {
- // Domain may be in Punycode,
- // de-punify it just in case.
- d, err := util.DePunify(m.TargetAccount.Domain)
+ // Domain may be in Punycode, de-punify it just in case.
+ d, err := util.DePunify(mention.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
+ return apimodel.Mention{}, gtserror.Newf("error de-punifying mention target %s domain: %w", mention.TargetAccount.UsernameDomain(), err)
}
- acct = m.TargetAccount.Username + "@" + d
+ // Set the de-punified username@domain combo.
+ acct = mention.TargetAccount.Username + "@" + d
}
return apimodel.Mention{
- ID: m.TargetAccount.ID,
- Username: m.TargetAccount.Username,
- URL: m.TargetAccount.URL,
+ ID: mention.TargetAccount.ID,
+ Username: mention.TargetAccount.Username,
+ URL: mention.TargetAccount.URL,
Acct: acct,
}, nil
}
// EmojiToAPIEmoji converts a gts model emoji into its api (frontend) representation for serialization on the API.
-func (c *Converter) EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (apimodel.Emoji, error) {
+func (c *Converter) EmojiToAPIEmoji(ctx context.Context, emoji *gtsmodel.Emoji) (apimodel.Emoji, error) {
var category string
- if e.CategoryID != "" {
- if e.Category == nil {
- var err error
- e.Category, err = c.state.DB.GetEmojiCategory(ctx, e.CategoryID)
- if err != nil {
- return apimodel.Emoji{}, err
- }
+ if emoji.CategoryID != "" {
+ var err error
+
+ emoji.Category, err = c.state.DB.GetEmojiCategory(ctx, emoji.CategoryID)
+ if err != nil {
+ return apimodel.Emoji{}, gtserror.Newf("db error getting emoji %s category: %w", emoji.ShortcodeDomain(), err)
}
- category = e.Category.Name
+
+ category = emoji.Category.Name
}
return apimodel.Emoji{
- Shortcode: e.Shortcode,
- URL: e.ImageURL,
- StaticURL: e.ImageStaticURL,
- VisibleInPicker: *e.VisibleInPicker,
+ Shortcode: emoji.Shortcode,
+ URL: emoji.ImageURL,
+ StaticURL: emoji.ImageStaticURL,
+ VisibleInPicker: *emoji.VisibleInPicker,
Category: category,
}, nil
}
// EmojiToAdminAPIEmoji converts a gts model emoji into an API representation with extra admin information.
-func (c *Converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*apimodel.AdminEmoji, error) {
- emoji, err := c.EmojiToAPIEmoji(ctx, e)
+func (c *Converter) EmojiToAdminAPIEmoji(ctx context.Context, emoji *gtsmodel.Emoji) (*apimodel.AdminEmoji, error) {
+ apiEmoji, err := c.EmojiToAPIEmoji(ctx, emoji)
if err != nil {
return nil, err
}
- if !e.IsLocal() {
+ if !emoji.IsLocal() {
// Domain may be in Punycode,
// de-punify it just in case.
var err error
- e.Domain, err = util.DePunify(e.Domain)
+ emoji.Domain, err = util.DePunify(emoji.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 nil, gtserror.Newf("error de-punifying emoji %s domain: %w", emoji.ShortcodeDomain(), err)
}
}
return &apimodel.AdminEmoji{
- Emoji: emoji,
- ID: e.ID,
- Disabled: *e.Disabled,
- Domain: e.Domain,
- UpdatedAt: util.FormatISO8601(e.UpdatedAt),
- TotalFileSize: e.ImageFileSize + e.ImageStaticFileSize,
- ContentType: e.ImageContentType,
- URI: e.URI,
+ Emoji: apiEmoji,
+ ID: emoji.ID,
+ Disabled: *emoji.Disabled,
+ Domain: emoji.Domain,
+ UpdatedAt: util.FormatISO8601(emoji.UpdatedAt),
+ TotalFileSize: emoji.ImageFileSize + emoji.ImageStaticFileSize,
+ ContentType: emoji.ImageContentType,
+ URI: emoji.URI,
}, nil
}
// EmojiCategoryToAPIEmojiCategory converts a gts model emoji category into its api (frontend) representation.
-func (c *Converter) EmojiCategoryToAPIEmojiCategory(ctx context.Context, category *gtsmodel.EmojiCategory) (*apimodel.EmojiCategory, error) {
+func EmojiCategoryToAPIEmojiCategory(category *gtsmodel.EmojiCategory) *apimodel.EmojiCategory {
return &apimodel.EmojiCategory{
ID: category.ID,
Name: category.Name,
- }, nil
+ }
}
// TagToAPITag converts a gts model tag into its api (frontend) representation for serialization on the API.
// If stubHistory is set to 'true', then the 'history' field of the tag will be populated with a pointer to an empty slice, for API compatibility reasons.
// following is an optional flag marking whether the currently authenticated user (if there is one) is following the tag.
-func (c *Converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag, stubHistory bool, following *bool) (apimodel.Tag, error) {
+func TagToAPITag(tag *gtsmodel.Tag, stubHistory bool, following *bool) apimodel.Tag {
return apimodel.Tag{
- Name: strings.ToLower(t.Name),
- URL: uris.URIForTag(t.Name),
+ Name: strings.ToLower(tag.Name),
+ URL: uris.URIForTag(tag.Name),
History: func() *[]any {
if !stubHistory {
return nil
@@ -844,7 +836,7 @@ func (c *Converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag, stubHistor
return &h
}(),
Following: following,
- }, nil
+ }
}
// StatusToAPIStatus converts a gts model
@@ -961,7 +953,7 @@ func (c *Converter) StatusToWebStatus(
}
// Convert status author to web model.
- acct, err := c.AccountToWebAccount(ctx, s.Account)
+ acct, err := c.AccountToWebAccount(ctx, s.Account, nil)
if err != nil {
return nil, err
}
@@ -1205,25 +1197,13 @@ func (c *Converter) baseStatusToFrontend(
return nil, gtserror.Newf("error counting faves: %w", err)
}
- apiAttachments, err := c.convertAttachmentsToAPIAttachments(ctx, status.Attachments, status.AttachmentIDs)
- if err != nil {
- log.Errorf(ctx, "error converting status attachments: %v", err)
- }
+ apiAttachments := c.attachmentsToAPI(ctx, status.Attachments, status.AttachmentIDs)
- apiMentions, err := c.convertMentionsToAPIMentions(ctx, status.Mentions, status.MentionIDs)
- if err != nil {
- log.Errorf(ctx, "error converting status mentions: %v", err)
- }
+ apiEmojis := c.emojisToAPI(ctx, status.Emojis, status.EmojiIDs)
- apiTags, err := c.convertTagsToAPITags(ctx, status.Tags, status.TagIDs)
- if err != nil {
- log.Errorf(ctx, "error converting status tags: %v", err)
- }
+ apiMentions := c.mentionsToAPI(ctx, status.Mentions, status.MentionIDs)
- apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, status.Emojis, status.EmojiIDs)
- if err != nil {
- log.Errorf(ctx, "error converting status emojis: %v", err)
- }
+ apiTags := c.tagsToAPI(ctx, status.Tags, status.TagIDs)
// Take status's interaction policy, or
// fall back to default for its visibility.
@@ -1262,7 +1242,7 @@ func (c *Converter) baseStatusToFrontend(
Card: nil, // TODO: implement cards
Text: status.Text,
ContentType: ContentTypeToAPIContentType(status.ContentType),
- InteractionPolicy: *apiInteractionPolicy,
+ InteractionPolicy: apiInteractionPolicy,
// Mastodon API says spoiler_text should be *text*, not HTML, so
// parse any HTML back to plaintext when serializing via the API,
@@ -1282,13 +1262,7 @@ func (c *Converter) baseStatusToFrontend(
switch {
case status.CreatedWithApplication != nil:
// App exists for this status and is set.
- apiStatus.Application, err = c.AppToAPIAppPublic(ctx, status.CreatedWithApplication)
- if err != nil {
- return nil, gtserror.Newf(
- "error converting application %s: %w",
- status.CreatedWithApplicationID, err,
- )
- }
+ apiStatus.Application = AppToAPIAppPublic(status.CreatedWithApplication)
case status.CreatedWithApplicationID != "":
// App existed for this status but not
@@ -1374,22 +1348,14 @@ func (c *Converter) StatusToEditHistory(
// Convert the status author account to API model.
apiAccount, err := c.AccountToAPIAccountPublic(ctx,
- status.Account,
- )
+ status.Account)
if err != nil {
return nil, gtserror.Newf("error converting account: %w", err)
}
- // Convert status emojis to their API models,
- // this includes all status emojis both current
- // and historic, so it gets passed to each edit.
- apiEmojis, err := c.convertEmojisToAPIEmojis(ctx,
- nil,
- status.EmojiIDs,
- )
- if err != nil {
- return nil, gtserror.Newf("error converting emojis: %w", err)
- }
+ // Convert status emojis to their API models, this includes all
+ // emojis both current and historic, so it gets passed to each edit.
+ apiEmojis := c.emojisToAPI(ctx, status.Emojis, status.EmojiIDs)
var votes []int
var options []string
@@ -1408,8 +1374,7 @@ func (c *Converter) StatusToEditHistory(
// Append *current* version of the status to last slot
// in the edits so we can add it at the bottom as latest
// revision using the below loop. Note: a new slice is
- // created here with the append, to avoid modifying
- // Edits on the status pointer.
+ // created here with the append, to avoid modifying Edits on status.
edits := append(status.Edits, &gtsmodel.StatusEdit{ //nolint:gocritic
Content: status.Content,
ContentWarning: status.ContentWarning,
@@ -1426,8 +1391,11 @@ func (c *Converter) StatusToEditHistory(
//
// This creates a slice of revisions that goes from
// oldest (original status) to newest (latest revision).
- editHistory := make([]*apimodel.StatusEdit, 0, len(edits))
- for _, edit := range edits {
+ apiEdits := make([]*apimodel.StatusEdit, len(edits))
+ if len(apiEdits) != len(edits) {
+ panic(gtserror.New("bound check elimination"))
+ }
+ for i, edit := range edits {
// Iterate through edit attachment IDs, getting model from 'media' lookup.
apiAttachments := make([]*apimodel.Attachment, 0, len(edit.AttachmentIDs))
@@ -1437,16 +1405,8 @@ func (c *Converter) StatusToEditHistory(
continue
}
- // Convert each media attachment to frontend API model.
- apiAttachment, err := c.AttachmentToAPIAttachment(ctx,
- attachment,
- )
- if err != nil {
- log.Error(ctx, "error converting attachment: %v", err)
- continue
- }
-
- // Append converted media attachment to return slice.
+ // Convert and append each media attachment to slice.
+ apiAttachment := AttachmentToAPIAttachment(attachment)
apiAttachments = append(apiAttachments, &apiAttachment)
}
@@ -1469,8 +1429,11 @@ func (c *Converter) StatusToEditHistory(
if len(edit.PollOptions) > 0 {
apiPoll = new(apimodel.Poll)
- // Iterate through poll options and attach to API poll model.
+ // Iterate through poll options and attach each to API poll model.
apiPoll.Options = make([]apimodel.PollOption, len(edit.PollOptions))
+ if len(apiPoll.Options) != len(edit.PollOptions) {
+ panic(gtserror.New("bound check elimination"))
+ }
for i, option := range edit.PollOptions {
apiPoll.Options[i] = apimodel.PollOption{
Title: option,
@@ -1485,8 +1448,8 @@ func (c *Converter) StatusToEditHistory(
}
}
- // Append this status edit to the return slice.
- editHistory = append(editHistory, &apimodel.StatusEdit{
+ // Set status edit on return slice.
+ apiEdits[i] = &apimodel.StatusEdit{
CreatedAt: util.FormatISO8601(edit.CreatedAt),
Content: edit.Content,
SpoilerText: edit.ContentWarning,
@@ -1495,10 +1458,10 @@ func (c *Converter) StatusToEditHistory(
Poll: apiPoll,
MediaAttachments: apiAttachments,
Emojis: apiEmojis, // same models used for whole status + all edits
- })
+ }
}
- return editHistory, nil
+ return apiEdits, nil
}
// VisToAPIVis converts a gts visibility into its api equivalent
@@ -2277,6 +2240,7 @@ func (c *Converter) MarkersToAPIMarker(ctx context.Context, markers []*gtsmodel.
// PollToAPIPoll converts a database (gtsmodel) Poll into an API model representation appropriate for the given requesting account.
func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Account, poll *gtsmodel.Poll) (*apimodel.Poll, error) {
+
// Ensure the poll model is fully populated for src status.
if err := c.state.DB.PopulatePoll(ctx, poll); err != nil {
return nil, gtserror.Newf("error populating poll: %w", err)
@@ -2290,11 +2254,13 @@ func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Accou
ownChoices *[]int
isAuthor bool
expiresAt *string
- emojis []apimodel.Emoji
)
// Preallocate a slice of frontend model poll choices.
options = make([]apimodel.PollOption, len(poll.Options))
+ if len(options) != len(poll.Options) {
+ panic(gtserror.New("bound check elimination"))
+ }
// Add the titles to all of the options.
for i, title := range poll.Options {
@@ -2364,17 +2330,11 @@ func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Accou
expiresAt = &str
}
- var err error
-
- // Try to inherit emojis from parent status.
- emojis, err = c.convertEmojisToAPIEmojis(ctx,
+ // Get emojis from parent status.
+ apiEmojis := c.emojisToAPI(ctx,
poll.Status.Emojis,
poll.Status.EmojiIDs,
)
- if err != nil {
- log.Errorf(ctx, "error converting emojis from parent status: %v", err)
- emojis = []apimodel.Emoji{} // fallback to empty slice.
- }
return &apimodel.Poll{
ID: poll.ID,
@@ -2386,42 +2346,10 @@ func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Accou
Voted: hasVoted,
OwnVotes: ownChoices,
Options: options,
- Emojis: emojis,
+ Emojis: apiEmojis,
}, nil
}
-// convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied.
-func (c *Converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]*apimodel.Attachment, error) {
- var errs gtserror.MultiError
-
- if len(attachments) == 0 && len(attachmentIDs) > 0 {
- // GTS model attachments were not populated
-
- var err error
-
- // Fetch GTS models for attachment IDs
- attachments, err = c.state.DB.GetAttachmentsByIDs(ctx, attachmentIDs)
- if err != nil {
- errs.Appendf("error fetching attachments from database: %w", err)
- }
- }
-
- // Preallocate expected frontend slice
- apiAttachments := make([]*apimodel.Attachment, 0, len(attachments))
-
- // Convert GTS models to frontend models
- for _, attachment := range attachments {
- apiAttachment, err := c.AttachmentToAPIAttachment(ctx, attachment)
- if err != nil {
- errs.Appendf("error converting attchment %s to api attachment: %w", attachment.ID, err)
- continue
- }
- apiAttachments = append(apiAttachments, &apiAttachment)
- }
-
- return apiAttachments, errs.Combine()
-}
-
// FilterToAPIFiltersV1 converts one GTS model filter into an API v1 filter list
func FilterToAPIFiltersV1(filter *gtsmodel.Filter) []*apimodel.FilterV1 {
apiFilters := make([]*apimodel.FilterV1, 0, len(filter.Keywords))
@@ -2537,107 +2465,6 @@ func FilterStatusToAPIFilterStatus(filterStatus *gtsmodel.FilterStatus) *apimode
}
}
-// convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied.
-func (c *Converter) convertEmojisToAPIEmojis(ctx context.Context, emojis []*gtsmodel.Emoji, emojiIDs []string) ([]apimodel.Emoji, error) {
- var errs gtserror.MultiError
-
- // GTS model attachments were not populated
- if len(emojis) == 0 && len(emojiIDs) > 0 {
- var err error
-
- // Fetch GTS models for emoji IDs
- emojis, err = c.state.DB.GetEmojisByIDs(ctx, emojiIDs)
- if err != nil {
- return nil, gtserror.Newf("db error fetching emojis: %w", err)
- }
- }
-
- // Preallocate expected frontend slice of emojis.
- apiEmojis := make([]apimodel.Emoji, 0, len(emojis))
- for _, emoji := range emojis {
-
- // Skip adding emojis that are
- // uncached, the empty URLs can
- // cause issues with some clients.
- if !*emoji.Cached {
- continue
- }
-
- // Convert each to a frontend API model emoji.
- apiEmoji, err := c.EmojiToAPIEmoji(ctx, emoji)
- if err != nil {
- errs.Appendf("error converting emoji %s to api emoji: %w", emoji.ID, err)
- continue
- }
-
- // Append converted emoji to return slice.
- apiEmojis = append(apiEmojis, apiEmoji)
- }
-
- return apiEmojis, errs.Combine()
-}
-
-// convertMentionsToAPIMentions will convert a slice of GTS model mentions to frontend API model mentions, falling back to IDs if no GTS models supplied.
-func (c *Converter) convertMentionsToAPIMentions(ctx context.Context, mentions []*gtsmodel.Mention, mentionIDs []string) ([]apimodel.Mention, error) {
- var errs gtserror.MultiError
-
- if len(mentions) == 0 && len(mentionIDs) > 0 {
- var err error
-
- // GTS model mentions were not populated
- //
- // Fetch GTS models for mention IDs
- mentions, err = c.state.DB.GetMentions(ctx, mentionIDs)
- if err != nil {
- errs.Appendf("error fetching mentions from database: %w", err)
- }
- }
-
- // Preallocate expected frontend slice
- apiMentions := make([]apimodel.Mention, 0, len(mentions))
-
- // Convert GTS models to frontend models
- for _, mention := range mentions {
- apiMention, err := c.MentionToAPIMention(ctx, mention)
- if err != nil {
- errs.Appendf("error converting mention %s to api mention: %w", mention.ID, err)
- continue
- }
- apiMentions = append(apiMentions, apiMention)
- }
-
- return apiMentions, errs.Combine()
-}
-
-// convertTagsToAPITags will convert a slice of GTS model tags to frontend API model tags, falling back to IDs if no GTS models supplied.
-func (c *Converter) convertTagsToAPITags(ctx context.Context, tags []*gtsmodel.Tag, tagIDs []string) ([]apimodel.Tag, error) {
- var errs gtserror.MultiError
-
- if len(tags) == 0 && len(tagIDs) > 0 {
- var err error
-
- tags, err = c.state.DB.GetTags(ctx, tagIDs)
- if err != nil {
- errs.Appendf("error fetching tags from database: %w", err)
- }
- }
-
- // Preallocate expected frontend slice
- apiTags := make([]apimodel.Tag, 0, len(tags))
-
- // Convert GTS models to frontend models
- for _, tag := range tags {
- apiTag, err := c.TagToAPITag(ctx, tag, false, nil)
- if err != nil {
- errs.Appendf("error converting tag %s to api tag: %w", tag.ID, err)
- continue
- }
- apiTags = append(apiTags, apiTag)
- }
-
- return apiTags, errs.Combine()
-}
-
// ThemesToAPIThemes converts a slice of gtsmodel Themes into apimodel Themes.
func (c *Converter) ThemesToAPIThemes(themes []*gtsmodel.Theme) []apimodel.Theme {
apiThemes := make([]apimodel.Theme, len(themes))
@@ -2667,9 +2494,10 @@ func (c *Converter) InteractionPolicyToAPIInteractionPolicy(
policy *gtsmodel.InteractionPolicy,
status *gtsmodel.Status,
requester *gtsmodel.Account,
-) (*apimodel.InteractionPolicy, error) {
- apiPolicy := new(apimodel.InteractionPolicy)
-
+) (
+ apiPolicy apimodel.InteractionPolicy,
+ err error,
+) {
// gtsmodel CanLike -> apimodel CanFavourite
if policy.CanLike != nil {
// Use the set CanLike value.
@@ -2739,8 +2567,7 @@ func (c *Converter) InteractionPolicyToAPIInteractionPolicy(
likeable, err := c.intFilter.StatusLikeable(ctx, requester, status)
if err != nil {
- err := gtserror.Newf("error checking status likeable by requester: %w", err)
- return nil, err
+ return apiPolicy, gtserror.Newf("error checking status likeable by requester: %w", err)
}
if likeable.Permission == gtsmodel.PolicyPermissionAutomaticApproval {
@@ -2759,8 +2586,7 @@ func (c *Converter) InteractionPolicyToAPIInteractionPolicy(
replyable, err := c.intFilter.StatusReplyable(ctx, requester, status)
if err != nil {
- err := gtserror.Newf("error checking status replyable by requester: %w", err)
- return nil, err
+ return apiPolicy, gtserror.Newf("error checking status replyable by requester: %w", err)
}
if replyable.Permission == gtsmodel.PolicyPermissionAutomaticApproval {
@@ -2779,8 +2605,7 @@ func (c *Converter) InteractionPolicyToAPIInteractionPolicy(
boostable, err := c.intFilter.StatusBoostable(ctx, requester, status)
if err != nil {
- err := gtserror.Newf("error checking status boostable by requester: %w", err)
- return nil, err
+ return apiPolicy, gtserror.Newf("error checking status boostable by requester: %w", err)
}
if boostable.Permission == gtsmodel.PolicyPermissionAutomaticApproval {
@@ -3000,10 +2825,7 @@ func (c *Converter) WebPushSubscriptionToAPIWebPushSubscription(
}, nil
}
-func (c *Converter) TokenToAPITokenInfo(
- ctx context.Context,
- token *gtsmodel.Token,
-) (*apimodel.TokenInfo, error) {
+func (c *Converter) TokenToAPITokenInfo(ctx context.Context, token *gtsmodel.Token) (*apimodel.TokenInfo, error) {
createdAt, err := id.TimeFromULID(token.ID)
if err != nil {
err := gtserror.Newf("error parsing time from token id: %w", err)
@@ -3021,11 +2843,7 @@ func (c *Converter) TokenToAPITokenInfo(
return nil, err
}
- apiApplication, err := c.AppToAPIAppPublic(ctx, application)
- if err != nil {
- err := gtserror.Newf("error converting application to api application: %w", err)
- return nil, err
- }
+ apiApplication := AppToAPIAppPublic(application)
return &apimodel.TokenInfo{
ID: token.ID,
@@ -3036,56 +2854,197 @@ func (c *Converter) TokenToAPITokenInfo(
}, nil
}
-func (c *Converter) ScheduledStatusToAPIScheduledStatus(
- ctx context.Context,
- scheduledStatus *gtsmodel.ScheduledStatus,
-) (*apimodel.ScheduledStatus, error) {
- apiAttachments, err := c.convertAttachmentsToAPIAttachments(
- ctx,
- scheduledStatus.MediaAttachments,
- scheduledStatus.MediaIDs,
- )
- if err != nil {
- log.Errorf(ctx, "error converting status attachments: %v", err)
- }
-
- scheduledAt := util.FormatISO8601(scheduledStatus.ScheduledAt)
+func (c *Converter) ScheduledStatusToAPIScheduledStatus(ctx context.Context, status *gtsmodel.ScheduledStatus) (*apimodel.ScheduledStatus, error) {
+ scheduledAt := util.FormatISO8601(status.ScheduledAt)
apiScheduledStatus := &apimodel.ScheduledStatus{
- ID: scheduledStatus.ID,
+ ID: status.ID,
ScheduledAt: scheduledAt,
Params: &apimodel.ScheduledStatusParams{
- Text: scheduledStatus.Text,
- MediaIDs: scheduledStatus.MediaIDs,
- Sensitive: *scheduledStatus.Sensitive,
- SpoilerText: scheduledStatus.SpoilerText,
- Visibility: VisToAPIVis(scheduledStatus.Visibility),
- InReplyToID: scheduledStatus.InReplyToID,
- Language: scheduledStatus.Language,
- ApplicationID: scheduledStatus.ApplicationID,
- LocalOnly: *scheduledStatus.LocalOnly,
- ContentType: apimodel.StatusContentType(scheduledStatus.ContentType),
+ Text: status.Text,
+ MediaIDs: status.MediaIDs,
+ Sensitive: *status.Sensitive,
+ SpoilerText: status.SpoilerText,
+ Visibility: VisToAPIVis(status.Visibility),
+ InReplyToID: status.InReplyToID,
+ Language: status.Language,
+ ApplicationID: status.ApplicationID,
+ LocalOnly: *status.LocalOnly,
+ ContentType: apimodel.StatusContentType(status.ContentType),
ScheduledAt: nil,
},
- MediaAttachments: apiAttachments,
+ MediaAttachments: c.attachmentsToAPI(ctx, status.MediaAttachments, status.MediaIDs),
}
- if len(scheduledStatus.Poll.Options) > 1 {
+ if len(status.Poll.Options) > 1 {
apiScheduledStatus.Params.Poll = &apimodel.ScheduledStatusParamsPoll{
- Options: scheduledStatus.Poll.Options,
- ExpiresIn: scheduledStatus.Poll.ExpiresIn,
- Multiple: *scheduledStatus.Poll.Multiple,
- HideTotals: *scheduledStatus.Poll.HideTotals,
+ Options: status.Poll.Options,
+ ExpiresIn: status.Poll.ExpiresIn,
+ Multiple: *status.Poll.Multiple,
+ HideTotals: *status.Poll.HideTotals,
}
}
- if scheduledStatus.InteractionPolicy != nil {
- apiInteractionPolicy, err := c.InteractionPolicyToAPIInteractionPolicy(ctx, scheduledStatus.InteractionPolicy, nil, nil)
+ if status.InteractionPolicy != nil {
+ apiInteractionPolicy, err := c.InteractionPolicyToAPIInteractionPolicy(ctx, status.InteractionPolicy, nil, nil)
if err != nil {
return nil, gtserror.Newf("error converting interaction policy: %w", err)
}
- apiScheduledStatus.Params.InteractionPolicy = apiInteractionPolicy
+ apiScheduledStatus.Params.InteractionPolicy = &apiInteractionPolicy
}
return apiScheduledStatus, nil
}
+
+// attachmentsToAPI converts database model media attachments (fetching
+// using IDs if necessary) to frontend API attachment models. all errors
+// are caught and logged, with the calling function name as a prefix.
+func (c *Converter) attachmentsToAPI(
+ ctx context.Context,
+ attachments []*gtsmodel.MediaAttachment,
+ attachmentIDs []string,
+) []*apimodel.Attachment {
+ caller := log.Caller(3)
+
+ // Check if media attachments are populated.
+ if len(attachments) != len(attachmentIDs) {
+ var err error
+
+ // Media attachments are not populated, fetch from the database.
+ attachments, err = c.state.DB.GetAttachmentsByIDs(ctx, attachmentIDs)
+ if err != nil {
+
+ log.Errorf(ctx, "%s: error getting media: %v", caller, err)
+ return []*apimodel.Attachment{}
+ }
+ }
+
+ // Convert all db media attachments to slice of API models.
+ apiModels := make([]*apimodel.Attachment, len(attachments))
+ if len(apiModels) != len(attachments) {
+ panic(gtserror.New("bound check elimination"))
+ }
+ for i, media := range attachments {
+ apiModel := AttachmentToAPIAttachment(media)
+ apiModels[i] = &apiModel
+ }
+
+ return apiModels
+}
+
+// emojisToAPI converts database model emojis (fetching using IDs if
+// necessary) to frontend API emoji models. all errors are caught and
+// logged, with the calling function name as a prefix.
+func (c *Converter) emojisToAPI(
+ ctx context.Context,
+ emojis []*gtsmodel.Emoji,
+ emojiIDs []string,
+) []apimodel.Emoji {
+ caller := log.Caller(3)
+
+ // Check if emojis are populated.
+ if len(emojis) != len(emojiIDs) {
+ var err error
+
+ // Emojis are not populated, fetch from the database.
+ emojis, err = c.state.DB.GetEmojisByIDs(ctx, emojiIDs)
+ if err != nil {
+
+ log.Errorf(ctx, "%s: error getting emojis: %v", caller, err)
+ return []apimodel.Emoji{}
+ }
+ }
+
+ // Preallocate a biggest-case slice of frontend emojis.
+ apiModels := make([]apimodel.Emoji, 0, len(emojis))
+ for _, emoji := range emojis {
+
+ // Convert each database emoji to API model.
+ apiModel, err := c.EmojiToAPIEmoji(ctx, emoji)
+ if err != nil {
+ log.Errorf(ctx, "%s: error converting emoji %s: %v", caller, emoji.ShortcodeDomain(), err)
+ continue
+ }
+
+ // Append API model to the return slice.
+ apiModels = append(apiModels, apiModel)
+ }
+
+ return apiModels
+}
+
+// mentionsToAPI converts database model mentions (fetching using IDs if
+// necessary) to frontend API mention models. all errors are caught and
+// logged, with the calling function name as a prefix.
+func (c *Converter) mentionsToAPI(
+ ctx context.Context,
+ mentions []*gtsmodel.Mention,
+ mentionIDs []string,
+) []apimodel.Mention {
+ caller := log.Caller(3)
+
+ // Check if mentions are populated.
+ if len(mentions) != len(mentionIDs) {
+ var err error
+
+ // Mentions are not populated, fetch from the database.
+ mentions, err = c.state.DB.GetMentions(ctx, mentionIDs)
+ if err != nil {
+
+ log.Errorf(ctx, "%s: error getting mentions: %v", caller, err)
+ return []apimodel.Mention{}
+ }
+ }
+
+ // Preallocate a biggest-case slice of frontend mentions.
+ apiModels := make([]apimodel.Mention, 0, len(mentions))
+ for _, mention := range mentions {
+
+ // Convert each database mention to frontend API model.
+ apiModel, err := c.MentionToAPIMention(ctx, mention)
+ if err != nil {
+ log.Errorf(ctx, "%s: error converting mention %s: %v", caller, mention.ID, err)
+ continue
+ }
+
+ // Append API model to the return slice.
+ apiModels = append(apiModels, apiModel)
+ }
+
+ return apiModels
+}
+
+// tagsToAPI converts database model tags (fetching using IDs if
+// necessary) to frontend API tag models. all errors are caught
+// and logged, with the calling function name as a prefix.
+func (c *Converter) tagsToAPI(
+ ctx context.Context,
+ tags []*gtsmodel.Tag,
+ tagIDs []string,
+) []apimodel.Tag {
+ caller := log.Caller(3)
+
+ // Check if mentions are populated.
+ if len(tags) != len(tagIDs) {
+ var err error
+
+ // Tags not populated, fetch from database.
+ tags, err = c.state.DB.GetTags(ctx, tagIDs)
+ if err != nil {
+
+ log.Errorf(ctx, "%s: error getting tags: %v", caller, err)
+ return []apimodel.Tag{}
+ }
+ }
+
+ // Convert all db tags to slice of API models.
+ apiModels := make([]apimodel.Tag, len(tags))
+ if len(apiModels) != len(tags) {
+ panic(gtserror.New("bound check elimination"))
+ }
+ for i, tag := range tags {
+ apiModels[i] = TagToAPITag(tag, false, nil)
+ }
+
+ return apiModels
+}