diff options
Diffstat (limited to 'internal')
21 files changed, 487 insertions, 664 deletions
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index f7bf84961..7f401ed57 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -112,7 +112,7 @@ func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.WebA return nil, gtserror.NewErrorInternalError(err) } - webAccount, err := p.converter.AccountToWebAccount(ctx, targetAccount) + webAccount, err := p.converter.AccountToWebAccount(ctx, targetAccount, nil) if err != nil { err := gtserror.Newf("error converting account: %w", err) return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/account/interactionpolicies.go b/internal/processing/account/interactionpolicies.go index 405c92230..d581112a6 100644 --- a/internal/processing/account/interactionpolicies.go +++ b/internal/processing/account/interactionpolicies.go @@ -89,10 +89,10 @@ func (p *Processor) DefaultInteractionPoliciesGet( } return &apimodel.DefaultPolicies{ - Direct: *directAPI, - Private: *privateAPI, - Unlisted: *unlistedAPI, - Public: *publicAPI, + Direct: directAPI, + Private: privateAPI, + Unlisted: unlistedAPI, + Public: publicAPI, }, nil } diff --git a/internal/processing/account/rss.go b/internal/processing/account/rss.go index 205027528..d6f367566 100644 --- a/internal/processing/account/rss.go +++ b/internal/processing/account/rss.go @@ -76,16 +76,44 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string, lastPostAt := account.Stats.LastStatusAt return func() (*feeds.Feed, gtserror.WithCode) { - // Assemble author namestring once only. - author := "@" + account.Username + "@" + config.GetAccountDomain() + var image *feeds.Image + + // Assemble author namestring. + author := "@" + account.Username + + "@" + config.GetAccountDomain() + + // Check if account has an avatar media attachment. + if id := account.AvatarMediaAttachmentID; id != "" { + if account.AvatarMediaAttachment == nil { + var err error + + // Populate the account's avatar media attachment from database by its ID. + account.AvatarMediaAttachment, err = p.state.DB.GetAttachmentByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting account avatar: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + } - // Derive image/thumbnail for this account (may be nil if no media). - image, errWithCode := p.rssImageForAccount(ctx, account, author) - if errWithCode != nil { - return nil, errWithCode + // If avatar is found, use as feed image. + if account.AvatarMediaAttachment != nil { + image = &feeds.Image{ + Title: "Avatar for " + author, + Url: account.AvatarMediaAttachment.Thumbnail.URL, + Link: account.URL, + } + } } + // Start creating feed. feed := &feeds.Feed{ + // we specifcally do not set the author, as a lot + // of feed readers rely on the RSS standard of the + // author being an email with optional name. but + // our @username@domain identifiers break this. + // + // attribution is handled in the title/description. + Title: "Posts from " + author, Description: "Posts from " + author, Link: &feeds.Link{Href: account.URL}, @@ -128,6 +156,17 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string, return nil, gtserror.NewErrorInternalError(err) } + // Check for no statuses. + if len(statuses) == 0 { + return feed, nil + } + + // Get next / prev paging parameters. + lo := statuses[len(statuses)-1].ID + hi := statuses[0].ID + next := page.Next(lo, hi) + prev := page.Prev(lo, hi) + // Add each status to the rss feed. for _, status := range statuses { item, err := p.converter.StatusToRSSItem(ctx, status) @@ -138,35 +177,11 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string, feed.Add(item) } + // TODO: when we have some manner of supporting + // atom:link in RSS (and Atom), set the paging + // parameters for next / prev feed pages here. + _, _ = next, prev + return feed, nil }, lastPostAt, nil } - -func (p *Processor) rssImageForAccount(ctx context.Context, account *gtsmodel.Account, author string) (*feeds.Image, gtserror.WithCode) { - if account.AvatarMediaAttachmentID == "" { - // No image, no problem! - return nil, nil - } - - // Ensure account avatar attachment populated. - if account.AvatarMediaAttachment == nil { - var err error - account.AvatarMediaAttachment, err = p.state.DB.GetAttachmentByID(ctx, account.AvatarMediaAttachmentID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - // No attachment found with this ID (race condition?). - return nil, nil - } - - // Real db error. - err = gtserror.Newf("db error fetching avatar media attachment: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } - } - - return &feeds.Image{ - Url: account.AvatarMediaAttachment.Thumbnail.URL, - Title: "Avatar for " + author, - Link: account.URL, - }, nil -} diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go index b053a3795..75aa20891 100644 --- a/internal/processing/account/rss_test.go +++ b/internal/processing/account/rss_test.go @@ -44,7 +44,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() { <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link> <description>@admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"</description> <content:encoded><![CDATA[<p>🐕🐕🐕🐕🐕</p>]]></content:encoded> - <author>@admin@localhost:8080</author> <guid isPermaLink="true">http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid> <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate> <source>http://localhost:8080/@admin/feed.rss</source> @@ -54,7 +53,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() { <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link> <description>@admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"</description> <content:encoded><![CDATA[<p>hello world! <a href="http://localhost:8080/tags/welcome" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>welcome</span></a> ! first post on the instance <img src="http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png" title=":rainbow:" alt=":rainbow:" width="25" height="25" /> !</p>]]></content:encoded> - <author>@admin@localhost:8080</author> <enclosure url="http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg" length="62529" type="image/jpeg"></enclosure> <guid isPermaLink="true">http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid> <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate> @@ -78,11 +76,7 @@ func (suite *GetRSSTestSuite) TestGetAccountAtomAdmin() { <id>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</id> <content type="html"><p>🐕🐕🐕🐕🐕</p></content> <link href="http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37" rel="alternate"></link> - <link href="" rel="enclosure"></link> <summary type="html">@admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"</summary> - <author> - <name>@admin@localhost:8080</name> - </author> </entry> <entry> <title>hello world! #welcome ! first post on the instance :rainbow: !</title> @@ -92,9 +86,6 @@ func (suite *GetRSSTestSuite) TestGetAccountAtomAdmin() { <link href="http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R" rel="alternate"></link> <link href="http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg" rel="enclosure" type="image/jpeg" length="62529"></link> <summary type="html">@admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"</summary> - <author> - <name>@admin@localhost:8080</name> - </author> </entry> </feed>`) } @@ -114,15 +105,7 @@ func (suite *GetRSSTestSuite) TestGetAccountJSONAdmin() { "title": "open to see some \u003cstrong\u003epuppies\u003c/strong\u003e", "content_html": "\u003cp\u003e🐕🐕🐕🐕🐕\u003c/p\u003e", "summary": "@admin@localhost:8080 made a new post: \"🐕🐕🐕🐕🐕\"", - "date_published": "2021-10-20T12:36:45Z", - "author": { - "name": "@admin@localhost:8080" - }, - "authors": [ - { - "name": "@admin@localhost:8080" - } - ] + "date_published": "2021-10-20T12:36:45Z" }, { "id": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", @@ -132,15 +115,7 @@ func (suite *GetRSSTestSuite) TestGetAccountJSONAdmin() { "content_html": "\u003cp\u003ehello world! \u003ca href=\"http://localhost:8080/tags/welcome\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\"\u003e#\u003cspan\u003ewelcome\u003c/span\u003e\u003c/a\u003e ! first post on the instance \u003cimg src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\" /\u003e !\u003c/p\u003e", "summary": "@admin@localhost:8080 posted 1 attachment: \"hello world! #welcome ! first post on the instance :rainbow: !\"", "image": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", - "date_published": "2021-10-20T11:36:45Z", - "author": { - "name": "@admin@localhost:8080" - }, - "authors": [ - { - "name": "@admin@localhost:8080" - } - ] + "date_published": "2021-10-20T11:36:45Z" } ] }`) @@ -165,7 +140,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { <link>http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</link> <description>@the_mighty_zork@localhost:8080 made a new post: "this is the latest revision of the status, with a content-warning"</description> <content:encoded><![CDATA[<p>this is the latest revision of the status, with a content-warning</p>]]></content:encoded> - <author>@the_mighty_zork@localhost:8080</author> <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</guid> <pubDate>Fri, 01 Nov 2024 09:00:00 +0000</pubDate> <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> @@ -210,7 +184,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { </div> </section> </code></pre><p>There, hope you liked that!</p>]]></content:encoded> - <author>@the_mighty_zork@localhost:8080</author> <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid> <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate> <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> @@ -220,7 +193,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link> <description>@the_mighty_zork@localhost:8080 made a new post: "hello everyone!"</description> <content:encoded><![CDATA[<p>hello everyone!</p>]]></content:encoded> - <author>@the_mighty_zork@localhost:8080</author> <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid> <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate> <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> @@ -248,7 +220,6 @@ func (suite *GetRSSTestSuite) TestGetAccountAtomZork() { <link>http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</link> <description>@the_mighty_zork@localhost:8080 made a new post: "this is the latest revision of the status, with a content-warning"</description> <content:encoded><![CDATA[<p>this is the latest revision of the status, with a content-warning</p>]]></content:encoded> - <author>@the_mighty_zork@localhost:8080</author> <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</guid> <pubDate>Fri, 01 Nov 2024 09:00:00 +0000</pubDate> <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> @@ -293,7 +264,6 @@ func (suite *GetRSSTestSuite) TestGetAccountAtomZork() { </div> </section> </code></pre><p>There, hope you liked that!</p>]]></content:encoded> - <author>@the_mighty_zork@localhost:8080</author> <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid> <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate> <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> @@ -303,7 +273,6 @@ func (suite *GetRSSTestSuite) TestGetAccountAtomZork() { <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link> <description>@the_mighty_zork@localhost:8080 made a new post: "hello everyone!"</description> <content:encoded><![CDATA[<p>hello everyone!</p>]]></content:encoded> - <author>@the_mighty_zork@localhost:8080</author> <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid> <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate> <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> @@ -328,15 +297,7 @@ func (suite *GetRSSTestSuite) TestGetAccountJSONZork() { "content_html": "\u003cp\u003ethis is the latest revision of the status, with a content-warning\u003c/p\u003e", "summary": "@the_mighty_zork@localhost:8080 made a new post: \"this is the latest revision of the status, with a content-warning\"", "date_published": "2024-11-01T09:00:00Z", - "date_modified": "2024-11-01T09:02:00Z", - "author": { - "name": "@the_mighty_zork@localhost:8080" - }, - "authors": [ - { - "name": "@the_mighty_zork@localhost:8080" - } - ] + "date_modified": "2024-11-01T09:02:00Z" }, { "id": "http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40", @@ -345,15 +306,7 @@ func (suite *GetRSSTestSuite) TestGetAccountJSONZork() { "title": "HTML in post", "content_html": "\u003cp\u003eHere's a bunch of HTML, read it and weep, weep then!\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-html\"\u003e\u0026lt;section class=\u0026#34;about-user\u0026#34;\u0026gt;\n \u0026lt;div class=\u0026#34;col-header\u0026#34;\u0026gt;\n \u0026lt;h2\u0026gt;About\u0026lt;/h2\u0026gt;\n \u0026lt;/div\u0026gt; \n \u0026lt;div class=\u0026#34;fields\u0026#34;\u0026gt;\n \u0026lt;h3 class=\u0026#34;sr-only\u0026#34;\u0026gt;Fields\u0026lt;/h3\u0026gt;\n \u0026lt;dl\u0026gt;\n \u0026lt;div class=\u0026#34;field\u0026#34;\u0026gt;\n \u0026lt;dt\u0026gt;should you follow me?\u0026lt;/dt\u0026gt;\n \u0026lt;dd\u0026gt;maybe!\u0026lt;/dd\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;field\u0026#34;\u0026gt;\n \u0026lt;dt\u0026gt;age\u0026lt;/dt\u0026gt;\n \u0026lt;dd\u0026gt;120\u0026lt;/dd\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;/dl\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;bio\u0026#34;\u0026gt;\n \u0026lt;h3 class=\u0026#34;sr-only\u0026#34;\u0026gt;Bio\u0026lt;/h3\u0026gt;\n \u0026lt;p\u0026gt;i post about things that concern me\u0026lt;/p\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;sr-only\u0026#34; role=\u0026#34;group\u0026#34;\u0026gt;\n \u0026lt;h3 class=\u0026#34;sr-only\u0026#34;\u0026gt;Stats\u0026lt;/h3\u0026gt;\n \u0026lt;span\u0026gt;Joined in Jun, 2022.\u0026lt;/span\u0026gt;\n \u0026lt;span\u0026gt;8 posts.\u0026lt;/span\u0026gt;\n \u0026lt;span\u0026gt;Followed by 1.\u0026lt;/span\u0026gt;\n \u0026lt;span\u0026gt;Following 1.\u0026lt;/span\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;accountstats\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt;\n \u0026lt;b\u0026gt;Joined\u0026lt;/b\u0026gt;\u0026lt;time datetime=\u0026#34;2022-06-04T13:12:00.000Z\u0026#34;\u0026gt;Jun, 2022\u0026lt;/time\u0026gt;\n \u0026lt;b\u0026gt;Posts\u0026lt;/b\u0026gt;\u0026lt;span\u0026gt;8\u0026lt;/span\u0026gt;\n \u0026lt;b\u0026gt;Followed by\u0026lt;/b\u0026gt;\u0026lt;span\u0026gt;1\u0026lt;/span\u0026gt;\n \u0026lt;b\u0026gt;Following\u0026lt;/b\u0026gt;\u0026lt;span\u0026gt;1\u0026lt;/span\u0026gt;\n \u0026lt;/div\u0026gt;\n\u0026lt;/section\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThere, hope you liked that!\u003c/p\u003e", "summary": "@the_mighty_zork@localhost:8080 made a new post: \"Here's a bunch of HTML, read it and weep, weep then!\n\n`+"```"+`html\n\u003csection class=\"about-user\"\u003e\n \u003cdiv class=\"col-header\"\u003e\n \u003ch2\u003eAbout\u003c/h2\u003e\n \u003c/div\u003e \n \u003cdiv class=\"fields\"\u003e\n \u003ch3 class=\"sr-only\"\u003eFields\u003c/h3\u003e\n \u003cdl\u003e\n...", - "date_published": "2023-12-10T09:24:00Z", - "author": { - "name": "@the_mighty_zork@localhost:8080" - }, - "authors": [ - { - "name": "@the_mighty_zork@localhost:8080" - } - ] + "date_published": "2023-12-10T09:24:00Z" }, { "id": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY", @@ -362,15 +315,7 @@ func (suite *GetRSSTestSuite) TestGetAccountJSONZork() { "title": "introduction post", "content_html": "\u003cp\u003ehello everyone!\u003c/p\u003e", "summary": "@the_mighty_zork@localhost:8080 made a new post: \"hello everyone!\"", - "date_published": "2021-10-20T10:40:37Z", - "author": { - "name": "@the_mighty_zork@localhost:8080" - }, - "authors": [ - { - "name": "@the_mighty_zork@localhost:8080" - } - ] + "date_published": "2021-10-20T10:40:37Z" } ] }`) diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go index 5c391bf82..8d568b9a8 100644 --- a/internal/processing/admin/emoji.go +++ b/internal/processing/admin/emoji.go @@ -32,6 +32,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/media" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" "codeberg.org/gruf/go-iotools" ) @@ -262,11 +263,7 @@ func (p *Processor) EmojiCategoriesGet( apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories)) for _, category := range categories { - apiCategory, err := p.converter.EmojiCategoryToAPIEmojiCategory(ctx, category) - if err != nil { - err := gtserror.Newf("error converting emoji category to api emoji category: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } + apiCategory := typeutils.EmojiCategoryToAPIEmojiCategory(category) apiCategories = append(apiCategories, apiCategory) } diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go index e925297ff..aaccf4bde 100644 --- a/internal/processing/media/create.go +++ b/internal/processing/media/create.go @@ -29,6 +29,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" "codeberg.org/gruf/go-iotools" ) @@ -89,11 +90,6 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form return nil, errWithCode } - apiAttachment, err := p.converter.AttachmentToAPIAttachment(ctx, attachment) - if err != nil { - err := fmt.Errorf("error parsing media attachment to frontend type: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - return &apiAttachment, nil + a := typeutils.AttachmentToAPIAttachment(attachment) + return &a, nil } diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go index 79ab08291..3b9b92adc 100644 --- a/internal/processing/media/getfile.go +++ b/internal/processing/media/getfile.go @@ -247,7 +247,7 @@ func (p *Processor) getEmojiContent( emoji, err = p.federator.RecacheEmoji( ctx, emoji, - false, + false, // async ) if err != nil { err := gtserror.Newf("error recaching emoji: %w", err) diff --git a/internal/processing/media/getmedia.go b/internal/processing/media/getmedia.go index 5144bbd8e..22e05cab3 100644 --- a/internal/processing/media/getmedia.go +++ b/internal/processing/media/getmedia.go @@ -26,6 +26,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { @@ -42,10 +43,6 @@ func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, mediaAtt return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account")) } - a, err := p.converter.AttachmentToAPIAttachment(ctx, attachment) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) - } - + a := typeutils.AttachmentToAPIAttachment(attachment) return &a, nil } diff --git a/internal/processing/media/unattach.go b/internal/processing/media/unattach.go index 8eec907fd..55d793647 100644 --- a/internal/processing/media/unattach.go +++ b/internal/processing/media/unattach.go @@ -26,6 +26,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // Unattach unattaches the media attachment with the given ID from any statuses it was attached to, making it available @@ -49,10 +50,6 @@ func (p *Processor) Unattach(ctx context.Context, account *gtsmodel.Account, med return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error updating attachment: %s", err)) } - a, err := p.converter.AttachmentToAPIAttachment(ctx, attachment) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) - } - + a := typeutils.AttachmentToAPIAttachment(attachment) return &a, nil } diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go index 3acf238b0..cccc27534 100644 --- a/internal/processing/media/update.go +++ b/internal/processing/media/update.go @@ -29,6 +29,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/text" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // Update updates a media attachment with the given id, using the provided form parameters. @@ -77,11 +78,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, media return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating media: %s", err)) } - a, err := p.converter.AttachmentToAPIAttachment(ctx, attachment) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) - } - + a := typeutils.AttachmentToAPIAttachment(attachment) return &a, nil } diff --git a/internal/processing/search/util.go b/internal/processing/search/util.go index 441f3f946..7d52204c3 100644 --- a/internal/processing/search/util.go +++ b/internal/processing/search/util.go @@ -24,6 +24,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // return true if given queryType should include accounts. @@ -128,42 +129,22 @@ func (p *Processor) packageStatuses( // packageHashtags is a util function that just // converts the given hashtags into an apimodel // hashtag slice, or errors appropriately. -func (p *Processor) packageHashtags( - ctx context.Context, - requestingAccount *gtsmodel.Account, - tags []*gtsmodel.Tag, - v1 bool, -) ([]any, gtserror.WithCode) { - apiTags := make([]any, 0, len(tags)) - - var rangeF func(*gtsmodel.Tag) +func packageHashtags(tags []*gtsmodel.Tag, v1 bool) []any { + apiTags := make([]any, len(tags)) + if len(apiTags) != len(tags) { + panic(gtserror.New("bound check elimination")) + } if v1 { - // If API version 1, just provide slice of tag names. - rangeF = func(tag *gtsmodel.Tag) { - apiTags = append(apiTags, tag.Name) + for i, tag := range tags { + apiTags[i] = tag.Name } } else { - // If API not version 1, provide slice of full tags. - rangeF = func(tag *gtsmodel.Tag) { - apiTag, err := p.converter.TagToAPITag(ctx, tag, true, nil) - if err != nil { - log.Debugf( - ctx, - "skipping tag %s because it couldn't be converted to its api representation: %s", - tag.Name, err, - ) - return - } - - apiTags = append(apiTags, &apiTag) + for i, tag := range tags { + apiTag := typeutils.TagToAPITag(tag, true, nil) + apiTags[i] = apiTag } } - - for _, tag := range tags { - rangeF(tag) - } - - return apiTags, nil + return apiTags } // packageSearchResult wraps up the given accounts @@ -197,10 +178,7 @@ func (p *Processor) packageSearchResult( return nil, errWithCode } - apiTags, errWithCode := p.packageHashtags(ctx, requestingAccount, tags, v1) - if errWithCode != nil { - return nil, errWithCode - } + apiTags := packageHashtags(tags, v1) return &apimodel.SearchResult{ Accounts: apiAccounts, diff --git a/internal/processing/tags/follow.go b/internal/processing/tags/follow.go index b0ee0a995..879a1d9e8 100644 --- a/internal/processing/tags/follow.go +++ b/internal/processing/tags/follow.go @@ -26,6 +26,8 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" + "code.superseriousbusiness.org/gotosocial/internal/util" ) // Follow follows the tag with the given name as the given account. @@ -63,5 +65,6 @@ func (p *Processor) Follow( ) } - return p.apiTag(ctx, tag, true) + apiTag := typeutils.TagToAPITag(tag, true, util.Ptr(true)) + return &apiTag, nil } diff --git a/internal/processing/tags/followed.go b/internal/processing/tags/followed.go index eaecc0983..958960623 100644 --- a/internal/processing/tags/followed.go +++ b/internal/processing/tags/followed.go @@ -24,8 +24,8 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" - "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/gotosocial/internal/paging" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" ) @@ -53,14 +53,10 @@ func (p *Processor) Followed( lo := tags[count-1].ID hi := tags[0].ID - items := make([]interface{}, 0, count) following := util.Ptr(true) + items := make([]interface{}, 0, count) for _, tag := range tags { - apiTag, err := p.converter.TagToAPITag(ctx, tag, true, following) - if err != nil { - log.Errorf(ctx, "error converting tag %s to API representation: %v", tag.ID, err) - continue - } + apiTag := typeutils.TagToAPITag(tag, true, following) items = append(items, apiTag) } diff --git a/internal/processing/tags/followedtags.go b/internal/processing/tags/followedtags.go index 1619e433a..c78e0cc23 100644 --- a/internal/processing/tags/followedtags.go +++ b/internal/processing/tags/followedtags.go @@ -18,11 +18,6 @@ package tags import ( - "context" - - apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - "code.superseriousbusiness.org/gotosocial/internal/gtserror" - "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/state" "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) @@ -38,16 +33,3 @@ func New(state *state.State, converter *typeutils.Converter) Processor { converter: converter, } } - -// apiTag is a shortcut to return the API version of the given tag, -// or return an appropriate error if conversion fails. -func (p *Processor) apiTag(ctx context.Context, tag *gtsmodel.Tag, following bool) (*apimodel.Tag, gtserror.WithCode) { - apiTag, err := p.converter.TagToAPITag(ctx, tag, true, &following) - if err != nil { - return nil, gtserror.NewErrorInternalError( - gtserror.Newf("error converting tag %s to API representation: %w", tag.Name, err), - ) - } - - return &apiTag, nil -} diff --git a/internal/processing/tags/get.go b/internal/processing/tags/get.go index c7e343150..6c515ee1a 100644 --- a/internal/processing/tags/get.go +++ b/internal/processing/tags/get.go @@ -25,6 +25,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // Get gets the tag with the given name, including whether it's followed by the given account. @@ -53,5 +54,6 @@ func (p *Processor) Get( ) } - return p.apiTag(ctx, tag, following) + apiTag := typeutils.TagToAPITag(tag, true, &following) + return &apiTag, nil } diff --git a/internal/processing/tags/unfollow.go b/internal/processing/tags/unfollow.go index 8f303466c..3d15d68c2 100644 --- a/internal/processing/tags/unfollow.go +++ b/internal/processing/tags/unfollow.go @@ -25,6 +25,8 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" + "code.superseriousbusiness.org/gotosocial/internal/util" ) // Unfollow unfollows the tag with the given name as the given account. @@ -54,5 +56,6 @@ func (p *Processor) Unfollow( ) } - return p.apiTag(ctx, tag, false) + apiTag := typeutils.TagToAPITag(tag, true, util.Ptr(false)) + return &apiTag, nil } 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, >smodel.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 +} diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 6c77c3f26..7a6a4f50d 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -27,6 +27,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/config" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" "code.superseriousbusiness.org/gotosocial/testrig" "github.com/stretchr/testify/suite" @@ -1684,8 +1685,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval() func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() { testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"] - apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(suite.T().Context(), testAttachment) - suite.NoError(err) + apiAttachment := typeutils.AttachmentToAPIAttachment(testAttachment) b, err := json.MarshalIndent(apiAttachment, "", " ") suite.NoError(err) diff --git a/internal/typeutils/internaltorss.go b/internal/typeutils/internaltorss.go index 59babcb2d..0936e18ce 100644 --- a/internal/typeutils/internaltorss.go +++ b/internal/typeutils/internaltorss.go @@ -19,14 +19,12 @@ package typeutils import ( "context" - "fmt" "strconv" "strings" - apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/config" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" - "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/gotosocial/internal/text" "github.com/gorilla/feeds" ) @@ -36,133 +34,106 @@ const ( rssDescriptionMaxRunes = 256 ) -func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*feeds.Item, error) { - // see https://cyber.harvard.edu/rss/rss.html +// see https://cyber.harvard.edu/rss/rss.html +func (c *Converter) StatusToRSSItem(ctx context.Context, status *gtsmodel.Status) (*feeds.Item, error) { + var err error - // Title -- The title of the item. - // example: Venice Film Festival Tries to Quit Sinking - var title string - if s.ContentWarning != "" { - title = trimTo(s.ContentWarning, rssTitleMaxRunes) - } else { - title = trimTo(s.Text, rssTitleMaxRunes) - } - - // Link -- The URL of the item. - // example: http://nytimes.com/2004/12/07FEST.html - link := &feeds.Link{ - Href: s.URL, + // Ensure account populated. + if status.Account == nil { + status.Account, err = c.state.DB.GetAccountByID(ctx, status.AccountID) + if err != nil { + return nil, gtserror.Newf("db error getting status author: %w", err) + } } - // Author -- Email address of the author of the item. - // example: oprah\@oxygen.net - if s.Account == nil { - a, err := c.state.DB.GetAccountByID(ctx, s.AccountID) + // Get first attachment if present. + var media0 *gtsmodel.MediaAttachment + if status.AttachmentsPopulated() && len(status.Attachments) > 0 { + media0 = status.Attachments[0] + } else if len(status.AttachmentIDs) > 0 { + media0, err = c.state.DB.GetAttachmentByID(ctx, status.AttachmentIDs[0]) if err != nil { - return nil, fmt.Errorf("error getting status author: %s", err) + return nil, gtserror.Newf("db error getting status attachment: %w", err) } - s.Account = a - } - authorName := "@" + s.Account.Username + "@" + config.GetAccountDomain() - author := &feeds.Author{ - Name: authorName, } - // Source -- The RSS channel that the item came from. - source := &feeds.Link{ - Href: s.Account.URL + "/feed.rss", + // Title -- The title of the item. + // example: Venice Film Festival Tries to Quit Sinking + var title string + if status.ContentWarning != "" { + title = trimTo(status.ContentWarning, rssTitleMaxRunes) + } else { + title = trimTo(status.Text, rssTitleMaxRunes) } - // Description -- The item synopsis. - // example: Some of the most heated chatter at the Venice Film Festival this week was about the way that the arrival of the stars at the Palazzo del Cinema was being staged. - descriptionBuilder := strings.Builder{} - descriptionBuilder.WriteString(authorName + " ") + // Generate author name string for status. + authorName := "@" + status.Account.Username + + "@" + config.GetAccountDomain() - attachmentCount := len(s.Attachments) - if len(s.AttachmentIDs) > attachmentCount { - attachmentCount = len(s.AttachmentIDs) - } - switch { - case attachmentCount > 1: - descriptionBuilder.WriteString(fmt.Sprintf("posted [%d] attachments", attachmentCount)) - case attachmentCount == 1: - descriptionBuilder.WriteString("posted 1 attachment") + var buf strings.Builder + buf.Grow(512) + + // Description -- The item synopsis. + // example: Some of the most heated chatter at the Venice Film Festival this week was + // about the way that the arrival of the stars at the Palazzo del Cinema was being staged. + buf.WriteString(authorName + " ") + switch l := len(status.AttachmentIDs); { + case l > 1: + buf.WriteString("posted [") + buf.WriteString(strconv.Itoa(l)) + buf.WriteString("] attachments") + case l == 1: + buf.WriteString("posted 1 attachment") default: - descriptionBuilder.WriteString("made a new post") + buf.WriteString("made a new post") } - - if s.Text != "" { - descriptionBuilder.WriteString(": \"") - descriptionBuilder.WriteString(s.Text) - descriptionBuilder.WriteString("\"") + if status.Text != "" { + buf.WriteString(": \"") + buf.WriteString(status.Text) + buf.WriteString("\"") } - - description := trimTo(descriptionBuilder.String(), rssDescriptionMaxRunes) - - // ID -- A string that uniquely identifies the item. - // example: http://inessential.com/2002/09/01.php#a2 - id := s.URL - - // Enclosure -- Describes a media object that is attached to the item. - enclosure := &feeds.Enclosure{} - // get first attachment if present - var attachment *gtsmodel.MediaAttachment - if len(s.Attachments) > 0 { - attachment = s.Attachments[0] - } else if len(s.AttachmentIDs) > 0 { - a, err := c.state.DB.GetAttachmentByID(ctx, s.AttachmentIDs[0]) - if err == nil { - attachment = a - } - } - if attachment != nil { - enclosure.Type = attachment.File.ContentType - enclosure.Length = strconv.Itoa(attachment.File.FileSize) - enclosure.Url = attachment.URL + description := trimTo(buf.String(), rssDescriptionMaxRunes) + + // Enclosure, describes a media object + // that is attached to the item. + var enclosure *feeds.Enclosure + + // Set media details. + if media0 != nil { + enclosure = new(feeds.Enclosure) + enclosure.Type = media0.File.ContentType + enclosure.Length = strconv.Itoa(media0.File.FileSize) + enclosure.Url = media0.URL } - // Content - apiEmojis := []apimodel.Emoji{} - // the status might already have some gts emojis on it if it's not been pulled directly from the database - // if so, we can directly convert the gts emojis into api ones - if s.Emojis != nil { - for _, gtsEmoji := range s.Emojis { - apiEmoji, err := c.EmojiToAPIEmoji(ctx, gtsEmoji) - if err != nil { - log.Errorf(ctx, "error converting emoji with id %s: %s", gtsEmoji.ID, err) - continue - } - apiEmojis = append(apiEmojis, apiEmoji) - } - // the status doesn't have gts emojis on it, but it does have emoji IDs - // in this case, we need to pull the gts emojis from the db to convert them into api ones - } else { - for _, e := range s.EmojiIDs { - gtsEmoji := >smodel.Emoji{} - if err := c.state.DB.GetByID(ctx, e, gtsEmoji); err != nil { - log.Errorf(ctx, "error getting emoji with id %s: %s", e, err) - continue - } - apiEmoji, err := c.EmojiToAPIEmoji(ctx, gtsEmoji) - if err != nil { - log.Errorf(ctx, "error converting emoji with id %s: %s", gtsEmoji.ID, err) - continue - } - apiEmojis = append(apiEmojis, apiEmoji) - } - } - content := text.EmojifyRSS(apiEmojis, s.Content) + // Generate emojified content. + apiEmojis := c.emojisToAPI(ctx, status.Emojis, status.EmojiIDs) + content := text.EmojifyRSS(apiEmojis, status.Content) return &feeds.Item{ + // we specifcally do not set the author, as a lot + // of feed readers rely on the RSS standard of the + // author being an email with optional name. but + // our @username@domain identifiers break this. + // + // attribution is handled in the title/description. + + // ID -- A string that uniquely identifies the item. + // example: http://inessential.com/2002/09/01.php#a2 + Id: status.URL, + + // Source -- The RSS channel that the item came from. + Source: &feeds.Link{Href: status.Account.URL + "/feed.rss"}, + + // Link -- The URL of the item. + // example: http://nytimes.com/2004/12/07FEST.html + Link: &feeds.Link{Href: status.URL}, + Title: title, - Link: link, - Author: author, - Source: source, Description: description, - Id: id, IsPermaLink: "true", - Updated: s.EditedAt, - Created: s.CreatedAt, + Updated: status.EditedAt, + Created: status.CreatedAt, Enclosure: enclosure, Content: content, }, nil diff --git a/internal/typeutils/internaltorss_test.go b/internal/typeutils/internaltorss_test.go index 89c88be27..498e44912 100644 --- a/internal/typeutils/internaltorss_test.go +++ b/internal/typeutils/internaltorss_test.go @@ -45,14 +45,10 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem1() { suite.Equal("", item.Source.Length) suite.Equal("", item.Source.Rel) suite.Equal("", item.Source.Type) - suite.Equal("", item.Author.Email) - suite.Equal("@the_mighty_zork@localhost:8080", item.Author.Name) suite.Equal("@the_mighty_zork@localhost:8080 made a new post: \"hello everyone!\"", item.Description) suite.Equal("http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY", item.Id) suite.EqualValues(1634726437, item.Created.Unix()) - suite.Equal("", item.Enclosure.Length) - suite.Equal("", item.Enclosure.Type) - suite.Equal("", item.Enclosure.Url) + suite.Nil(item.Enclosure) suite.Equal("<p>hello everyone!</p>", item.Content) } @@ -70,8 +66,6 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem2() { suite.Equal("", item.Source.Length) suite.Equal("", item.Source.Rel) suite.Equal("", item.Source.Type) - suite.Equal("", item.Author.Email) - suite.Equal("@admin@localhost:8080", item.Author.Name) suite.Equal("@admin@localhost:8080 posted 1 attachment: \"hello world! #welcome ! first post on the instance :rainbow: !\"", item.Description) suite.Equal("http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", item.Id) suite.EqualValues(1634729805, item.Created.Unix()) @@ -118,20 +112,11 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() { <Type></Type> <Length></Length> </Source> - <Author> - <Name>@admin@localhost:8080</Name> - <Email></Email> - </Author> <Description>@admin@localhost:8080 made a new post</Description> <Id>http://localhost:8080/@admin/statuses/01H7G0VW1ACBZTRHN6RSA4JWVH</Id> <IsPermaLink>true</IsPermaLink> <Updated>0001-01-01T00:00:00Z</Updated> <Created>0001-01-01T00:00:00Z</Created> - <Enclosure> - <Url></Url> - <Length></Length> - <Type></Type> - </Enclosure> <Content>这是另一段,只是为了确保这篇文章足够长。 通过前肢上长而弯曲的爪子的数量可以轻松识别不同的树懒类别。 顾名思义,二趾树懒的前肢上有两个爪子,而三趾树懒的四个肢上都有三个爪子。 二趾树懒也比三趾树懒稍大,并且都属于不同的分类科。 美洲共有六种树懒,主要分布在中美洲和南美洲的热带雨林中。



	霍夫曼二趾树懒 (Choloepus hoffmanni)

	林奈二趾树懒 (Choloepus didactylus)

	侏儒三趾树懒 (Bradypus pygmaeus)

	鬃三趾树懒 (Bradypus torquatus)

	棕喉树懒 (Bradypus variegatus)

	浅喉树懒 (Bradypus tridactylus)



目前,有 4 种树懒被 IUCN 濒危物种红色名录列为最不受关注的物种。 鬃毛三趾树懒很脆弱,而侏儒三趾树懒则极度濒危,树懒物种面临最大的灭绝风险。</Content> </Item>`, string(data)) } diff --git a/internal/web/rss.go b/internal/web/rss.go index 7fe941320..95f2a9a44 100644 --- a/internal/web/rss.go +++ b/internal/web/rss.go @@ -232,14 +232,14 @@ func unixAfter(t1 time.Time, t2 time.Time) bool { // If no time was provided, or the provided time was // not parseable, it will return a zero time. func extractIfModifiedSince(r *http.Request) time.Time { - imsStr := r.Header.Get(ifModifiedSinceHeader) - if imsStr == "" { + val := r.Header.Get(ifModifiedSinceHeader) + if val == "" { return time.Time{} // Nothing set. } - ifModifiedSince, err := http.ParseTime(imsStr) + ifModifiedSince, err := http.ParseTime(val) if err != nil { - log.Errorf(r.Context(), "couldn't parse %s value '%s' as time: %q", ifModifiedSinceHeader, imsStr, err) + log.Errorf(r.Context(), "couldn't parse %q as time: %v", val, err) return time.Time{} } |
