[chore] Make paging logic more generic (#901)

* make paging logic more generic
not just for timelines!

* linty linterson
This commit is contained in:
tobi 2022-10-10 15:52:49 +02:00 committed by GitHub
parent 80663061d8
commit 832befd727
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 257 additions and 200 deletions

View file

@ -18,12 +18,10 @@
package model package model
import "github.com/superseriousbusiness/gotosocial/internal/timeline" // PageableResponse wraps a slice of items, ready to be serialized, along with the Link
// header for the previous and next queries / pages, to be returned to the client.
// TimelineResponse wraps a slice of timelineables, ready to be serialized, along with the Link type PageableResponse struct {
// header for the previous and next queries, to be returned to the client. Items []interface{}
type TimelineResponse struct {
Items []timeline.Timelineable
LinkHeader string LinkHeader string
NextLink string NextLink string
PrevLink string PrevLink string

View file

@ -55,11 +55,11 @@ func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form
return p.accountProcessor.Update(ctx, authed.Account, form) return p.accountProcessor.Update(ctx, authed.Account, form)
} }
func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
} }
func (p *processor) AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.accountProcessor.WebStatusesGet(ctx, targetAccountID, maxID) return p.accountProcessor.WebStatusesGet(ctx, targetAccountID, maxID)
} }

View file

@ -60,10 +60,10 @@ type Processor interface {
Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) 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 // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed. // the account given in authed.
StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode)
// WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only // WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only
// statuses which are suitable for showing on the public web profile of an account. // statuses which are suitable for showing on the public web profile of an account.
WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode)
// FollowersGet fetches a list of the target account's followers. // FollowersGet fetches a list of the target account's followers.
FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// FollowingGet fetches a list of the accounts that target account is following. // FollowingGet fetches a list of the accounts that target account is following.

View file

@ -26,11 +26,10 @@
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) {
if requestingAccount != nil { if requestingAccount != nil {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
@ -42,7 +41,7 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel
statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
return util.EmptyTimelineResponse(), nil return util.EmptyPageableResponse(), nil
} }
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
@ -55,25 +54,37 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel
} }
} }
if len(filtered) == 0 { count := len(filtered)
return util.EmptyTimelineResponse(), nil
if count == 0 {
return util.EmptyPageableResponse(), nil
} }
timelineables := []timeline.Timelineable{} items := []interface{}{}
for _, i := range filtered { nextMaxIDValue := ""
apiStatus, err := p.tc.StatusToAPIStatus(ctx, i, requestingAccount) prevMinIDValue := ""
for i, s := range filtered {
item, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err))
} }
timelineables = append(timelineables, apiStatus) if i == count-1 {
nextMaxIDValue = item.GetID()
}
if i == 0 {
prevMinIDValue = item.GetID()
}
items = append(items, item)
} }
return util.PackageTimelineableResponse(util.TimelineableResponseParams{ return util.PackagePageableResponse(util.PageableResponseParams{
Items: timelineables, Items: items,
Path: fmt.Sprintf("/api/v1/accounts/%s/statuses", targetAccountID), Path: fmt.Sprintf("/api/v1/accounts/%s/statuses", targetAccountID),
NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), NextMaxIDValue: nextMaxIDValue,
PrevMinIDValue: timelineables[0].GetID(), PrevMinIDValue: prevMinIDValue,
Limit: limit, Limit: limit,
ExtraQueryParams: []string{ ExtraQueryParams: []string{
fmt.Sprintf("exclude_replies=%t", excludeReplies), fmt.Sprintf("exclude_replies=%t", excludeReplies),
@ -85,7 +96,7 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel
}) })
} }
func (p *processor) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) {
acct, err := p.db.GetAccountByID(ctx, targetAccountID) acct, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
@ -103,26 +114,42 @@ func (p *processor) WebStatusesGet(ctx context.Context, targetAccountID string,
statuses, err := p.db.GetAccountWebStatuses(ctx, targetAccountID, 10, maxID) statuses, err := p.db.GetAccountWebStatuses(ctx, targetAccountID, 10, maxID)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
return util.EmptyTimelineResponse(), nil return util.EmptyPageableResponse(), nil
} }
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
timelineables := []timeline.Timelineable{} count := len(statuses)
for _, i := range statuses {
apiStatus, err := p.tc.StatusToAPIStatus(ctx, i, nil) if count == 0 {
return util.EmptyPageableResponse(), nil
}
items := []interface{}{}
nextMaxIDValue := ""
prevMinIDValue := ""
for i, s := range statuses {
item, err := p.tc.StatusToAPIStatus(ctx, s, nil)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err))
} }
timelineables = append(timelineables, apiStatus) if i == count-1 {
nextMaxIDValue = item.GetID()
}
if i == 0 {
prevMinIDValue = item.GetID()
}
items = append(items, item)
} }
return util.PackageTimelineableResponse(util.TimelineableResponseParams{ return util.PackagePageableResponse(util.PageableResponseParams{
Items: timelineables, Items: items,
Path: "/@" + acct.Username, Path: "/@" + acct.Username,
NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), NextMaxIDValue: nextMaxIDValue,
PrevMinIDValue: timelineables[0].GetID(), PrevMinIDValue: prevMinIDValue,
ExtraQueryParams: []string{}, ExtraQueryParams: []string{},
}) })
} }

View file

@ -25,36 +25,48 @@
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode) {
notifs, err := p.db.GetNotifications(ctx, authed.Account.ID, excludeTypes, limit, maxID, sinceID) notifs, err := p.db.GetNotifications(ctx, authed.Account.ID, excludeTypes, limit, maxID, sinceID)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
if len(notifs) == 0 { count := len(notifs)
return util.EmptyTimelineResponse(), nil
if count == 0 {
return util.EmptyPageableResponse(), nil
} }
timelineables := []timeline.Timelineable{} items := []interface{}{}
for _, n := range notifs { nextMaxIDValue := ""
apiNotif, err := p.tc.NotificationToAPINotification(ctx, n) prevMinIDValue := ""
for i, n := range notifs {
item, err := p.tc.NotificationToAPINotification(ctx, n)
if err != nil { if err != nil {
log.Debugf("got an error converting a notification to api, will skip it: %s", err) log.Debugf("got an error converting a notification to api, will skip it: %s", err)
continue continue
} }
timelineables = append(timelineables, apiNotif)
if i == count-1 {
nextMaxIDValue = item.GetID()
}
if i == 0 {
prevMinIDValue = item.GetID()
}
items = append(items, item)
} }
return util.PackageTimelineableResponse(util.TimelineableResponseParams{ return util.PackagePageableResponse(util.PageableResponseParams{
Items: timelineables, Items: items,
Path: "api/v1/notifications", Path: "api/v1/notifications",
NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), NextMaxIDValue: nextMaxIDValue,
PrevMinIDKey: "since_id", PrevMinIDKey: "since_id",
PrevMinIDValue: timelineables[0].GetID(), PrevMinIDValue: prevMinIDValue,
Limit: limit, Limit: limit,
}) })
} }

View file

@ -89,10 +89,10 @@ type Processor interface {
AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) 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 // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed. // the account given in authed.
AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode)
// AccountWebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only // AccountWebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only
// statuses which are suitable for showing on the public web profile of an account. // statuses which are suitable for showing on the public web profile of an account.
AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode)
// AccountFollowersGet fetches a list of the target account's followers. // AccountFollowersGet fetches a list of the target account's followers.
AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// AccountFollowingGet fetches a list of the accounts that target account is following. // AccountFollowingGet fetches a list of the accounts that target account is following.
@ -160,7 +160,7 @@ type Processor interface {
MediaUpdate(ctx context.Context, authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) MediaUpdate(ctx context.Context, authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
// NotificationsGet // NotificationsGet
NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode) NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode)
// NotificationsClear // NotificationsClear
NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode
@ -192,11 +192,11 @@ type Processor interface {
StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
// HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters. // HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode)
// PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters. // PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters.
PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode)
// FavedTimelineGet returns faved statuses, with the given filters/parameters. // FavedTimelineGet returns faved statuses, with the given filters/parameters.
FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
// AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid. // AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid.
AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode)

View file

@ -137,40 +137,47 @@ func StatusSkipInsertFunction() timeline.SkipInsertFunction {
} }
} }
func (p *processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) {
preparedItems, err := p.statusTimelines.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local) preparedItems, err := p.statusTimelines.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
if len(preparedItems) == 0 { count := len(preparedItems)
return util.EmptyTimelineResponse(), nil
if count == 0 {
return util.EmptyPageableResponse(), nil
} }
timelineables := []timeline.Timelineable{} items := []interface{}{}
for _, i := range preparedItems { nextMaxIDValue := ""
status, ok := i.(*apimodel.Status) prevMinIDValue := ""
if !ok { for i, item := range preparedItems {
return nil, gtserror.NewErrorInternalError(errors.New("error converting prepared timeline entry to api status")) if i == count-1 {
nextMaxIDValue = item.GetID()
} }
timelineables = append(timelineables, status)
if i == 0 {
prevMinIDValue = item.GetID()
}
items = append(items, item)
} }
return util.PackageTimelineableResponse(util.TimelineableResponseParams{ return util.PackagePageableResponse(util.PageableResponseParams{
Items: timelineables, Items: items,
Path: "api/v1/timelines/home", Path: "api/v1/timelines/home",
NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), NextMaxIDValue: nextMaxIDValue,
PrevMinIDValue: timelineables[0].GetID(), PrevMinIDValue: prevMinIDValue,
Limit: limit, Limit: limit,
}) })
} }
func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) {
statuses, err := p.db.GetPublicTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local) statuses, err := p.db.GetPublicTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
// there are just no entries left // there are just no entries left
return util.EmptyTimelineResponse(), nil return util.EmptyPageableResponse(), nil
} }
// there's an actual error // there's an actual error
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
@ -181,30 +188,41 @@ func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, m
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
if len(filtered) == 0 { count := len(filtered)
return util.EmptyTimelineResponse(), nil
if count == 0 {
return util.EmptyPageableResponse(), nil
} }
timelineables := []timeline.Timelineable{} items := []interface{}{}
for _, i := range filtered { nextMaxIDValue := ""
timelineables = append(timelineables, i) prevMinIDValue := ""
for i, item := range filtered {
if i == count-1 {
nextMaxIDValue = item.GetID()
}
if i == 0 {
prevMinIDValue = item.GetID()
}
items = append(items, item)
} }
return util.PackageTimelineableResponse(util.TimelineableResponseParams{ return util.PackagePageableResponse(util.PageableResponseParams{
Items: timelineables, Items: items,
Path: "api/v1/timelines/public", Path: "api/v1/timelines/public",
NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), NextMaxIDValue: nextMaxIDValue,
PrevMinIDValue: timelineables[0].GetID(), PrevMinIDValue: prevMinIDValue,
Limit: limit, Limit: limit,
}) })
} }
func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode) { func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimeline(ctx, authed.Account.ID, maxID, minID, limit) statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimeline(ctx, authed.Account.ID, maxID, minID, limit)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
// there are just no entries left // there are just no entries left
return util.EmptyTimelineResponse(), nil return util.EmptyPageableResponse(), nil
} }
// there's an actual error // there's an actual error
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
@ -216,16 +234,16 @@ func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma
} }
if len(filtered) == 0 { if len(filtered) == 0 {
return util.EmptyTimelineResponse(), nil return util.EmptyPageableResponse(), nil
} }
timelineables := []timeline.Timelineable{} items := []interface{}{}
for _, i := range filtered { for _, item := range filtered {
timelineables = append(timelineables, i) items = append(items, item)
} }
return util.PackageTimelineableResponse(util.TimelineableResponseParams{ return util.PackagePageableResponse(util.PageableResponseParams{
Items: timelineables, Items: items,
Path: "api/v1/favourites", Path: "api/v1/favourites",
NextMaxIDValue: nextMaxID, NextMaxIDValue: nextMaxID,
PrevMinIDValue: prevMinID, PrevMinIDValue: prevMinID,

119
internal/util/paging.go Normal file
View file

@ -0,0 +1,119 @@
/*
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 util
import (
"fmt"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// PageableResponseParams models the parameters to pass to PackagePageableResponse.
//
// The given items will be provided in the paged response.
//
// The other values are all used to create the Link header so that callers know
// which endpoint to query next and previously in order to do paging.
type PageableResponseParams struct {
Items []interface{} // Sorted slice of items (statuses, notifications, etc)
Path string // path to use for next/prev queries in the link header
NextMaxIDKey string // key to use for the next max id query param in the link header, defaults to 'max_id'
NextMaxIDValue string // value to use for next max id
PrevMinIDKey string // key to use for the prev min id query param in the link header, defaults to 'min_id'
PrevMinIDValue string // value to use for prev min id
Limit int // limit number of entries to return
ExtraQueryParams []string // any extra query parameters to provide in the link header, should be in the format 'example=value'
}
// PackagePageableResponse is a convenience function for returning
// a bunch of pageable items (notifications, statuses, etc), as well
// as a Link header to inform callers of where to find next/prev items.
func PackagePageableResponse(params PageableResponseParams) (*apimodel.PageableResponse, gtserror.WithCode) {
if params.NextMaxIDKey == "" {
params.NextMaxIDKey = "max_id"
}
if params.PrevMinIDKey == "" {
params.PrevMinIDKey = "min_id"
}
pageableResponse := EmptyPageableResponse()
if len(params.Items) == 0 {
return pageableResponse, nil
}
// items
pageableResponse.Items = params.Items
protocol := config.GetProtocol()
host := config.GetHost()
// next
nextRaw := params.NextMaxIDKey + "=" + params.NextMaxIDValue
if params.Limit != 0 {
nextRaw = fmt.Sprintf("limit=%d&", params.Limit) + nextRaw
}
for _, p := range params.ExtraQueryParams {
nextRaw = nextRaw + "&" + p
}
nextLink := &url.URL{
Scheme: protocol,
Host: host,
Path: params.Path,
RawQuery: nextRaw,
}
nextLinkString := nextLink.String()
pageableResponse.NextLink = nextLinkString
// prev
prevRaw := params.PrevMinIDKey + "=" + params.PrevMinIDValue
if params.Limit != 0 {
prevRaw = fmt.Sprintf("limit=%d&", params.Limit) + prevRaw
}
for _, p := range params.ExtraQueryParams {
prevRaw = prevRaw + "&" + p
}
prevLink := &url.URL{
Scheme: protocol,
Host: host,
Path: params.Path,
RawQuery: prevRaw,
}
prevLinkString := prevLink.String()
pageableResponse.PrevLink = prevLinkString
// link header
next := fmt.Sprintf("<%s>; rel=\"next\"", nextLinkString)
prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLinkString)
pageableResponse.LinkHeader = next + ", " + prev
return pageableResponse, nil
}
// EmptyPageableResponse just returns an empty
// PageableResponse with no link header or items.
func EmptyPageableResponse() *apimodel.PageableResponse {
return &apimodel.PageableResponse{
Items: []interface{}{},
}
}

View file

@ -1,117 +0,0 @@
/*
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 util
import (
"fmt"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
)
// TimelineableResponseParams models the parameters to pass to PackageTimelineableResponse.
//
// The given items will be provided in the timeline response.
//
// The other values are all used to create the Link header so that callers know
// which endpoint to query next and previously in order to do paging.
type TimelineableResponseParams struct {
Items []timeline.Timelineable // Sorted slice of Timelineables (statuses, notifications, etc)
Path string // path to use for next/prev queries in the link header
NextMaxIDKey string // key to use for the next max id query param in the link header, defaults to 'max_id'
NextMaxIDValue string // value to use for next max id
PrevMinIDKey string // key to use for the prev min id query param in the link header, defaults to 'min_id'
PrevMinIDValue string // value to use for prev min id
Limit int // limit number of entries to return
ExtraQueryParams []string // any extra query parameters to provide in the link header, should be in the format 'example=value'
}
// PackageTimelineableResponse is a convenience function for returning
// a bunch of timelineable items (notifications, statuses, etc), as well
// as a Link header to inform callers of where to find next/prev items.
func PackageTimelineableResponse(params TimelineableResponseParams) (*apimodel.TimelineResponse, gtserror.WithCode) {
if params.NextMaxIDKey == "" {
params.NextMaxIDKey = "max_id"
}
if params.PrevMinIDKey == "" {
params.PrevMinIDKey = "min_id"
}
timelineResponse := &apimodel.TimelineResponse{
Items: params.Items,
}
if len(params.Items) != 0 {
protocol := config.GetProtocol()
host := config.GetHost()
// next
nextRaw := params.NextMaxIDKey + "=" + params.NextMaxIDValue
if params.Limit != 0 {
nextRaw = fmt.Sprintf("limit=%d&", params.Limit) + nextRaw
}
for _, p := range params.ExtraQueryParams {
nextRaw = nextRaw + "&" + p
}
nextLink := &url.URL{
Scheme: protocol,
Host: host,
Path: params.Path,
RawQuery: nextRaw,
}
nextLinkString := nextLink.String()
timelineResponse.NextLink = nextLinkString
// prev
prevRaw := params.PrevMinIDKey + "=" + params.PrevMinIDValue
if params.Limit != 0 {
prevRaw = fmt.Sprintf("limit=%d&", params.Limit) + prevRaw
}
for _, p := range params.ExtraQueryParams {
prevRaw = prevRaw + "&" + p
}
prevLink := &url.URL{
Scheme: protocol,
Host: host,
Path: params.Path,
RawQuery: prevRaw,
}
prevLinkString := prevLink.String()
timelineResponse.PrevLink = prevLinkString
// link header
next := fmt.Sprintf("<%s>; rel=\"next\"", nextLinkString)
prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLinkString)
timelineResponse.LinkHeader = next + ", " + prev
}
return timelineResponse, nil
}
// EmptyTimelineResponse just returns an empty
// TimelineResponse with no link header or items.
func EmptyTimelineResponse() *apimodel.TimelineResponse {
return &apimodel.TimelineResponse{
Items: []timeline.Timelineable{},
}
}