diff options
Diffstat (limited to 'internal/processing')
-rw-r--r-- | internal/processing/account.go | 5 | ||||
-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 | ||||
-rw-r--r-- | internal/processing/processor.go | 5 |
6 files changed, 186 insertions, 0 deletions
diff --git a/internal/processing/account.go b/internal/processing/account.go index ada511133..6cba8b9c4 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -20,6 +20,7 @@ package processing import ( "context" + "time" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -46,6 +47,10 @@ func (p *processor) AccountGetCustomCSSForUsername(ctx context.Context, username return p.accountProcessor.GetCustomCSSForUsername(ctx, username) } +func (p *processor) AccountGetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) { + return p.accountProcessor.GetRSSFeedForUsername(ctx, username) +} + func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { return p.accountProcessor.Update(ctx, authed.Account, form) } 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)) diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 81ed3c8e5..09bb579ba 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -22,6 +22,7 @@ import ( "context" "net/http" "net/url" + "time" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" @@ -80,6 +81,10 @@ type Processor interface { // AccountGet processes the given request for account information. AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode) AccountGetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) + // AccountGetRSSFeedForUsername returns a function to get the RSS feed of latest posts for given local account username. + // This function should only be called if necessary: the given lastModified time can be used to check this. + // Will return 404 if an rss feed for that user is not available, or a different error if something else goes wrong. + AccountGetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) // AccountUpdate processes the update of an account with the given form AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for |