diff options
author | 2022-10-08 14:00:39 +0200 | |
---|---|---|
committer | 2022-10-08 14:00:39 +0200 | |
commit | 80663061d8f361ae4bcea1307a10a40c41174ebe (patch) | |
tree | 999f9e8decfb3e6e211e8462415103819eddd1c2 /internal/processing/account | |
parent | [chore] Standardize database queries, use `bun.Ident()` properly (#886) (diff) | |
download | gotosocial-80663061d8f361ae4bcea1307a10a40c41174ebe.tar.xz |
[feature] Add opt-in RSS feed for account's latest Public posts (#897)
* start adding rss functionality
* add gorilla/feeds dependency
* first bash at building rss feed
still needs work, this is an interim commit
* tidy up a bit
* add publicOnly option to GetAccountLastPosted
* implement rss endpoint
* fix test
* add initial user docs for rss
* update rss logo
* docs update
* add rssFeed to frontend
* feed -> feed.rss
* enableRSS
* increase rss logo size a lil bit
* add rss toggle
* move emojify to text package
* fiddle with rss feed formatting
* add Text field to test statuses
* move status to rss item to typeconverter
* update bun schema for enablerss
* simplify 304 checking
* assume account not rss
* update tests
* update swagger docs
* allow more characters in title, trim nicer
* update last posted to be more consistent
Diffstat (limited to 'internal/processing/account')
-rw-r--r-- | internal/processing/account/account.go | 3 | ||||
-rw-r--r-- | internal/processing/account/getrss.go | 108 | ||||
-rw-r--r-- | internal/processing/account/getrss_test.go | 61 | ||||
-rw-r--r-- | internal/processing/account/update.go | 4 |
4 files changed, 176 insertions, 0 deletions
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index aca46394a..b18e705ca 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -21,6 +21,7 @@ package account import ( "context" "mime/multipart" + "time" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" @@ -53,6 +54,8 @@ type Processor interface { GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) // GetCustomCSSForUsername returns custom css for the given local username. GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) + // GetRSSFeedForUsername returns RSS feed for the given local username. + GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) // Update processes the update of an account with the given form Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for diff --git a/internal/processing/account/getrss.go b/internal/processing/account/getrss.go new file mode 100644 index 000000000..f07204b56 --- /dev/null +++ b/internal/processing/account/getrss.go @@ -0,0 +1,108 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package account + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/gorilla/feeds" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +const rssFeedLength = 20 + +func (p *processor) GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) { + account, err := p.db.GetAccountByUsernameDomain(ctx, username, "") + if err != nil { + if err == db.ErrNoEntries { + return nil, time.Time{}, gtserror.NewErrorNotFound(errors.New("GetRSSFeedForUsername: account not found")) + } + return nil, time.Time{}, gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) + } + + if !*account.EnableRSS { + return nil, time.Time{}, gtserror.NewErrorNotFound(errors.New("GetRSSFeedForUsername: account RSS feed not enabled")) + } + + lastModified, err := p.db.GetAccountLastPosted(ctx, account.ID, true) + if err != nil { + return nil, time.Time{}, gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) + } + + return func() (string, gtserror.WithCode) { + statuses, err := p.db.GetAccountWebStatuses(ctx, account.ID, rssFeedLength, "") + if err != nil && err != db.ErrNoEntries { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) + } + + author := "@" + account.Username + "@" + config.GetAccountDomain() + title := "Posts from " + author + description := "Posts from " + author + link := &feeds.Link{Href: account.URL} + + var image *feeds.Image + if account.AvatarMediaAttachmentID != "" { + if account.AvatarMediaAttachment == nil { + avatar, err := p.db.GetAttachmentByID(ctx, account.AvatarMediaAttachmentID) + if err != nil { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error fetching avatar attachment: %s", err)) + } + account.AvatarMediaAttachment = avatar + } + image = &feeds.Image{ + Url: account.AvatarMediaAttachment.Thumbnail.URL, + Title: "Avatar for " + author, + Link: account.URL, + } + } + + feed := &feeds.Feed{ + Title: title, + Description: description, + Link: link, + Image: image, + } + + for i, s := range statuses { + // take the date of the first (ie., latest) status as feed updated value + if i == 0 { + feed.Updated = s.UpdatedAt + } + + item, err := p.tc.StatusToRSSItem(ctx, s) + if err != nil { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: error converting status to feed item: %s", err)) + } + + feed.Add(item) + } + + rss, err := feed.ToRss() + if err != nil { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: error converting feed to rss string: %s", err)) + } + + return rss, nil + }, lastModified, nil +} diff --git a/internal/processing/account/getrss_test.go b/internal/processing/account/getrss_test.go new file mode 100644 index 000000000..dc81434a0 --- /dev/null +++ b/internal/processing/account/getrss_test.go @@ -0,0 +1,61 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package account_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/suite" +) + +type GetRSSTestSuite struct { + AccountStandardTestSuite +} + +func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() { + getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "admin") + suite.NoError(err) + suite.EqualValues(1634733405, lastModified.Unix()) + + feed, err := getFeed() + suite.NoError(err) + + fmt.Println(feed) + + suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" class=\"emoji\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed) +} + +func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { + getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "the_mighty_zork") + suite.NoError(err) + suite.EqualValues(1634726437, lastModified.Unix()) + + feed, err := getFeed() + suite.NoError(err) + + fmt.Println(feed) + + suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n <description>Posts from @the_mighty_zork@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 10:40:37 +0000</lastBuildDate>\n <image>\n <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg</url>\n <title>Avatar for @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n </image>\n <item>\n <title>introduction post</title>\n <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>\n <description>@the_mighty_zork@localhost:8080 made a new post: "hello everyone!"</description>\n <content:encoded><![CDATA[hello everyone!]]></content:encoded>\n <author>@the_mighty_zork@localhost:8080</author>\n <guid>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>\n <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n </item>\n </channel>\n</rss>", feed) +} + +func TestGetRSSTestSuite(t *testing.T) { + suite.Run(t, new(GetRSSTestSuite)) +} diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 94e91ca4c..f39361c06 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -160,6 +160,10 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form account.CustomCSS = text.SanitizePlaintext(customCSS) } + if form.EnableRSS != nil { + account.EnableRSS = form.EnableRSS + } + updatedAccount, err := p.db.UpdateAccount(ctx, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err)) |