timelines working pretty alright!

This commit is contained in:
tsmethurst 2021-06-13 18:34:36 +02:00
parent d9d9a7a626
commit a25e53af4e
12 changed files with 218 additions and 237 deletions

View File

@ -30,7 +30,7 @@ import (
// WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org // WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org
func (m *Module) WebfingerGETRequest(c *gin.Context) { func (m *Module) WebfingerGETRequest(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "WebfingerGETRequest", "func": "WebfingerGETRequest",
"user-agent": c.Request.UserAgent(), "user-agent": c.Request.UserAgent(),
}) })

View File

@ -1165,33 +1165,19 @@ func (ps *postgresService) GetStatusesWhereFollowing(accountID string, maxID str
q = q.ColumnExpr("status.*"). q = q.ColumnExpr("status.*").
Join("JOIN follows AS f ON f.target_account_id = status.account_id"). Join("JOIN follows AS f ON f.target_account_id = status.account_id").
Where("f.account_id = ?", accountID) Where("f.account_id = ?", accountID).
Order("status.id DESC")
if maxID != "" { if maxID != "" {
s := &gtsmodel.Status{} q = q.Where("status.id < ?", maxID)
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err == nil {
q = q.Where("status.created_at < ?", s.CreatedAt)
}
} }
if sinceID != "" { if sinceID != "" {
s := &gtsmodel.Status{} q = q.Where("status.id > ?", sinceID)
if err := ps.conn.Model(s).Where("id = ?", sinceID).Select(); err == nil {
q = q.Where("status.created_at > ?", s.CreatedAt)
}
} }
if minID != "" { if minID != "" {
s := &gtsmodel.Status{} q = q.Where("status.id > ?", minID)
if err := ps.conn.Model(s).Where("id = ?", minID).Select(); err == nil {
q = q.Where("status.created_at > ?", s.CreatedAt)
}
}
if minID != "" {
q = q.Order("status.created_at")
} else {
q = q.Order("status.created_at DESC")
} }
if local { if local {
@ -1204,9 +1190,14 @@ func (ps *postgresService) GetStatusesWhereFollowing(accountID string, maxID str
err := q.Select() err := q.Select()
if err != nil { if err != nil {
if err != pg.ErrNoRows { if err == pg.ErrNoRows {
return nil, err return nil, db.ErrNoEntries{}
} }
return nil, err
}
if len(statuses) == 0 {
return nil, db.ErrNoEntries{}
} }
return statuses, nil return statuses, nil
@ -1219,42 +1210,34 @@ func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID s
Where("visibility = ?", gtsmodel.VisibilityPublic). Where("visibility = ?", gtsmodel.VisibilityPublic).
Where("? IS NULL", pg.Ident("in_reply_to_id")). Where("? IS NULL", pg.Ident("in_reply_to_id")).
Where("? IS NULL", pg.Ident("boost_of_id")). Where("? IS NULL", pg.Ident("boost_of_id")).
Limit(limit). Order("status.id DESC")
Order("created_at DESC")
if maxID != "" { if maxID != "" {
s := &gtsmodel.Status{} q = q.Where("status.id < ?", maxID)
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
return nil, err
}
q = q.Where("created_at < ?", s.CreatedAt)
}
if minID != "" {
s := &gtsmodel.Status{}
if err := ps.conn.Model(s).Where("id = ?", minID).Select(); err != nil {
return nil, err
}
q = q.Where("created_at > ?", s.CreatedAt)
} }
if sinceID != "" { if sinceID != "" {
s := &gtsmodel.Status{} q = q.Where("status.id > ?", sinceID)
if err := ps.conn.Model(s).Where("id = ?", sinceID).Select(); err != nil { }
return nil, err
} if minID != "" {
q = q.Where("created_at > ?", s.CreatedAt) q = q.Where("status.id > ?", minID)
} }
if local { if local {
q = q.Where("local = ?", local) q = q.Where("status.local = ?", local)
}
if limit > 0 {
q = q.Limit(limit)
} }
err := q.Select() err := q.Select()
if err != nil { if err != nil {
if err != pg.ErrNoRows { if err == pg.ErrNoRows {
return nil, err return nil, db.ErrNoEntries{}
} }
return nil, err
} }
return statuses, nil return statuses, nil
@ -1266,19 +1249,11 @@ func (ps *postgresService) GetNotificationsForAccount(accountID string, limit in
q := ps.conn.Model(&notifications).Where("target_account_id = ?", accountID) q := ps.conn.Model(&notifications).Where("target_account_id = ?", accountID)
if maxID != "" { if maxID != "" {
n := &gtsmodel.Notification{} q = q.Where("id < ?", maxID)
if err := ps.conn.Model(n).Where("id = ?", maxID).Select(); err != nil {
return nil, err
}
q = q.Where("created_at < ?", n.CreatedAt)
} }
if sinceID != "" { if sinceID != "" {
n := &gtsmodel.Notification{} q = q.Where("id > ?", sinceID)
if err := ps.conn.Model(n).Where("id = ?", sinceID).Select(); err != nil {
return nil, err
}
q = q.Where("created_at > ?", n.CreatedAt)
} }
if limit != 0 { if limit != 0 {

View File

@ -95,7 +95,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
continue continue
} }
switch iter.GetType().GetTypeName() { switch iter.GetType().GetTypeName() {
// we have the whole object so we can figure out what we're accepting // we have the whole object so we can figure out what we're accepting
case string(gtsmodel.ActivityStreamsFollow): case string(gtsmodel.ActivityStreamsFollow):
// ACCEPT FOLLOW // ACCEPT FOLLOW
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow) asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)

View File

@ -21,9 +21,7 @@ package processing
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"sort"
"sync" "sync"
"time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@ -38,80 +36,14 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st
Statuses: []*apimodel.Status{}, Statuses: []*apimodel.Status{},
} }
apiStatuses := []*apimodel.Status{} apiStatuses, err := p.timelineManager.HomeTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local)
maxIDMarker := maxID
sinceIDMarker := sinceID
minIDMarker := minID
gtsStatuses, err := p.db.GetStatusesWhereFollowing(authed.Account.ID, maxIDMarker, sinceIDMarker, minIDMarker, limit, local)
if err != nil { if err != nil {
if _, ok := err.(db.ErrNoEntries); !ok { return nil, gtserror.NewErrorInternalError(err)
return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting statuses from db: %s", err))
}
} }
for _, gtsStatus := range gtsStatuses {
// pull relevant accounts from the status -- we need this both for checking visibility and for serializing
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(gtsStatus)
if err != nil {
continue
}
visible, err := p.db.StatusVisible(gtsStatus, authed.Account, relevantAccounts)
if err != nil {
continue
}
if visible {
// check if this is a boost...
var reblogOfStatus *gtsmodel.Status
if gtsStatus.BoostOfID != "" {
s := &gtsmodel.Status{}
if err := p.db.GetByID(s.BoostOfID, s); err != nil {
continue
}
reblogOfStatus = s
}
// serialize the status (or, at least, convert it to a form that's ready to be serialized)
apiStatus, err := p.tc.StatusToMasto(gtsStatus, relevantAccounts.StatusAuthor, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, reblogOfStatus)
if err != nil {
continue
}
apiStatuses = append(apiStatuses, apiStatus)
sort.Slice(apiStatuses, func(i int, j int) bool {
is, err := time.Parse(time.RFC3339, apiStatuses[i].CreatedAt)
if err != nil {
panic(err)
}
js, err := time.Parse(time.RFC3339, apiStatuses[j].CreatedAt)
if err != nil {
panic(err)
}
return is.After(js)
})
if len(apiStatuses) == limit {
// we have enough
break
}
}
if len(apiStatuses) != 0 {
if maxIDMarker != "" {
maxIDMarker = apiStatuses[len(apiStatuses)-1].ID
}
if minIDMarker != "" {
minIDMarker = apiStatuses[0].ID
}
}
}
resp.Statuses = apiStatuses resp.Statuses = apiStatuses
if len(resp.Statuses) != 0 { // prepare the next and previous links
if len(apiStatuses) != 0 {
nextLink := &url.URL{ nextLink := &url.URL{
Scheme: p.config.Protocol, Scheme: p.config.Protocol,
Host: p.config.Host, Host: p.config.Host,
@ -259,7 +191,9 @@ func (p *processor) initTimelineFor(account *gtsmodel.Account, wg *sync.WaitGrou
statuses, err := p.db.GetStatusesWhereFollowing(account.ID, "", "", "", desiredIndexLength, false) statuses, err := p.db.GetStatusesWhereFollowing(account.ID, "", "", "", desiredIndexLength, false)
if err != nil { if err != nil {
l.Error(fmt.Errorf("initTimelineFor: error getting statuses: %s", err)) if _, ok := err.(db.ErrNoEntries); !ok {
l.Error(fmt.Errorf("initTimelineFor: error getting statuses: %s", err))
}
return return
} }
p.indexAndIngest(statuses, account, desiredIndexLength) p.indexAndIngest(statuses, account, desiredIndexLength)

View File

@ -8,11 +8,53 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
) )
func (t *timeline) Get(amount int, maxID string, sinceID string, minID string) ([]*apimodel.Status, error) {
var statuses []*apimodel.Status
var err error
// no params are defined to just fetch from the top
if maxID == "" && sinceID == "" && minID == "" {
statuses, err = t.GetXFromTop(amount)
// aysnchronously prepare the next predicted query so it's ready when the user asks for it
if len(statuses) != 0 {
nextMaxID := statuses[len(statuses)-1].ID
go t.prepareNextQuery(amount, nextMaxID, "", "")
}
}
// maxID is defined but sinceID isn't so take from behind
if maxID != "" && sinceID == "" {
statuses, err = t.GetXBehindID(amount, maxID)
// aysnchronously prepare the next predicted query so it's ready when the user asks for it
if len(statuses) != 0 {
nextMaxID := statuses[len(statuses)-1].ID
go t.prepareNextQuery(amount, nextMaxID, "", "")
}
}
// maxID is defined and sinceID || minID are as well, so take a slice between them
if maxID != "" && sinceID != "" {
statuses, err = t.GetXBetweenID(amount, maxID, minID)
}
if maxID != "" && minID != "" {
statuses, err = t.GetXBetweenID(amount, maxID, minID)
}
// maxID isn't defined, but sinceID || minID are, so take x before
if maxID == "" && sinceID != "" {
statuses, err = t.GetXBeforeID(amount, sinceID, true)
}
if maxID == "" && minID != "" {
statuses, err = t.GetXBeforeID(amount, minID, true)
}
return statuses, err
}
func (t *timeline) GetXFromTop(amount int) ([]*apimodel.Status, error) { func (t *timeline) GetXFromTop(amount int) ([]*apimodel.Status, error) {
// make a slice of statuses with the length we need to return // make a slice of statuses with the length we need to return
statuses := make([]*apimodel.Status, 0, amount) statuses := make([]*apimodel.Status, 0, amount)
// if there are no prepared posts, just return the empty slice
if t.preparedPosts.data == nil { if t.preparedPosts.data == nil {
t.preparedPosts.data = &list.List{} t.preparedPosts.data = &list.List{}
} }
@ -45,7 +87,6 @@ func (t *timeline) GetXBehindID(amount int, behindID string) ([]*apimodel.Status
// make a slice of statuses with the length we need to return // make a slice of statuses with the length we need to return
statuses := make([]*apimodel.Status, 0, amount) statuses := make([]*apimodel.Status, 0, amount)
// if there are no prepared posts, just return the empty slice
if t.preparedPosts.data == nil { if t.preparedPosts.data == nil {
t.preparedPosts.data = &list.List{} t.preparedPosts.data = &list.List{}
} }
@ -53,6 +94,7 @@ func (t *timeline) GetXBehindID(amount int, behindID string) ([]*apimodel.Status
// iterate through the modified list until we hit the mark we're looking for // iterate through the modified list until we hit the mark we're looking for
var position int var position int
var behindIDMark *list.Element var behindIDMark *list.Element
findMarkLoop: findMarkLoop:
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() { for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
position = position + 1 position = position + 1
@ -62,7 +104,6 @@ findMarkLoop:
} }
if entry.statusID == behindID { if entry.statusID == behindID {
fmt.Printf("\n\n\n GETXBEHINDID: FOUND BEHINDID %s WITH POSITION %d AND CREATEDAT %s \n\n\n", behindID, position, entry.createdAt.String())
behindIDMark = e behindIDMark = e
break findMarkLoop break findMarkLoop
} }
@ -70,22 +111,29 @@ findMarkLoop:
// we didn't find it, so we need to make sure it's indexed and prepared and then try again // we didn't find it, so we need to make sure it's indexed and prepared and then try again
if behindIDMark == nil { if behindIDMark == nil {
if err := t.IndexBehind(behindID, true, amount); err != nil { if err := t.IndexBehind(behindID, amount); err != nil {
return nil, fmt.Errorf("GetXBehindID: error indexing behind and including ID %s", behindID) return nil, fmt.Errorf("GetXBehindID: error indexing behind and including ID %s", behindID)
} }
if err := t.PrepareBehind(behindID, true, amount); err != nil { if err := t.PrepareBehind(behindID, amount); err != nil {
return nil, fmt.Errorf("GetXBehindID: error preparing behind and including ID %s", behindID) return nil, fmt.Errorf("GetXBehindID: error preparing behind and including ID %s", behindID)
} }
oldestID, err := t.OldestPreparedPostID()
if err != nil {
return nil, err
}
if oldestID == "" || oldestID == behindID {
// there is no oldest prepared post, or the oldest prepared post is still the post we're looking for entries after
// this means we should just return the empty statuses slice since we don't have any more posts to offer
return statuses, nil
}
return t.GetXBehindID(amount, behindID) return t.GetXBehindID(amount, behindID)
} }
// make sure we have enough posts prepared behind it to return what we're being asked for // make sure we have enough posts prepared behind it to return what we're being asked for
if t.preparedPosts.data.Len() < amount+position { if t.preparedPosts.data.Len() < amount+position {
fmt.Printf("\n\n\n GETXBEHINDID: PREPARED POSTS LENGTH %d WAS LESS THAN AMOUNT %d PLUS POSITION %d", t.preparedPosts.data.Len(), amount, position) if err := t.PrepareBehind(behindID, amount); err != nil {
if err := t.PrepareBehind(behindID, false, amount); err != nil {
return nil, err return nil, err
} }
fmt.Printf("\n\n\n GETXBEHINDID: PREPARED POSTS LENGTH IS NOW %d", t.preparedPosts.data.Len())
} }
// start serving from the entry right after the mark // start serving from the entry right after the mark
@ -97,7 +145,6 @@ serveloop:
return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry") return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry")
} }
fmt.Printf("\n\n\n GETXBEHINDID: SERVING STATUS ID %s WITH CREATEDAT %s \n\n\n", entry.statusID, entry.createdAt.String())
// serve up to the amount requested // serve up to the amount requested
statuses = append(statuses, entry.prepared) statuses = append(statuses, entry.prepared)
served = served + 1 served = served + 1
@ -165,8 +212,7 @@ findMarkLoop:
break serveloopFromTop break serveloopFromTop
} }
} }
} else if !startFromTop {
} else if startFromTop {
// start serving from the entry right before the mark // start serving from the entry right before the mark
serveloopFromBottom: serveloopFromBottom:
for e := beforeIDMark.Prev(); e != nil; e = e.Prev() { for e := beforeIDMark.Prev(); e != nil; e = e.Prev() {
@ -191,7 +237,6 @@ func (t *timeline) GetXBetweenID(amount int, behindID string, beforeID string) (
// make a slice of statuses with the length we need to return // make a slice of statuses with the length we need to return
statuses := make([]*apimodel.Status, 0, amount) statuses := make([]*apimodel.Status, 0, amount)
// if there are no prepared posts, just return the empty slice
if t.preparedPosts.data == nil { if t.preparedPosts.data == nil {
t.preparedPosts.data = &list.List{} t.preparedPosts.data = &list.List{}
} }
@ -220,7 +265,7 @@ findMarkLoop:
// make sure we have enough posts prepared behind it to return what we're being asked for // make sure we have enough posts prepared behind it to return what we're being asked for
if t.preparedPosts.data.Len() < amount+position { if t.preparedPosts.data.Len() < amount+position {
if err := t.PrepareBehind(behindID, false, amount); err != nil { if err := t.PrepareBehind(behindID, amount); err != nil {
return nil, err return nil, err
} }
} }

View File

@ -1,8 +1,12 @@
package timeline package timeline
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
func (t *timeline) IndexBefore(statusID string, include bool, amount int) error { func (t *timeline) IndexBefore(statusID string, include bool, amount int) error {
@ -44,41 +48,43 @@ func (t *timeline) IndexBefore(statusID string, include bool, amount int) error
return nil return nil
} }
func (t *timeline) IndexBehind(statusID string, include bool, amount int) error { func (t *timeline) IndexBehind(statusID string, amount int) error {
// filtered := []*gtsmodel.Status{} filtered := []*gtsmodel.Status{}
// offsetStatus := statusID offsetStatus := statusID
// grabloop: fmt.Println("\n\n\nENTERING GRABLOOP\n\n\n")
// for len(filtered) < amount { grabloop:
// statuses, err := t.db.GetStatusesWhereFollowing(t.accountID, amount, offsetStatus, include, false) for len(filtered) < amount {
// if err != nil { statuses, err := t.db.GetStatusesWhereFollowing(t.accountID, offsetStatus, "", "", amount, false)
// if _, ok := err.(db.ErrNoEntries); !ok { if err != nil {
// return fmt.Errorf("IndexBehindAndIncluding: error getting statuses from db: %s", err) 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
// break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail }
// } return fmt.Errorf("IndexBehindAndIncluding: error getting statuses from db: %s", err)
}
// for _, s := range statuses { for _, s := range statuses {
// relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(s) relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(s)
// if err != nil { if err != nil {
// continue continue
// } }
// visible, err := t.db.StatusVisible(s, t.account, relevantAccounts) visible, err := t.db.StatusVisible(s, t.account, relevantAccounts)
// if err != nil { if err != nil {
// continue continue
// } }
// if visible { if visible {
// filtered = append(filtered, s) filtered = append(filtered, s)
// } }
// offsetStatus = s.ID offsetStatus = s.ID
// } }
// } }
fmt.Println("\n\n\nLEAVING GRABLOOP\n\n\n")
// for _, s := range filtered { for _, s := range filtered {
// if err := t.IndexOne(s.CreatedAt, s.ID); err != nil { if err := t.IndexOne(s.CreatedAt, s.ID); err != nil {
// return fmt.Errorf("IndexBehindAndIncluding: error indexing status with id %s: %s", s.ID, err) return fmt.Errorf("IndexBehindAndIncluding: error indexing status with id %s: %s", s.ID, err)
// } }
// } }
return nil return nil
} }
@ -92,8 +98,7 @@ func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string) error {
defer t.Unlock() defer t.Unlock()
postIndexEntry := &postIndexEntry{ postIndexEntry := &postIndexEntry{
createdAt: statusCreatedAt, statusID: statusID,
statusID: statusID,
} }
return t.postIndex.insertIndexed(postIndexEntry) return t.postIndex.insertIndexed(postIndexEntry)
@ -104,8 +109,7 @@ func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string
defer t.Unlock() defer t.Unlock()
postIndexEntry := &postIndexEntry{ postIndexEntry := &postIndexEntry{
createdAt: statusCreatedAt, statusID: statusID,
statusID: statusID,
} }
if err := t.postIndex.insertIndexed(postIndexEntry); err != nil { if err := t.postIndex.insertIndexed(postIndexEntry); err != nil {
@ -118,3 +122,24 @@ func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string
return nil return nil
} }
func (t *timeline) OldestIndexedPostID() (string, error) {
var id string
if t.postIndex == nil || t.postIndex.data == nil {
// return an empty string if postindex hasn't been initialized yet
return id, nil
}
e := t.postIndex.data.Back()
if e == nil {
// return an empty string if there's no back entry (ie., the index list hasn't been initialized yet)
return id, nil
}
entry, ok := e.Value.(*postIndexEntry)
if !ok {
return id, errors.New("OldestIndexedPostID: could not parse e as a postIndexEntry")
}
return entry.statusID, nil
}

View File

@ -60,7 +60,7 @@ type Manager interface {
IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error
// HomeTimeline returns limit n amount of entries from the home timeline of the given account ID, in descending chronological order. // HomeTimeline returns limit n amount of entries from the home timeline of the given account ID, in descending chronological order.
// If maxID is provided, it will return entries from that maxID onwards, inclusive. // If maxID is provided, it will return entries from that maxID onwards, inclusive.
HomeTimeline(timelineAccountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, error) HomeTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, error)
// GetIndexedLength returns the amount of posts/statuses that have been *indexed* for the given account ID. // GetIndexedLength returns the amount of posts/statuses that have been *indexed* for the given account ID.
GetIndexedLength(timelineAccountID string) int GetIndexedLength(timelineAccountID string) int
// GetDesiredIndexLength returns the amount of posts that we, ideally, index for each user. // GetDesiredIndexLength returns the amount of posts that we, ideally, index for each user.
@ -143,18 +143,7 @@ func (m *manager) HomeTimeline(timelineAccountID string, maxID string, sinceID s
t := m.getOrCreateTimeline(timelineAccountID) t := m.getOrCreateTimeline(timelineAccountID)
var err error statuses, err := t.Get(limit, maxID, sinceID, minID)
var statuses []*apimodel.Status
if maxID != "" && sinceID != "" {
statuses, err = t.GetXBetweenID(limit, maxID, sinceID)
} else if maxID != "" {
statuses, err = t.GetXBehindID(limit, maxID)
} else if sinceID != "" {
statuses, err = t.GetXBeforeID(limit, sinceID, true)
} else {
statuses, err = t.GetXFromTop(limit)
}
if err != nil { if err != nil {
l.Errorf("error getting statuses: %s", err) l.Errorf("error getting statuses: %s", err)
} }

View File

@ -3,7 +3,6 @@ package timeline
import ( import (
"container/list" "container/list"
"errors" "errors"
"time"
) )
type postIndex struct { type postIndex struct {
@ -11,8 +10,7 @@ type postIndex struct {
} }
type postIndexEntry struct { type postIndexEntry struct {
createdAt time.Time statusID string
statusID string
} }
func (p *postIndex) insertIndexed(i *postIndexEntry) error { func (p *postIndex) insertIndexed(i *postIndexEntry) error {
@ -37,7 +35,7 @@ func (p *postIndex) insertIndexed(i *postIndexEntry) error {
// if the post to index is newer than e, insert it before e in the list // if the post to index is newer than e, insert it before e in the list
if insertMark == nil { if insertMark == nil {
if i.createdAt.After(entry.createdAt) { if i.statusID > entry.statusID {
insertMark = e insertMark = e
} }
} }

View File

@ -8,7 +8,26 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
func (t *timeline) PrepareBehind(statusID string, include bool, amount int) error { func (t *timeline) prepareNextQuery(amount int, maxID string, sinceID string, minID string) error {
var err error
// maxID is defined but sinceID isn't so take from behind
if maxID != "" && sinceID == "" {
err = t.PrepareBehind(maxID, amount)
}
// maxID isn't defined, but sinceID || minID are, so take x before
if maxID == "" && sinceID != "" {
err = t.PrepareBefore(sinceID, false, amount)
}
if maxID == "" && minID != "" {
err = t.PrepareBefore(minID, false, amount)
}
return err
}
func (t *timeline) PrepareBehind(statusID string, amount int) error {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
@ -25,9 +44,6 @@ prepareloop:
// we haven't hit the position we need to prepare from yet // we haven't hit the position we need to prepare from yet
if entry.statusID == statusID { if entry.statusID == statusID {
preparing = true preparing = true
if !include {
continue
}
} }
} }
@ -171,10 +187,29 @@ func (t *timeline) prepare(statusID string) error {
// shove it in prepared posts as a prepared posts entry // shove it in prepared posts as a prepared posts entry
preparedPostsEntry := &preparedPostsEntry{ preparedPostsEntry := &preparedPostsEntry{
createdAt: gtsStatus.CreatedAt, statusID: statusID,
statusID: statusID, prepared: apiModelStatus,
prepared: apiModelStatus,
} }
return t.preparedPosts.insertPrepared(preparedPostsEntry) return t.preparedPosts.insertPrepared(preparedPostsEntry)
} }
func (t *timeline) OldestPreparedPostID() (string, error) {
var id string
if t.preparedPosts == nil || t.preparedPosts.data == nil {
// return an empty string if prepared posts hasn't been initialized yet
return id, nil
}
e := t.preparedPosts.data.Back()
if e == nil {
// return an empty string if there's no back entry (ie., the index list hasn't been initialized yet)
return id, nil
}
entry, ok := e.Value.(*preparedPostsEntry)
if !ok {
return id, errors.New("OldestPreparedPostID: could not parse e as a preparedPostsEntry")
}
return entry.statusID, nil
}

View File

@ -3,7 +3,6 @@ package timeline
import ( import (
"container/list" "container/list"
"errors" "errors"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
) )
@ -13,9 +12,8 @@ type preparedPosts struct {
} }
type preparedPostsEntry struct { type preparedPostsEntry struct {
createdAt time.Time statusID string
statusID string prepared *apimodel.Status
prepared *apimodel.Status
} }
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error { func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
@ -40,7 +38,7 @@ func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
// if the post to index is newer than e, insert it before e in the list // if the post to index is newer than e, insert it before e in the list
if insertMark == nil { if insertMark == nil {
if i.createdAt.After(entry.createdAt) { if i.statusID > entry.statusID {
insertMark = e insertMark = e
} }
} }

View File

@ -19,7 +19,6 @@
package timeline package timeline
import ( import (
"errors"
"sync" "sync"
"time" "time"
@ -39,6 +38,7 @@ type Timeline interface {
RETRIEVAL FUNCTIONS RETRIEVAL FUNCTIONS
*/ */
Get(amount int, maxID string, sinceID string, minID string) ([]*apimodel.Status, error)
// GetXFromTop returns x amount of posts from the top of the timeline, from newest to oldest. // GetXFromTop returns x amount of posts from the top of the timeline, from newest to oldest.
GetXFromTop(amount int) ([]*apimodel.Status, error) GetXFromTop(amount int) ([]*apimodel.Status, error)
// GetXBehindID returns x amount of posts from the given id onwards, from newest to oldest. // GetXBehindID returns x amount of posts from the given id onwards, from newest to oldest.
@ -63,12 +63,7 @@ type Timeline interface {
// IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property. // IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property.
IndexOne(statusCreatedAt time.Time, statusID string) error IndexOne(statusCreatedAt time.Time, statusID string) error
// Remove removes a status from both the index and prepared posts.
//
// If a status has multiple entries in a timeline, they will all be removed.
//
// The returned int indicates the amount of entries that were removed.
Remove(statusID string) (int, error)
// OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong. // OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong.
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this. // If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
OldestIndexedPostID() (string, error) OldestIndexedPostID() (string, error)
@ -81,10 +76,13 @@ type Timeline interface {
PrepareFromTop(amount int) error PrepareFromTop(amount int) error
// PrepareBehind instructs the timeline to prepare the next amount of entries for serialization, from position onwards. // PrepareBehind instructs the timeline to prepare the next amount of entries for serialization, from position onwards.
// If include is true, then the given status ID will also be prepared, otherwise only entries behind it will be prepared. // If include is true, then the given status ID will also be prepared, otherwise only entries behind it will be prepared.
PrepareBehind(statusID string, include bool, amount int) error PrepareBehind(statusID string, amount int) error
// IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property, // IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property,
// and then immediately prepares it. // and then immediately prepares it.
IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) error IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) error
// OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared post, or an error if something goes wrong.
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
OldestPreparedPostID() (string, error)
/* /*
INFO FUNCTIONS INFO FUNCTIONS
@ -99,6 +97,12 @@ type Timeline interface {
// Reset instructs the timeline to reset to its base state -- cache only the minimum amount of posts. // Reset instructs the timeline to reset to its base state -- cache only the minimum amount of posts.
Reset() error Reset() error
// Remove removes a status from both the index and prepared posts.
//
// If a status has multiple entries in a timeline, they will all be removed.
//
// The returned int indicates the amount of entries that were removed.
Remove(statusID string) (int, error)
} }
// timeline fulfils the Timeline interface // timeline fulfils the Timeline interface
@ -134,24 +138,3 @@ func (t *timeline) PostIndexLength() int {
return t.postIndex.data.Len() return t.postIndex.data.Len()
} }
func (t *timeline) OldestIndexedPostID() (string, error) {
var id string
if t.postIndex == nil || t.postIndex.data == nil {
// return an empty string if postindex hasn't been initialized yet
return id, nil
}
e := t.postIndex.data.Back()
if e == nil {
// return an empty string if there's no back entry (ie., the index list hasn't been initialized yet)
return id, nil
}
entry, ok := e.Value.(*postIndexEntry)
if !ok {
return id, errors.New("OldestIndexedPostID: could not parse e as a postIndexEntry")
}
return entry.statusID, nil
}

View File

@ -125,7 +125,6 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo
acct.URL = uri.String() acct.URL = uri.String()
} }
// InboxURI // InboxURI
if accountable.GetActivityStreamsInbox() != nil && accountable.GetActivityStreamsInbox().GetIRI() != nil { if accountable.GetActivityStreamsInbox() != nil && accountable.GetActivityStreamsInbox().GetIRI() != nil {
acct.InboxURI = accountable.GetActivityStreamsInbox().GetIRI().String() acct.InboxURI = accountable.GetActivityStreamsInbox().GetIRI().String()