diff --git a/PROGRESS.md b/PROGRESS.md
index 20cc450ae..54e11d2b5 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -67,8 +67,8 @@ Things are moving on the project! As of July 2021 you can now:
* [ ] /api/v1/accounts/search GET (Search for an account)
* [ ] Bookmarks
* [ ] /api/v1/bookmarks GET (See bookmarked statuses)
- * [ ] Favourites
- * [ ] /api/v1/favourites GET (See faved statuses)
+ * [x] Favourites
+ * [x] /api/v1/favourites GET (See faved statuses)
* [ ] Mutes
* [ ] /api/v1/mutes GET (See list of muted accounts)
* [ ] Blocks
diff --git a/internal/api/client/favourites/favourites.go b/internal/api/client/favourites/favourites.go
new file mode 100644
index 000000000..e083f32f9
--- /dev/null
+++ b/internal/api/client/favourites/favourites.go
@@ -0,0 +1,67 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 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 .
+*/
+
+package favourites
+
+import (
+ "net/http"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
+ "github.com/superseriousbusiness/gotosocial/internal/router"
+)
+
+const (
+ // BasePath is the base URI path for serving favourites
+ BasePath = "/api/v1/favourites"
+
+ // MaxIDKey is the url query for setting a max status ID to return
+ MaxIDKey = "max_id"
+ // SinceIDKey is the url query for returning results newer than the given ID
+ SinceIDKey = "since_id"
+ // MinIDKey is the url query for returning results immediately newer than the given ID
+ MinIDKey = "min_id"
+ // LimitKey is for specifying maximum number of results to return.
+ LimitKey = "limit"
+ // LocalKey is for specifying whether only local statuses should be returned
+ LocalKey = "local"
+)
+
+// Module implements the ClientAPIModule interface for everything relating to viewing favourites
+type Module struct {
+ config *config.Config
+ processor processing.Processor
+ log *logrus.Logger
+}
+
+// New returns a new favourites module
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
+ return &Module{
+ config: config,
+ processor: processor,
+ log: log,
+ }
+}
+
+// Route attaches all routes from this module to the given router
+func (m *Module) Route(r router.Router) error {
+ r.AttachHandler(http.MethodGet, BasePath, m.FavouritesGETHandler)
+ return nil
+}
diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go
new file mode 100644
index 000000000..76eb921e0
--- /dev/null
+++ b/internal/api/client/favourites/favouritesget.go
@@ -0,0 +1,57 @@
+package favourites
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// FavouritesGETHandler handles GETting favourites.
+func (m *Module) FavouritesGETHandler(c *gin.Context) {
+ l := m.log.WithField("func", "PublicTimelineGETHandler")
+
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ l.Debugf("error authing: %s", err)
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ return
+ }
+
+ maxID := ""
+ maxIDString := c.Query(MaxIDKey)
+ if maxIDString != "" {
+ maxID = maxIDString
+ }
+
+ minID := ""
+ minIDString := c.Query(MinIDKey)
+ if minIDString != "" {
+ minID = minIDString
+ }
+
+ limit := 20
+ limitString := c.Query(LimitKey)
+ if limitString != "" {
+ i, err := strconv.ParseInt(limitString, 10, 64)
+ if err != nil {
+ l.Debugf("error parsing limit string: %s", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ return
+ }
+ limit = int(i)
+ }
+
+ resp, errWithCode := m.processor.FavedTimelineGet(authed, maxID, minID, limit)
+ if errWithCode != nil {
+ l.Debugf("error from processor FavedTimelineGet: %s", errWithCode)
+ c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ return
+ }
+
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
+ c.JSON(http.StatusOK, resp.Statuses)
+}
diff --git a/internal/api/client/timeline/home.go b/internal/api/client/timeline/home.go
index 86606a0dd..cb72895f9 100644
--- a/internal/api/client/timeline/home.go
+++ b/internal/api/client/timeline/home.go
@@ -94,6 +94,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
return
}
- c.Header("Link", resp.LinkHeader)
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
c.JSON(http.StatusOK, resp.Statuses)
}
diff --git a/internal/api/client/timeline/public.go b/internal/api/client/timeline/public.go
index f4b233064..6898d781b 100644
--- a/internal/api/client/timeline/public.go
+++ b/internal/api/client/timeline/public.go
@@ -81,12 +81,15 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
local = i
}
- statuses, errWithCode := m.processor.PublicTimelineGet(authed, maxID, sinceID, minID, limit, local)
+ resp, errWithCode := m.processor.PublicTimelineGet(authed, maxID, sinceID, minID, limit, local)
if errWithCode != nil {
- l.Debugf("error from processor account statuses get: %s", errWithCode)
+ l.Debugf("error from processor PublicTimelineGet: %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.Statuses)
}
diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go
index dfe05f47a..b8eb2e381 100644
--- a/internal/cliactions/server/server.go
+++ b/internal/cliactions/server/server.go
@@ -15,6 +15,7 @@
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
"github.com/superseriousbusiness/gotosocial/internal/api/client/filter"
"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest"
@@ -141,6 +142,7 @@
statusModule := status.New(c, processor, log)
securityModule := security.New(c, dbService, log)
streamingModule := streaming.New(c, processor, log)
+ favouritesModule := favourites.New(c, processor, log)
apis := []api.ClientModule{
// modules with middleware go first
@@ -167,6 +169,7 @@
emojiModule,
listsModule,
streamingModule,
+ favouritesModule,
}
for _, m := range apis {
diff --git a/internal/cliactions/testrig/testrig.go b/internal/cliactions/testrig/testrig.go
index 43d2db726..312d19a62 100644
--- a/internal/cliactions/testrig/testrig.go
+++ b/internal/cliactions/testrig/testrig.go
@@ -17,6 +17,7 @@
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
"github.com/superseriousbusiness/gotosocial/internal/api/client/filter"
"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest"
@@ -86,6 +87,7 @@
statusModule := status.New(c, processor, log)
securityModule := security.New(c, dbService, log)
streamingModule := streaming.New(c, processor, log)
+ favouritesModule := favourites.New(c, processor, log)
apis := []api.ClientModule{
// modules with middleware go first
@@ -112,6 +114,7 @@
emojiModule,
listsModule,
streamingModule,
+ favouritesModule,
}
for _, m := range apis {
diff --git a/internal/db/db.go b/internal/db/db.go
index 1d7ed8b58..0a3979df6 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -241,13 +241,26 @@ type DB interface {
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)
- // GetStatusesWhereFollowing returns a slice of statuses from accounts that are followed by the given account id.
- GetStatusesWhereFollowing(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
+ // GetHomeTimelineForAccount returns a slice of statuses from accounts that are followed by the given account id.
+ //
+ // Statuses should be returned in descending order of when they were created (newest first).
+ GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
- // GetPublicTimelineForAccount fetches the account's PUBLIC timline -- ie., posts and replies that are public.
+ // GetPublicTimelineForAccount fetches the account's PUBLIC timeline -- ie., posts and replies that are public.
// It will use the given filters and try to return as many statuses as possible up to the limit.
+ //
+ // Statuses should be returned in descending order of when they were created (newest first).
GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
+ // GetFavedTimelineForAccount fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved.
+ // It will use the given filters and try to return as many statuses as possible up to the limit.
+ //
+ // Note that unlike the other GetTimeline functions, the returned statuses will be arranged by their FAVE id, not the STATUS id.
+ // In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created.
+ //
+ // Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers.
+ GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error)
+
// GetNotificationsForAccount returns a list of notifications that pertain to the given accountID.
GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error)
diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go
index 614968e22..ad75cef15 100644
--- a/internal/db/pg/pg.go
+++ b/internal/db/pg/pg.go
@@ -814,92 +814,6 @@ func (ps *postgresService) WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmode
return accounts, nil
}
-func (ps *postgresService) GetStatusesWhereFollowing(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) {
- statuses := []*gtsmodel.Status{}
-
- q := ps.conn.Model(&statuses)
-
- q = q.ColumnExpr("status.*").
- Join("JOIN follows AS f ON f.target_account_id = status.account_id").
- Where("f.account_id = ?", accountID).
- Order("status.id DESC")
-
- if maxID != "" {
- q = q.Where("status.id < ?", maxID)
- }
-
- if sinceID != "" {
- q = q.Where("status.id > ?", sinceID)
- }
-
- if minID != "" {
- q = q.Where("status.id > ?", minID)
- }
-
- if local {
- q = q.Where("status.local = ?", local)
- }
-
- if limit > 0 {
- q = q.Limit(limit)
- }
-
- err := q.Select()
- if err != nil {
- if err == pg.ErrNoRows {
- return nil, db.ErrNoEntries{}
- }
- return nil, err
- }
-
- if len(statuses) == 0 {
- return nil, db.ErrNoEntries{}
- }
-
- return statuses, nil
-}
-
-func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) {
- statuses := []*gtsmodel.Status{}
-
- q := ps.conn.Model(&statuses).
- Where("visibility = ?", gtsmodel.VisibilityPublic).
- Where("? IS NULL", pg.Ident("in_reply_to_id")).
- Where("? IS NULL", pg.Ident("in_reply_to_uri")).
- Where("? IS NULL", pg.Ident("boost_of_id")).
- Order("status.id DESC")
-
- if maxID != "" {
- q = q.Where("status.id < ?", maxID)
- }
-
- if sinceID != "" {
- q = q.Where("status.id > ?", sinceID)
- }
-
- if minID != "" {
- q = q.Where("status.id > ?", minID)
- }
-
- if local {
- q = q.Where("status.local = ?", local)
- }
-
- if limit > 0 {
- q = q.Limit(limit)
- }
-
- err := q.Select()
- if err != nil {
- if err == pg.ErrNoRows {
- return nil, db.ErrNoEntries{}
- }
- return nil, err
- }
-
- return statuses, nil
-}
-
func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error) {
notifications := []*gtsmodel.Notification{}
diff --git a/internal/db/pg/timeline.go b/internal/db/pg/timeline.go
new file mode 100644
index 000000000..95acd4f38
--- /dev/null
+++ b/internal/db/pg/timeline.go
@@ -0,0 +1,185 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 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 .
+*/
+
+package pg
+
+import (
+ "sort"
+
+ "github.com/go-pg/pg/v10"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) {
+ statuses := []*gtsmodel.Status{}
+
+ q := ps.conn.Model(&statuses)
+
+ q = q.ColumnExpr("status.*").
+ Join("LEFT JOIN follows AS f ON f.target_account_id = status.account_id").
+ Where("f.account_id = ?", accountID).
+ Order("status.id DESC")
+
+ if maxID != "" {
+ q = q.Where("status.id < ?", maxID)
+ }
+
+ if sinceID != "" {
+ q = q.Where("status.id > ?", sinceID)
+ }
+
+ if minID != "" {
+ q = q.Where("status.id > ?", minID)
+ }
+
+ if local {
+ q = q.Where("status.local = ?", local)
+ }
+
+ if limit > 0 {
+ q = q.Limit(limit)
+ }
+
+ err := q.Select()
+ if err != nil {
+ if err == pg.ErrNoRows {
+ return nil, db.ErrNoEntries{}
+ }
+ return nil, err
+ }
+
+ if len(statuses) == 0 {
+ return nil, db.ErrNoEntries{}
+ }
+
+ return statuses, nil
+}
+
+func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) {
+ statuses := []*gtsmodel.Status{}
+
+ q := ps.conn.Model(&statuses).
+ Where("visibility = ?", gtsmodel.VisibilityPublic).
+ Where("? IS NULL", pg.Ident("in_reply_to_id")).
+ Where("? IS NULL", pg.Ident("in_reply_to_uri")).
+ Where("? IS NULL", pg.Ident("boost_of_id")).
+ Order("status.id DESC")
+
+ if maxID != "" {
+ q = q.Where("status.id < ?", maxID)
+ }
+
+ if sinceID != "" {
+ q = q.Where("status.id > ?", sinceID)
+ }
+
+ if minID != "" {
+ q = q.Where("status.id > ?", minID)
+ }
+
+ if local {
+ q = q.Where("status.local = ?", local)
+ }
+
+ if limit > 0 {
+ q = q.Limit(limit)
+ }
+
+ err := q.Select()
+ if err != nil {
+ if err == pg.ErrNoRows {
+ return nil, db.ErrNoEntries{}
+ }
+ return nil, err
+ }
+
+ if len(statuses) == 0 {
+ return nil, db.ErrNoEntries{}
+ }
+
+ return statuses, nil
+}
+
+// TODO optimize this query and the logic here, because it's slow as balls -- it takes like a literal second to return with a limit of 20!
+// It might be worth serving it through a timeline instead of raw DB queries, like we do for Home feeds.
+func (ps *postgresService) GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error) {
+
+ faves := []*gtsmodel.StatusFave{}
+
+ fq := ps.conn.Model(&faves).
+ Where("account_id = ?", accountID).
+ Order("id DESC")
+
+ if maxID != "" {
+ fq = fq.Where("id < ?", maxID)
+ }
+
+ if minID != "" {
+ fq = fq.Where("id > ?", minID)
+ }
+
+ if limit > 0 {
+ fq = fq.Limit(limit)
+ }
+
+ err := fq.Select()
+ if err != nil {
+ if err == pg.ErrNoRows {
+ return nil, "", "", db.ErrNoEntries{}
+ }
+ return nil, "", "", err
+ }
+
+ if len(faves) == 0 {
+ return nil, "", "", db.ErrNoEntries{}
+ }
+
+ // map[statusID]faveID -- we need this to sort statuses by fave ID rather than their own ID
+ statusesFavesMap := map[string]string{}
+
+ in := []string{}
+ for _, f := range faves {
+ statusesFavesMap[f.StatusID] = f.ID
+ in = append(in, f.StatusID)
+ }
+
+ statuses := []*gtsmodel.Status{}
+ err = ps.conn.Model(&statuses).Where("id IN (?)", pg.In(in)).Select()
+ if err != nil {
+ if err == pg.ErrNoRows {
+ return nil, "", "", db.ErrNoEntries{}
+ }
+ return nil, "", "", err
+ }
+
+ if len(statuses) == 0 {
+ return nil, "", "", db.ErrNoEntries{}
+ }
+
+ // arrange statuses by fave ID
+ sort.Slice(statuses, func(i int, j int) bool {
+ statusI := statuses[i]
+ statusJ := statuses[j]
+ return statusesFavesMap[statusI.ID] < statusesFavesMap[statusJ.ID]
+ })
+
+ nextMaxID := faves[len(faves)-1].ID
+ prevMinID := faves[0].ID
+ return statuses, nextMaxID, prevMinID, nil
+}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 302368411..bb4cd2da7 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -151,7 +151,9 @@ type Processor interface {
// HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode)
// PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters.
- PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode)
+ PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode)
+ // FavedTimelineGet returns faved statuses, with the given filters/parameters.
+ FavedTimelineGet(authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode)
// AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid.
AuthorizeStreamingRequest(accessToken string) (*gtsmodel.Account, error)
diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go
index 23f0d2944..0dfee6233 100644
--- a/internal/processing/status/fave.go
+++ b/internal/processing/status/fave.go
@@ -60,7 +60,7 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api
}
if newFave {
- thisFaveID, err := id.NewRandomULID()
+ thisFaveID, err := id.NewULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go
index 8f6b1d26b..b5cbf433a 100644
--- a/internal/processing/timeline.go
+++ b/internal/processing/timeline.go
@@ -31,33 +31,27 @@
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+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{},
}
-
- apiStatuses, err := p.timelineManager.HomeTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- resp.Statuses = apiStatuses
+ resp.Statuses = statuses
// prepare the next and previous links
- if len(apiStatuses) != 0 {
+ if len(statuses) != 0 {
nextLink := &url.URL{
Scheme: p.config.Protocol,
Host: p.config.Host,
- Path: "/api/v1/timelines/home",
- RawPath: url.PathEscape("api/v1/timelines/home"),
- RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, apiStatuses[len(apiStatuses)-1].ID),
+ Path: path,
+ RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, nextMaxID),
}
next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String())
prevLink := &url.URL{
Scheme: p.config.Protocol,
Host: p.config.Host,
- Path: "/api/v1/timelines/home",
- RawQuery: fmt.Sprintf("limit=%d&min_id=%s", limit, apiStatuses[0].ID),
+ 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)
@@ -66,37 +60,81 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st
return resp, nil
}
-func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode) {
- statuses, err := p.db.GetPublicTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
+func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+ statuses, err := p.timelineManager.HomeTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- s, err := p.filterStatuses(authed, statuses)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
+ if len(statuses) == 0 {
+ return &apimodel.StatusTimelineResponse{
+ Statuses: []*apimodel.Status{},
+ }, nil
}
- return s, nil
+ return p.packageStatusResponse(statuses, "api/v1/timelines/home", statuses[len(statuses)-1].ID, statuses[0].ID, limit)
}
-func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) {
- l := p.log.WithField("func", "filterStatuses")
+func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+ statuses, err := p.db.GetPublicTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
+ if err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // there are just no entries left
+ return &apimodel.StatusTimelineResponse{
+ Statuses: []*apimodel.Status{},
+ }, nil
+ }
+ // there's an actual error
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ s, err := p.filterPublicStatuses(authed, statuses)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.packageStatusResponse(s, "api/v1/timelines/public", s[len(s)-1].ID, s[0].ID, limit)
+}
+
+func (p *processor) FavedTimelineGet(authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+ statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimelineForAccount(authed.Account.ID, maxID, minID, limit)
+ if err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // there are just no entries left
+ return &apimodel.StatusTimelineResponse{
+ Statuses: []*apimodel.Status{},
+ }, nil
+ }
+ // there's an actual error
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ s, err := p.filterFavedStatuses(authed, statuses)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.packageStatusResponse(s, "api/v1/favourites", nextMaxID, prevMinID, limit)
+}
+
+func (p *processor) filterPublicStatuses(authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) {
+ l := p.log.WithField("func", "filterPublicStatuses")
apiStatuses := []*apimodel.Status{}
for _, s := range statuses {
targetAccount := >smodel.Account{}
if err := p.db.GetByID(s.AccountID, targetAccount); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
- l.Debugf("skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)
+ l.Debugf("filterPublicStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)
continue
}
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err))
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("filterPublicStatuses: error getting status author: %s", err))
}
- timelineable, err := p.filter.StatusHometimelineable(s, authed.Account)
+ timelineable, err := p.filter.StatusPublictimelineable(s, authed.Account)
if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err))
+ l.Debugf("filterPublicStatuses: skipping status %s because of an error checking status visibility: %s", s.ID, err)
+ continue
}
if !timelineable {
continue
@@ -104,7 +142,42 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
apiStatus, err := p.tc.StatusToMasto(s, authed.Account)
if err != nil {
- l.Debugf("skipping status %s because it couldn't be converted to its mastodon representation: %s", s.ID, err)
+ l.Debugf("filterPublicStatuses: skipping status %s because it couldn't be converted to its mastodon representation: %s", s.ID, err)
+ continue
+ }
+
+ apiStatuses = append(apiStatuses, apiStatus)
+ }
+
+ return apiStatuses, nil
+}
+
+func (p *processor) filterFavedStatuses(authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) {
+ l := p.log.WithField("func", "filterFavedStatuses")
+
+ apiStatuses := []*apimodel.Status{}
+ for _, s := range statuses {
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(s.AccountID, targetAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ l.Debugf("filterFavedStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)
+ continue
+ }
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("filterPublicStatuses: error getting status author: %s", err))
+ }
+
+ timelineable, err := p.filter.StatusVisible(s, authed.Account)
+ if err != nil {
+ l.Debugf("filterFavedStatuses: skipping status %s because of an error checking status visibility: %s", s.ID, err)
+ continue
+ }
+ if !timelineable {
+ continue
+ }
+
+ apiStatus, err := p.tc.StatusToMasto(s, authed.Account)
+ if err != nil {
+ l.Debugf("filterFavedStatuses: skipping status %s because it couldn't be converted to its mastodon representation: %s", s.ID, err)
continue
}
@@ -157,7 +230,7 @@ func (p *processor) initTimelineFor(account *gtsmodel.Account, wg *sync.WaitGrou
desiredIndexLength := p.timelineManager.GetDesiredIndexLength()
- statuses, err := p.db.GetStatusesWhereFollowing(account.ID, "", "", "", desiredIndexLength, false)
+ statuses, err := p.db.GetHomeTimelineForAccount(account.ID, "", "", "", desiredIndexLength, false)
if err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
l.Error(fmt.Errorf("initTimelineFor: error getting statuses: %s", err))
@@ -176,7 +249,7 @@ func (p *processor) initTimelineFor(account *gtsmodel.Account, wg *sync.WaitGrou
}
if rearmostStatusID != "" {
- moreStatuses, err := p.db.GetStatusesWhereFollowing(account.ID, rearmostStatusID, "", "", desiredIndexLength/2, false)
+ moreStatuses, err := p.db.GetHomeTimelineForAccount(account.ID, rearmostStatusID, "", "", desiredIndexLength/2, false)
if err != nil {
l.Error(fmt.Errorf("initTimelineFor: error getting more statuses: %s", err))
return
diff --git a/internal/timeline/index.go b/internal/timeline/index.go
index 261722b74..8c6b0d578 100644
--- a/internal/timeline/index.go
+++ b/internal/timeline/index.go
@@ -23,7 +23,7 @@ func (t *timeline) IndexBefore(statusID string, include bool, amount int) error
grabloop:
for len(filtered) < amount {
- statuses, err := t.db.GetStatusesWhereFollowing(t.accountID, "", offsetStatus, "", amount, false)
+ statuses, err := t.db.GetHomeTimelineForAccount(t.accountID, "", offsetStatus, "", amount, false)
if err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail
@@ -58,7 +58,7 @@ func (t *timeline) IndexBehind(statusID string, amount int) error {
grabloop:
for len(filtered) < amount {
- statuses, err := t.db.GetStatusesWhereFollowing(t.accountID, offsetStatus, "", "", amount, false)
+ statuses, err := t.db.GetHomeTimelineForAccount(t.accountID, offsetStatus, "", "", amount, false)
if err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail
diff --git a/internal/visibility/filter.go b/internal/visibility/filter.go
index d12ad0ff6..181eb8ee7 100644
--- a/internal/visibility/filter.go
+++ b/internal/visibility/filter.go
@@ -17,6 +17,11 @@ type Filter interface {
//
// This function will call StatusVisible internally, so it's not necessary to call it beforehand.
StatusHometimelineable(targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error)
+
+ // StatusPublictimelineable returns true if targetStatus should be in the public timeline of the requesting account.
+ //
+ // This function will call StatusVisible internally, so it's not necessary to call it beforehand.
+ StatusPublictimelineable(targetStatus *gtsmodel.Status, timelineOwnerAccount *gtsmodel.Account) (bool, error)
}
type filter struct {
diff --git a/internal/visibility/statuspublictimelineable.go b/internal/visibility/statuspublictimelineable.go
new file mode 100644
index 000000000..d7f68faee
--- /dev/null
+++ b/internal/visibility/statuspublictimelineable.go
@@ -0,0 +1,37 @@
+package visibility
+
+import (
+ "fmt"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (f *filter) StatusPublictimelineable(targetStatus *gtsmodel.Status, timelineOwnerAccount *gtsmodel.Account) (bool, error) {
+ l := f.log.WithFields(logrus.Fields{
+ "func": "StatusPublictimelineable",
+ "statusID": targetStatus.ID,
+ })
+
+ // Don't timeline a reply
+ if targetStatus.InReplyToURI != "" || targetStatus.InReplyToID != "" || targetStatus.InReplyToAccountID != "" {
+ return false, nil
+ }
+
+ // status owner should always be able to see their own status in their timeline so we can return early if this is the case
+ if timelineOwnerAccount != nil && targetStatus.AccountID == timelineOwnerAccount.ID {
+ return true, nil
+ }
+
+ v, err := f.StatusVisible(targetStatus, timelineOwnerAccount)
+ if err != nil {
+ return false, fmt.Errorf("StatusPublictimelineable: error checking visibility of status with id %s: %s", targetStatus.ID, err)
+ }
+
+ if !v {
+ l.Debug("status is not publicTimelineable because it's not visible to the requester")
+ return false, nil
+ }
+
+ return true, nil
+}