summaryrefslogtreecommitdiff
path: root/internal/typeutils
diff options
context:
space:
mode:
Diffstat (limited to 'internal/typeutils')
-rw-r--r--internal/typeutils/internaltofrontend.go172
-rw-r--r--internal/typeutils/internaltofrontend_test.go130
2 files changed, 279 insertions, 23 deletions
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index e0276a53b..3208fcb51 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -1216,21 +1216,6 @@ func (c *Converter) StatusToWebStatus(
return webStatus, nil
}
-// StatusToAPIStatusSource returns the *apimodel.StatusSource of the given status.
-// Callers should check beforehand whether a requester has permission to view the
-// source of the status, and ensure they're passing only a local status into this function.
-func (c *Converter) StatusToAPIStatusSource(ctx context.Context, s *gtsmodel.Status) (*apimodel.StatusSource, error) {
- // TODO: remove this when edit support is added.
- text := "**STATUS EDITS ARE NOT CURRENTLY SUPPORTED IN GOTOSOCIAL (coming in 2024)**\n" +
- "You can review the original text of your status below, but you will not be able to submit this edit.\n\n---\n\n" + s.Text
-
- return &apimodel.StatusSource{
- ID: s.ID,
- Text: text,
- SpoilerText: s.ContentWarning,
- }, nil
-}
-
// statusToFrontend is a package internal function for
// parsing a status into its initial frontend representation.
//
@@ -1472,6 +1457,149 @@ func (c *Converter) baseStatusToFrontend(
return apiStatus, nil
}
+// StatusToAPIEdits converts a status and its historical edits (if any) to a slice of API model status edits.
+func (c *Converter) StatusToAPIEdits(ctx context.Context, status *gtsmodel.Status) ([]*apimodel.StatusEdit, error) {
+ var media map[string]*gtsmodel.MediaAttachment
+
+ // Gather attachments of status AND edits.
+ attachmentIDs := status.AllAttachmentIDs()
+ if len(attachmentIDs) > 0 {
+
+ // Fetch all of the gathered status attachments from the database.
+ attachments, err := c.state.DB.GetAttachmentsByIDs(ctx, attachmentIDs)
+ if err != nil {
+ return nil, gtserror.Newf("error getting attachments from db: %w", err)
+ }
+
+ // Generate a lookup map in 'media' of status attachments by their IDs.
+ media = util.KeyBy(attachments, func(m *gtsmodel.MediaAttachment) string {
+ return m.ID
+ })
+ }
+
+ // Convert the status author account to API model.
+ apiAccount, err := c.AccountToAPIAccountPublic(ctx,
+ 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)
+ }
+
+ var votes []int
+ var options []string
+
+ if status.Poll != nil {
+ // Extract status poll options.
+ options = status.Poll.Options
+
+ // Show votes only if closed / allowed.
+ if !status.Poll.ClosedAt.IsZero() ||
+ !*status.Poll.HideCounts {
+ votes = status.Poll.Votes
+ }
+ }
+
+ // Append status itself to final slot in the edits
+ // so we can add its revision using the below loop.
+ edits := append(status.Edits, &gtsmodel.StatusEdit{ //nolint:gocritic
+ Content: status.Content,
+ ContentWarning: status.ContentWarning,
+ Sensitive: status.Sensitive,
+ PollOptions: options,
+ PollVotes: votes,
+ AttachmentIDs: status.AttachmentIDs,
+ AttachmentDescriptions: nil, // no change from current
+ CreatedAt: status.UpdatedAt,
+ })
+
+ // Iterate through status edits, starting at newest.
+ apiEdits := make([]*apimodel.StatusEdit, 0, len(edits))
+ for i := len(edits) - 1; i >= 0; i-- {
+ edit := edits[i]
+
+ // Iterate through edit attachment IDs, getting model from 'media' lookup.
+ apiAttachments := make([]*apimodel.Attachment, 0, len(edit.AttachmentIDs))
+ for _, id := range edit.AttachmentIDs {
+ attachment, ok := media[id]
+ if !ok {
+ 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.
+ apiAttachments = append(apiAttachments, &apiAttachment)
+ }
+
+ // If media descriptions are set, update API model descriptions.
+ if len(edit.AttachmentIDs) == len(edit.AttachmentDescriptions) {
+ var j int
+ for i, id := range edit.AttachmentIDs {
+ descr := edit.AttachmentDescriptions[i]
+ for ; j < len(apiAttachments); j++ {
+ if apiAttachments[j].ID == id {
+ apiAttachments[j].Description = &descr
+ break
+ }
+ }
+ }
+ }
+
+ // Attach status poll if set.
+ var apiPoll *apimodel.Poll
+ if len(edit.PollOptions) > 0 {
+ apiPoll = new(apimodel.Poll)
+
+ // Iterate through poll options and attach to API poll model.
+ apiPoll.Options = make([]apimodel.PollOption, len(edit.PollOptions))
+ for i, option := range edit.PollOptions {
+ apiPoll.Options[i] = apimodel.PollOption{
+ Title: option,
+ }
+ }
+
+ // If poll votes are attached, set vote counts.
+ if len(edit.PollVotes) == len(apiPoll.Options) {
+ for i, votes := range edit.PollVotes {
+ apiPoll.Options[i].VotesCount = &votes
+ }
+ }
+ }
+
+ // Append this status edit to the return slice.
+ apiEdits = append(apiEdits, &apimodel.StatusEdit{
+ CreatedAt: util.FormatISO8601(edit.CreatedAt),
+ Content: edit.Content,
+ SpoilerText: edit.ContentWarning,
+ Sensitive: util.PtrOrZero(edit.Sensitive),
+ Account: apiAccount,
+ Poll: apiPoll,
+ MediaAttachments: apiAttachments,
+ Emojis: apiEmojis, // same models used for whole status + all edits
+ })
+ }
+
+ return apiEdits, nil
+}
+
// VisToAPIVis converts a gts visibility into its api equivalent
func (c *Converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apimodel.Visibility {
switch m {
@@ -1488,7 +1616,7 @@ func (c *Converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apim
}
// InstanceRuleToAdminAPIRule converts a local instance rule into its api equivalent for serving at /api/v1/admin/instance/rules/:id
-func (c *Converter) InstanceRuleToAPIRule(r gtsmodel.Rule) apimodel.InstanceRule {
+func InstanceRuleToAPIRule(r gtsmodel.Rule) apimodel.InstanceRule {
return apimodel.InstanceRule{
ID: r.ID,
Text: r.Text,
@@ -1496,18 +1624,16 @@ func (c *Converter) InstanceRuleToAPIRule(r gtsmodel.Rule) apimodel.InstanceRule
}
// InstanceRulesToAPIRules converts all local instance rules into their api equivalent for serving at /api/v1/instance/rules
-func (c *Converter) InstanceRulesToAPIRules(r []gtsmodel.Rule) []apimodel.InstanceRule {
+func InstanceRulesToAPIRules(r []gtsmodel.Rule) []apimodel.InstanceRule {
rules := make([]apimodel.InstanceRule, len(r))
-
for i, v := range r {
- rules[i] = c.InstanceRuleToAPIRule(v)
+ rules[i] = InstanceRuleToAPIRule(v)
}
-
return rules
}
// InstanceRuleToAdminAPIRule converts a local instance rule into its api equivalent for serving at /api/v1/admin/instance/rules/:id
-func (c *Converter) InstanceRuleToAdminAPIRule(r *gtsmodel.Rule) *apimodel.AdminInstanceRule {
+func InstanceRuleToAdminAPIRule(r *gtsmodel.Rule) *apimodel.AdminInstanceRule {
return &apimodel.AdminInstanceRule{
ID: r.ID,
CreatedAt: util.FormatISO8601(r.CreatedAt),
@@ -1540,7 +1666,7 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
ApprovalRequired: true, // approval always required
InvitesEnabled: false, // todo: not supported yet
MaxTootChars: uint(config.GetStatusesMaxChars()), // #nosec G115 -- Already validated.
- Rules: c.InstanceRulesToAPIRules(i.Rules),
+ Rules: InstanceRulesToAPIRules(i.Rules),
Terms: i.Terms,
TermsRaw: i.TermsText,
}
@@ -1674,7 +1800,7 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
CustomCSS: i.CustomCSS,
Usage: apimodel.InstanceV2Usage{}, // todo: not implemented
Languages: config.GetInstanceLanguages().TagStrs(),
- Rules: c.InstanceRulesToAPIRules(i.Rules),
+ Rules: InstanceRulesToAPIRules(i.Rules),
Terms: i.Terms,
TermsText: i.TermsText,
}
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index 0ec9ea05f..39a9bd9d4 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -3737,6 +3737,136 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
}`, string(b))
}
+func (suite *InternalToFrontendTestSuite) TestStatusToAPIEdits() {
+ ctx, cncl := context.WithCancel(context.Background())
+ defer cncl()
+
+ statusID := suite.testStatuses["local_account_1_status_9"].ID
+
+ status, err := suite.state.DB.GetStatusByID(ctx, statusID)
+ suite.NoError(err)
+
+ err = suite.state.DB.PopulateStatusEdits(ctx, status)
+ suite.NoError(err)
+
+ apiEdits, err := suite.typeconverter.StatusToAPIEdits(ctx, status)
+ suite.NoError(err)
+
+ b, err := json.MarshalIndent(apiEdits, "", " ")
+ suite.NoError(err)
+
+ suite.Equal(`[
+ {
+ "content": "\u003cp\u003ethis is the latest revision of the status, with a content-warning\u003c/p\u003e",
+ "spoiler_text": "edited status",
+ "sensitive": false,
+ "created_at": "2024-11-01T09:02:00.000Z",
+ "account": {
+ "id": "01F8MH1H7YV1Z7D2C8K2730QBF",
+ "username": "the_mighty_zork",
+ "acct": "the_mighty_zork",
+ "display_name": "original zork (he/they)",
+ "locked": false,
+ "discoverable": true,
+ "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.jpg",
+ "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
+ "avatar_description": "a green goblin looking nasty",
+ "avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S",
+ "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
+ "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
+ "header_description": "A very old-school screenshot of the original team fortress mod for quake",
+ "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
+ "followers_count": 2,
+ "following_count": 2,
+ "statuses_count": 9,
+ "last_status_at": "2024-11-01",
+ "emojis": [],
+ "fields": [],
+ "enable_rss": true
+ },
+ "poll": null,
+ "media_attachments": [],
+ "emojis": []
+ },
+ {
+ "content": "\u003cp\u003ethis is the first status edit! now with content-warning\u003c/p\u003e",
+ "spoiler_text": "edited status",
+ "sensitive": false,
+ "created_at": "2024-11-01T09:01:00.000Z",
+ "account": {
+ "id": "01F8MH1H7YV1Z7D2C8K2730QBF",
+ "username": "the_mighty_zork",
+ "acct": "the_mighty_zork",
+ "display_name": "original zork (he/they)",
+ "locked": false,
+ "discoverable": true,
+ "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.jpg",
+ "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
+ "avatar_description": "a green goblin looking nasty",
+ "avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S",
+ "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
+ "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
+ "header_description": "A very old-school screenshot of the original team fortress mod for quake",
+ "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
+ "followers_count": 2,
+ "following_count": 2,
+ "statuses_count": 9,
+ "last_status_at": "2024-11-01",
+ "emojis": [],
+ "fields": [],
+ "enable_rss": true
+ },
+ "poll": null,
+ "media_attachments": [],
+ "emojis": []
+ },
+ {
+ "content": "\u003cp\u003ethis is the original status\u003c/p\u003e",
+ "spoiler_text": "",
+ "sensitive": false,
+ "created_at": "2024-11-01T09:00:00.000Z",
+ "account": {
+ "id": "01F8MH1H7YV1Z7D2C8K2730QBF",
+ "username": "the_mighty_zork",
+ "acct": "the_mighty_zork",
+ "display_name": "original zork (he/they)",
+ "locked": false,
+ "discoverable": true,
+ "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.jpg",
+ "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
+ "avatar_description": "a green goblin looking nasty",
+ "avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S",
+ "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
+ "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
+ "header_description": "A very old-school screenshot of the original team fortress mod for quake",
+ "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
+ "followers_count": 2,
+ "following_count": 2,
+ "statuses_count": 9,
+ "last_status_at": "2024-11-01",
+ "emojis": [],
+ "fields": [],
+ "enable_rss": true
+ },
+ "poll": null,
+ "media_attachments": [],
+ "emojis": []
+ }
+]`, string(b))
+}
+
func TestInternalToFrontendTestSuite(t *testing.T) {
suite.Run(t, new(InternalToFrontendTestSuite))
}