[feature] add paging support to rss feed endpoint, and support JSON / atom feed types (#4442)

originally based on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4396

hope this is okay https://codeberg.org/zordsdavini !

closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4411
closes https://codeberg.org/superseriousbusiness/gotosocial/issues/3407

Co-authored-by: Arnas Udovic <zordsdavini@gmail.com>
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4442
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-09-18 16:33:23 +02:00 committed by kim
commit 6607e1c944
13 changed files with 447 additions and 182 deletions

View file

@ -20,21 +20,19 @@ package account
import (
"context"
"errors"
"fmt"
"time"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/paging"
"github.com/gorilla/feeds"
)
const (
rssFeedLength = 20
)
var never time.Time
type GetRSSFeed func() (string, gtserror.WithCode)
type GetRSSFeed func() (*feeds.Feed, gtserror.WithCode)
// GetRSSFeedForUsername returns a function to return the RSS feed of a local account
// with the given username, and the last-modified time (time that the account last
@ -45,33 +43,30 @@ type GetRSSFeed func() (string, gtserror.WithCode)
//
// If the account has not yet posted an RSS-eligible status, the returned last-modified
// time will be zero, and the GetRSSFeed func will return a valid RSS xml with no items.
func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string) (GetRSSFeed, time.Time, gtserror.WithCode) {
var (
never = time.Time{}
)
func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string, page *paging.Page) (GetRSSFeed, time.Time, gtserror.WithCode) {
// Fetch local (i.e. empty domain) account from database by username.
account, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// Simply no account with this username.
err = gtserror.New("account not found")
return nil, never, gtserror.NewErrorNotFound(err)
}
// Real db error.
err = gtserror.Newf("db error getting account %s: %w", username, err)
err := gtserror.Newf("db error getting account %s: %w", username, err)
return nil, never, gtserror.NewErrorInternalError(err)
}
// Check if exists.
if account == nil {
err := gtserror.New("account not found")
return nil, never, gtserror.NewErrorNotFound(err)
}
// Ensure account has rss feed enabled.
if !*account.Settings.EnableRSS {
err = gtserror.New("account RSS feed not enabled")
err := gtserror.New("account RSS feed not enabled")
return nil, never, gtserror.NewErrorNotFound(err)
}
// Ensure account stats populated.
// Ensure account stats populated for last status fetch information.
if err := p.state.DB.PopulateAccountStats(ctx, account); err != nil {
err = gtserror.Newf("db error getting account stats %s: %w", username, err)
err := gtserror.Newf("db error getting account stats %s: %w", username, err)
return nil, never, gtserror.NewErrorInternalError(err)
}
@ -80,14 +75,14 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
// eligible to appear in the RSS feed; that's fine.
lastPostAt := account.Stats.LastStatusAt
return func() (string, gtserror.WithCode) {
return func() (*feeds.Feed, gtserror.WithCode) {
// Assemble author namestring once only.
author := "@" + account.Username + "@" + config.GetAccountDomain()
// Derive image/thumbnail for this account (may be nil).
// Derive image/thumbnail for this account (may be nil if no media).
image, errWithCode := p.rssImageForAccount(ctx, account, author)
if errWithCode != nil {
return "", errWithCode
return nil, errWithCode
}
feed := &feeds.Feed{
@ -106,7 +101,7 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
// since we already know there's no eligible statuses.
if lastPostAt.IsZero() {
feed.Updated = account.CreatedAt
return stringifyFeed(feed)
return feed, nil
}
// Account has posted at least one status that's
@ -120,32 +115,30 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
//
// Take into account whether the user wants
// their web view laid out in gallery mode.
mediaOnly := account.Settings != nil &&
account.Settings.WebLayout == gtsmodel.WebLayoutGallery
mediaOnly := (account.Settings != nil &&
account.Settings.WebLayout == gtsmodel.WebLayoutGallery)
statuses, err := p.state.DB.GetAccountWebStatuses(
ctx,
account,
page,
mediaOnly,
rssFeedLength,
"", // Latest posts from the top.
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("db error getting account web statuses: %w", err)
return "", gtserror.NewErrorInternalError(err)
err := gtserror.Newf("db error getting account web statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Add each status to the rss feed.
for _, status := range statuses {
item, err := p.converter.StatusToRSSItem(ctx, status)
if err != nil {
err = gtserror.Newf("error converting status to feed item: %w", err)
return "", gtserror.NewErrorInternalError(err)
err := gtserror.Newf("error converting status to feed item: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
feed.Add(item)
}
return stringifyFeed(feed)
return feed, nil
}, lastPostAt, nil
}
@ -177,15 +170,3 @@ func (p *Processor) rssImageForAccount(ctx context.Context, account *gtsmodel.Ac
Link: account.URL,
}, nil
}
func stringifyFeed(feed *feeds.Feed) (string, gtserror.WithCode) {
// Stringify the feed. Even with no statuses,
// this will still produce valid rss xml.
rss, err := feed.ToRss()
if err != nil {
err := gtserror.Newf("error converting feed to rss string: %w", err)
return "", gtserror.NewErrorInternalError(err)
}
return rss, nil
}