mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-28 03:11:01 +00:00
[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
This commit is contained in:
parent
99fab9c6b6
commit
6f6e89e271
17 changed files with 303 additions and 98 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount)
|
||||
if len(filtered) == 0 {
|
||||
return util.EmptyTimelineResponse(), nil
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
return p.packageStatusResponse(s, "api/v1/timelines/public", s[len(s)-1].ID, s[0].ID, limit)
|
||||
timelineables := []timeline.Timelineable{}
|
||||
for _, i := range filtered {
|
||||
timelineables = append(timelineables, i)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return p.packageStatusResponse(s, "api/v1/favourites", nextMaxID, prevMinID, limit)
|
||||
timelineables := []timeline.Timelineable{}
|
||||
for _, i := range filtered {
|
||||
timelineables = append(timelineables, i)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
104
internal/util/timeline.go
Normal file
104
internal/util/timeline.go
Normal file
|
@ -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{},
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue