summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2022-06-08 20:22:49 +0200
committerLibravatar GitHub <noreply@github.com>2022-06-08 20:22:49 +0200
commit6f6e89e2715c9ecbadda6b8dbe5227995348dae8 (patch)
treeda867633cdd72981460baf76615a81ab3390ce9c
parent[frontend] linkify header mascot+title (#633) (diff)
downloadgotosocial-6f6e89e2715c9ecbadda6b8dbe5227995348dae8.tar.xz
[feature] Add paging via `Link` header for notifications and account statuses (#629)
* test link headers * page get account statuses properly * page get notifications * add util func for packaging timeline responses * return timelined stuff from accountstatusesget * rename timeline response * use new convenience function * go fmt
-rw-r--r--internal/api/client/account/statuses.go7
-rw-r--r--internal/api/client/account/statuses_test.go44
-rw-r--r--internal/api/client/favourites/favouritesget.go2
-rw-r--r--internal/api/client/notification/notificationsget.go7
-rw-r--r--internal/api/client/timeline/home.go2
-rw-r--r--internal/api/client/timeline/public.go2
-rw-r--r--internal/api/model/notification.go21
-rw-r--r--internal/api/model/timeline.go8
-rw-r--r--internal/processing/account.go2
-rw-r--r--internal/processing/account/account.go2
-rw-r--r--internal/processing/account/getstatuses.go39
-rw-r--r--internal/processing/notification.go21
-rw-r--r--internal/processing/notification_test.go12
-rw-r--r--internal/processing/processor.go10
-rw-r--r--internal/processing/statustimeline.go107
-rw-r--r--internal/util/timeline.go104
-rw-r--r--internal/web/profile.go11
17 files changed, 303 insertions, 98 deletions
diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go
index b440e582a..18b551fcc 100644
--- a/internal/api/client/account/statuses.go
+++ b/internal/api/client/account/statuses.go
@@ -222,12 +222,15 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
publicOnly = i
}
- statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
+ resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if errWithCode != nil {
l.Debugf("error from processor account statuses get: %s", errWithCode)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
- c.JSON(http.StatusOK, statuses)
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
+ c.JSON(http.StatusOK, resp.Items)
}
diff --git a/internal/api/client/account/statuses_test.go b/internal/api/client/account/statuses_test.go
index 0e95d47fc..1f935896c 100644
--- a/internal/api/client/account/statuses_test.go
+++ b/internal/api/client/account/statuses_test.go
@@ -37,7 +37,47 @@ type AccountStatusesTestSuite struct {
AccountStandardTestSuite
}
-func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() {
+func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() {
+ // set up the request
+ // we're getting statuses of admin
+ targetAccount := suite.testAccounts["admin_account"]
+ recorder := httptest.NewRecorder()
+ ctx := suite.newContext(recorder, http.MethodGet, nil, fmt.Sprintf("/api/v1/accounts/%s/statuses?limit=20&only_media=false&only_public=true", targetAccount.ID), "")
+ ctx.Params = gin.Params{
+ gin.Param{
+ Key: account.IDKey,
+ Value: targetAccount.ID,
+ },
+ }
+
+ // call the handler
+ suite.accountModule.AccountStatusesGETHandler(ctx)
+
+ // 1. we should have OK because our request was valid
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // 2. we should have no error message in the result body
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ // check the response
+ b, err := ioutil.ReadAll(result.Body)
+ assert.NoError(suite.T(), err)
+
+ // unmarshal the returned statuses
+ apimodelStatuses := []*apimodel.Status{}
+ err = json.Unmarshal(b, &apimodelStatuses)
+ suite.NoError(err)
+ suite.NotEmpty(apimodelStatuses)
+
+ for _, s := range apimodelStatuses {
+ suite.Equal(apimodel.VisibilityPublic, s.Visibility)
+ }
+
+ suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
+}
+
+func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() {
// set up the request
// we're getting statuses of admin
targetAccount := suite.testAccounts["admin_account"]
@@ -74,6 +114,8 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() {
suite.NotEmpty(s.MediaAttachments)
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
}
+
+ suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="prev"`, result.Header.Get("link"))
}
func TestAccountStatusesTestSuite(t *testing.T) {
diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go
index d112e9b95..5a317b2ea 100644
--- a/internal/api/client/favourites/favouritesget.go
+++ b/internal/api/client/favourites/favouritesget.go
@@ -61,5 +61,5 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
- c.JSON(http.StatusOK, resp.Statuses)
+ c.JSON(http.StatusOK, resp.Items)
}
diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notification/notificationsget.go
index 0a56ee80b..b6f7cdd01 100644
--- a/internal/api/client/notification/notificationsget.go
+++ b/internal/api/client/notification/notificationsget.go
@@ -74,12 +74,15 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
sinceID = sinceIDString
}
- notifs, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
+ resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
if errWithCode != nil {
l.Debugf("error processing notifications get: %s", errWithCode.Error())
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
- c.JSON(http.StatusOK, notifs)
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
+ c.JSON(http.StatusOK, resp.Items)
}
diff --git a/internal/api/client/timeline/home.go b/internal/api/client/timeline/home.go
index dfe1a0db0..47bc2943e 100644
--- a/internal/api/client/timeline/home.go
+++ b/internal/api/client/timeline/home.go
@@ -171,5 +171,5 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
- c.JSON(http.StatusOK, resp.Statuses)
+ c.JSON(http.StatusOK, resp.Items)
}
diff --git a/internal/api/client/timeline/public.go b/internal/api/client/timeline/public.go
index 075d9267c..06da6e947 100644
--- a/internal/api/client/timeline/public.go
+++ b/internal/api/client/timeline/public.go
@@ -171,5 +171,5 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
- c.JSON(http.StatusOK, resp.Statuses)
+ c.JSON(http.StatusOK, resp.Items)
}
diff --git a/internal/api/model/notification.go b/internal/api/model/notification.go
index 8f929cab4..efcd2431b 100644
--- a/internal/api/model/notification.go
+++ b/internal/api/model/notification.go
@@ -43,3 +43,24 @@ type Notification struct {
// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.
Status *Status `json:"status,omitempty"`
}
+
+/*
+ The below functions are added onto the apimodel notification so that it satisfies
+ the Timelineable interface in internal/timeline.
+*/
+
+func (n *Notification) GetID() string {
+ return n.ID
+}
+
+func (n *Notification) GetAccountID() string {
+ return ""
+}
+
+func (n *Notification) GetBoostOfID() string {
+ return ""
+}
+
+func (n *Notification) GetBoostOfAccountID() string {
+ return ""
+}
diff --git a/internal/api/model/timeline.go b/internal/api/model/timeline.go
index 66a5263b8..71d839ed2 100644
--- a/internal/api/model/timeline.go
+++ b/internal/api/model/timeline.go
@@ -18,9 +18,11 @@
package model
-// StatusTimelineResponse wraps a slice of statuses, ready to be serialized, along with the Link
+import "github.com/superseriousbusiness/gotosocial/internal/timeline"
+
+// TimelineResponse wraps a slice of timelineables, ready to be serialized, along with the Link
// header for the previous and next queries, to be returned to the client.
-type StatusTimelineResponse struct {
- Statuses []*Status
+type TimelineResponse struct {
+ Items []timeline.Timelineable
LinkHeader string
}
diff --git a/internal/processing/account.go b/internal/processing/account.go
index 25f024785..43c4f65e6 100644
--- a/internal/processing/account.go
+++ b/internal/processing/account.go
@@ -46,7 +46,7 @@ func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, 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.Status, 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.TimelineResponse, gtserror.WithCode) {
return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
}
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
index 7668da02c..2a27fc743 100644
--- a/internal/processing/account/account.go
+++ b/internal/processing/account/account.go
@@ -55,7 +55,7 @@ type Processor interface {
Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// 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.Status, 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.TimelineResponse, gtserror.WithCode)
// FollowersGet fetches a list of the target account's followers.
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.
diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go
index c185302c5..90bf8e06e 100644
--- a/internal/processing/account/getstatuses.go
+++ b/internal/processing/account/getstatuses.go
@@ -26,9 +26,11 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/timeline"
+ "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.Status, 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.TimelineResponse, gtserror.WithCode) {
if requestingAccount != nil {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
@@ -37,29 +39,48 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel
}
}
- apiStatuses := []apimodel.Status{}
-
statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if err != nil {
if err == db.ErrNoEntries {
- return apiStatuses, nil
+ return util.EmptyTimelineResponse(), nil
}
return nil, gtserror.NewErrorInternalError(err)
}
+ var filtered []*gtsmodel.Status
for _, s := range statuses {
visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
- if err != nil || !visible {
- continue
+ if err == nil && visible {
+ filtered = append(filtered, s)
}
+ }
+
+ if len(filtered) == 0 {
+ return util.EmptyTimelineResponse(), nil
+ }
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount)
+ timelineables := []timeline.Timelineable{}
+ for _, i := range filtered {
+ apiStatus, err := p.tc.StatusToAPIStatus(ctx, i, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err))
}
- apiStatuses = append(apiStatuses, *apiStatus)
+ timelineables = append(timelineables, apiStatus)
}
- return apiStatuses, nil
+ return util.PackageTimelineableResponse(util.TimelineableResponseParams{
+ Items: timelineables,
+ Path: fmt.Sprintf("/api/v1/accounts/%s/statuses", targetAccountID),
+ NextMaxIDValue: timelineables[len(timelineables)-1].GetID(),
+ PrevMinIDValue: timelineables[0].GetID(),
+ Limit: limit,
+ ExtraQueryParams: []string{
+ fmt.Sprintf("exclude_replies=%t", excludeReplies),
+ fmt.Sprintf("exclude_reblogs=%t", excludeReblogs),
+ fmt.Sprintf("pinned_only=%t", pinnedOnly),
+ fmt.Sprintf("only_media=%t", mediaOnly),
+ fmt.Sprintf("only_public=%t", publicOnly),
+ },
+ })
}
diff --git a/internal/processing/notification.go b/internal/processing/notification.go
index e976952e8..ee0e4ae34 100644
--- a/internal/processing/notification.go
+++ b/internal/processing/notification.go
@@ -26,9 +26,11 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/timeline"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
)
-func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) {
+func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode) {
l := logrus.WithField("func", "NotificationsGet")
notifs, err := p.db.GetNotifications(ctx, authed.Account.ID, limit, maxID, sinceID)
@@ -36,15 +38,26 @@ func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, li
return nil, gtserror.NewErrorInternalError(err)
}
- apiNotifs := []*apimodel.Notification{}
+ if len(notifs) == 0 {
+ return util.EmptyTimelineResponse(), nil
+ }
+
+ timelineables := []timeline.Timelineable{}
for _, n := range notifs {
apiNotif, err := p.tc.NotificationToAPINotification(ctx, n)
if err != nil {
l.Debugf("got an error converting a notification to api, will skip it: %s", err)
continue
}
- apiNotifs = append(apiNotifs, apiNotif)
+ timelineables = append(timelineables, apiNotif)
}
- return apiNotifs, nil
+ return util.PackageTimelineableResponse(util.TimelineableResponseParams{
+ Items: timelineables,
+ Path: "api/v1/notifications",
+ NextMaxIDValue: timelineables[len(timelineables)-1].GetID(),
+ PrevMinIDKey: "since_id",
+ PrevMinIDValue: timelineables[0].GetID(),
+ Limit: limit,
+ })
}
diff --git a/internal/processing/notification_test.go b/internal/processing/notification_test.go
index 6f2d44c5c..6ecca92a9 100644
--- a/internal/processing/notification_test.go
+++ b/internal/processing/notification_test.go
@@ -23,6 +23,7 @@ import (
"testing"
"github.com/stretchr/testify/suite"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
)
type NotificationTestSuite struct {
@@ -32,14 +33,19 @@ type NotificationTestSuite struct {
// get a notification where someone has liked our status
func (suite *NotificationTestSuite) TestGetNotifications() {
receivingAccount := suite.testAccounts["local_account_1"]
- notifs, err := suite.processor.NotificationsGet(context.Background(), suite.testAutheds["local_account_1"], 10, "", "")
+ notifsResponse, err := suite.processor.NotificationsGet(context.Background(), suite.testAutheds["local_account_1"], 10, "", "")
suite.NoError(err)
- suite.Len(notifs, 1)
- notif := notifs[0]
+ suite.Len(notifsResponse.Items, 1)
+ notif, ok := notifsResponse.Items[0].(*apimodel.Notification)
+ if !ok {
+ panic("notif in response wasn't *apimodel.Notification")
+ }
+
suite.NotNil(notif.Status)
suite.NotNil(notif.Status)
suite.NotNil(notif.Status.Account)
suite.Equal(receivingAccount.ID, notif.Status.Account.ID)
+ suite.Equal(`<http://localhost:8080/api/v1/notifications?limit=10&max_id=01F8Q0ANPTWW10DAKTX7BRPBJP>; rel="next", <http://localhost:8080/api/v1/notifications?limit=10&since_id=01F8Q0ANPTWW10DAKTX7BRPBJP>; rel="prev"`, notifsResponse.LinkHeader)
}
func TestNotificationTestSuite(t *testing.T) {
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 225606b70..a10e0d6b3 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -83,7 +83,7 @@ type Processor interface {
AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// 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.Status, 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.TimelineResponse, gtserror.WithCode)
// AccountFollowersGet fetches a list of the target account's followers.
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.
@@ -150,7 +150,7 @@ type Processor interface {
MediaUpdate(ctx context.Context, authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
// NotificationsGet
- NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode)
+ NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode)
// SearchGet performs a search with the given params, resolving/dereferencing remotely as desired
SearchGet(ctx context.Context, authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode)
@@ -177,11 +177,11 @@ type Processor interface {
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(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode)
+ HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode)
// 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.StatusTimelineResponse, gtserror.WithCode)
+ PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode)
// FavedTimelineGet returns faved statuses, with the given filters/parameters.
- FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode)
+ FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode)
// 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, error)
diff --git a/internal/processing/statustimeline.go b/internal/processing/statustimeline.go
index d4388f381..081709302 100644
--- a/internal/processing/statustimeline.go
+++ b/internal/processing/statustimeline.go
@@ -22,18 +22,17 @@ import (
"context"
"errors"
"fmt"
- "net/url"
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
)
@@ -139,114 +138,100 @@ func StatusSkipInsertFunction() timeline.SkipInsertFunction {
}
}
-func (p *processor) packageStatusResponse(statuses []*apimodel.Status, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
- resp := &apimodel.StatusTimelineResponse{
- Statuses: []*apimodel.Status{},
- }
- resp.Statuses = statuses
-
- // prepare the next and previous links
- if len(statuses) != 0 {
- protocol := config.GetProtocol()
- host := config.GetHost()
-
- nextLink := &url.URL{
- Scheme: protocol,
- Host: host,
- Path: path,
- RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, nextMaxID),
- }
- next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String())
-
- prevLink := &url.URL{
- Scheme: protocol,
- Host: host,
- Path: path,
- RawQuery: fmt.Sprintf("limit=%d&min_id=%s", limit, prevMinID),
- }
- prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String())
- resp.LinkHeader = fmt.Sprintf("%s, %s", next, prev)
- }
-
- return resp, nil
-}
-
-func (p *processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+func (p *processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) {
preparedItems, err := p.statusTimelines.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if len(preparedItems) == 0 {
- return &apimodel.StatusTimelineResponse{
- Statuses: []*apimodel.Status{},
- }, nil
+ return util.EmptyTimelineResponse(), nil
}
- statuses := []*apimodel.Status{}
+ timelineables := []timeline.Timelineable{}
for _, i := range preparedItems {
status, ok := i.(*apimodel.Status)
if !ok {
return nil, gtserror.NewErrorInternalError(errors.New("error converting prepared timeline entry to api status"))
}
- statuses = append(statuses, status)
+ timelineables = append(timelineables, status)
}
- return p.packageStatusResponse(statuses, "api/v1/timelines/home", statuses[len(preparedItems)-1].ID, statuses[0].ID, limit)
+ return util.PackageTimelineableResponse(util.TimelineableResponseParams{
+ Items: timelineables,
+ Path: "api/v1/timelines/home",
+ NextMaxIDValue: timelineables[len(timelineables)-1].GetID(),
+ PrevMinIDValue: timelineables[0].GetID(),
+ Limit: limit,
+ })
}
-func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) {
statuses, err := p.db.GetPublicTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local)
if err != nil {
if err == db.ErrNoEntries {
// there are just no entries left
- return &apimodel.StatusTimelineResponse{
- Statuses: []*apimodel.Status{},
- }, nil
+ return util.EmptyTimelineResponse(), nil
}
// there's an actual error
return nil, gtserror.NewErrorInternalError(err)
}
- s, err := p.filterPublicStatuses(ctx, authed, statuses)
+ filtered, err := p.filterPublicStatuses(ctx, authed, statuses)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- if len(s) == 0 {
- return &apimodel.StatusTimelineResponse{
- Statuses: []*apimodel.Status{},
- }, nil
+ if len(filtered) == 0 {
+ return util.EmptyTimelineResponse(), nil
+ }
+
+ timelineables := []timeline.Timelineable{}
+ for _, i := range filtered {
+ timelineables = append(timelineables, i)
}
- return p.packageStatusResponse(s, "api/v1/timelines/public", s[len(s)-1].ID, s[0].ID, limit)
+ return util.PackageTimelineableResponse(util.TimelineableResponseParams{
+ Items: timelineables,
+ Path: "api/v1/timelines/public",
+ NextMaxIDValue: timelineables[len(timelineables)-1].GetID(),
+ PrevMinIDValue: timelineables[0].GetID(),
+ Limit: limit,
+ })
}
-func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode) {
statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimeline(ctx, authed.Account.ID, maxID, minID, limit)
if err != nil {
if err == db.ErrNoEntries {
// there are just no entries left
- return &apimodel.StatusTimelineResponse{
- Statuses: []*apimodel.Status{},
- }, nil
+ return util.EmptyTimelineResponse(), nil
}
// there's an actual error
return nil, gtserror.NewErrorInternalError(err)
}
- s, err := p.filterFavedStatuses(ctx, authed, statuses)
+ filtered, err := p.filterFavedStatuses(ctx, authed, statuses)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- if len(s) == 0 {
- return &apimodel.StatusTimelineResponse{
- Statuses: []*apimodel.Status{},
- }, nil
+ if len(filtered) == 0 {
+ return util.EmptyTimelineResponse(), nil
+ }
+
+ timelineables := []timeline.Timelineable{}
+ for _, i := range filtered {
+ timelineables = append(timelineables, i)
}
- return p.packageStatusResponse(s, "api/v1/favourites", nextMaxID, prevMinID, limit)
+ return util.PackageTimelineableResponse(util.TimelineableResponseParams{
+ Items: timelineables,
+ Path: "api/v1/favourites",
+ NextMaxIDValue: nextMaxID,
+ PrevMinIDValue: prevMinID,
+ Limit: limit,
+ })
}
func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) {
diff --git a/internal/util/timeline.go b/internal/util/timeline.go
new file mode 100644
index 000000000..929464add
--- /dev/null
+++ b/internal/util/timeline.go
@@ -0,0 +1,104 @@
+/*
+ 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,
+ }
+
+ // prepare the next and previous links
+ if len(params.Items) != 0 {
+ protocol := config.GetProtocol()
+ host := config.GetHost()
+
+ nextRaw := fmt.Sprintf("limit=%d&%s=%s", params.Limit, params.NextMaxIDKey, params.NextMaxIDValue)
+ for _, p := range params.ExtraQueryParams {
+ nextRaw = nextRaw + "&" + p
+ }
+ nextLink := &url.URL{
+ Scheme: protocol,
+ Host: host,
+ Path: params.Path,
+ RawQuery: nextRaw,
+ }
+ next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String())
+
+ prevRaw := fmt.Sprintf("limit=%d&%s=%s", params.Limit, params.PrevMinIDKey, params.PrevMinIDValue)
+ for _, p := range params.ExtraQueryParams {
+ prevRaw = prevRaw + "&" + p
+ }
+ prevLink := &url.URL{
+ Scheme: protocol,
+ Host: host,
+ Path: params.Path,
+ RawQuery: prevRaw,
+ }
+ prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String())
+ 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{},
+ }
+}
diff --git a/internal/web/profile.go b/internal/web/profile.go
index 66419dcc7..3155c022d 100644
--- a/internal/web/profile.go
+++ b/internal/web/profile.go
@@ -29,6 +29,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -79,7 +80,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) {
// get latest 10 top-level public statuses;
// ie., exclude replies and boosts, public only,
// with or without media
- statuses, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true)
+ statusResp, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true)
if errWithCode != nil {
l.Debugf("error getting statuses from processor: %s", errWithCode.Error())
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
@@ -92,7 +93,11 @@ func (m *Module) profileTemplateHandler(c *gin.Context) {
randomIndex := rand.Intn(len(m.defaultAvatars))
dummyAvatar := m.defaultAvatars[randomIndex]
account.Avatar = dummyAvatar
- for _, s := range statuses {
+ for _, i := range statusResp.Items {
+ s, ok := i.(*apimodel.Status)
+ if !ok {
+ panic("timelineable was not *apimodel.Status")
+ }
s.Account.Avatar = dummyAvatar
}
}
@@ -100,7 +105,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) {
c.HTML(http.StatusOK, "profile.tmpl", gin.H{
"instance": instance,
"account": account,
- "statuses": statuses,
+ "statuses": statusResp.Items,
"stylesheets": []string{
"/assets/Fork-Awesome/css/fork-awesome.min.css",
"/assets/status.css",