diff options
Diffstat (limited to 'internal/db/bundb/timeline.go')
| -rw-r--r-- | internal/db/bundb/timeline.go | 708 |
1 files changed, 216 insertions, 492 deletions
diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go index 404cb6601..8278de647 100644 --- a/internal/db/bundb/timeline.go +++ b/internal/db/bundb/timeline.go @@ -20,15 +20,13 @@ package bundb import ( "context" "errors" - "fmt" "slices" - "time" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/uptrace/bun" ) @@ -38,346 +36,147 @@ type timelineDB struct { state *state.State } -func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) { - // Ensure reasonable - if limit < 0 { - limit = 0 - } - - // Make educated guess for slice size - var ( - statusIDs = make([]string, 0, limit) - frontToBack = true - ) - - // As this is the home timeline, it should be - // populated by statuses from accounts followed - // by accountID, and posts from accountID itself. - // - // So, begin by seeing who accountID follows. - // It should be a little cheaper to do this in - // a separate query like this, rather than using - // a join, since followIDs are cached in memory. - follows, err := t.state.DB.GetAccountFollows( - gtscontext.SetBarebones(ctx), - accountID, - nil, // select all - ) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.Newf("db error getting follows for account %s: %w", accountID, err) - } - - // To take account of exclusive lists, get all of - // this account's lists, so we can filter out follows - // that are in contained in exclusive lists. - lists, err := t.state.DB.GetListsByAccountID(ctx, accountID) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.Newf("db error getting lists for account %s: %w", accountID, err) - } - - // Index all follow IDs that fall in exclusive lists. - ignoreFollowIDs := make(map[string]struct{}) - for _, list := range lists { - if !*list.Exclusive { - // Not exclusive, - // we don't care. - continue - } - - // Fetch all follow IDs of the entries ccontained in this list. - listFollowIDs, err := t.state.DB.GetFollowIDsInList(ctx, list.ID, nil) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.Newf("db error getting list entry follow ids: %w", err) - } - - // Exclusive list, index all its follow IDs. - for _, followID := range listFollowIDs { - ignoreFollowIDs[followID] = struct{}{} - } - } - - // Extract just the accountID from each follow, - // ignoring follows that are in exclusive lists. - targetAccountIDs := make([]string, 0, len(follows)+1) - for _, f := range follows { - _, ignore := ignoreFollowIDs[f.ID] - if !ignore { - targetAccountIDs = append( - targetAccountIDs, - f.TargetAccountID, +func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.Status, error) { + return loadStatusTimelinePage(ctx, t.db, t.state, + + // Paging + // params. + page, + + // The actual meat of the home-timeline query, outside + // of any paging parameters that selects by followings. + func(q *bun.SelectQuery) (*bun.SelectQuery, error) { + + // As this is the home timeline, it should be + // populated by statuses from accounts followed + // by accountID, and posts from accountID itself. + // + // So, begin by seeing who accountID follows. + // It should be a little cheaper to do this in + // a separate query like this, rather than using + // a join, since followIDs are cached in memory. + follows, err := t.state.DB.GetAccountFollows( + gtscontext.SetBarebones(ctx), + accountID, + nil, // select all + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting follows for account %s: %w", accountID, err) + } + + // To take account of exclusive lists, get all of + // this account's lists, so we can filter out follows + // that are in contained in exclusive lists. + lists, err := t.state.DB.GetListsByAccountID(ctx, accountID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting lists for account %s: %w", accountID, err) + } + + // Index all follow IDs that fall in exclusive lists. + ignoreFollowIDs := make(map[string]struct{}) + for _, list := range lists { + if !*list.Exclusive { + // Not exclusive, + // we don't care. + continue + } + + // Fetch all follow IDs of the entries ccontained in this list. + listFollowIDs, err := t.state.DB.GetFollowIDsInList(ctx, list.ID, nil) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting list entry follow ids: %w", err) + } + + // Exclusive list, index all its follow IDs. + for _, followID := range listFollowIDs { + ignoreFollowIDs[followID] = struct{}{} + } + } + + // Extract just the accountID from each follow, + // ignoring follows that are in exclusive lists. + targetAccountIDs := make([]string, 0, len(follows)+1) + for _, f := range follows { + _, ignore := ignoreFollowIDs[f.ID] + if !ignore { + targetAccountIDs = append( + targetAccountIDs, + f.TargetAccountID, + ) + } + } + + // Add accountID itself as a pseudo follow so that + // accountID can see its own posts in the timeline. + targetAccountIDs = append(targetAccountIDs, accountID) + + // Select only statuses authored by + // accounts with IDs in the slice. + q = q.Where( + "? IN (?)", + bun.Ident("account_id"), + bun.In(targetAccountIDs), ) - } - } - - // Add accountID itself as a pseudo follow so that - // accountID can see its own posts in the timeline. - targetAccountIDs = append(targetAccountIDs, accountID) - - // Now start building the database query. - q := t.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). - // Select only IDs from table - Column("status.id") - - if maxID == "" || maxID >= id.Highest { - const future = 24 * time.Hour - - // don't return statuses more than 24hr in the future - maxID = id.NewULIDFromTime(time.Now().Add(future)) - } - - // return only statuses LOWER (ie., older) than maxID - q = q.Where("? < ?", bun.Ident("status.id"), maxID) - - if sinceID != "" { - // return only statuses HIGHER (ie., newer) than sinceID - q = q.Where("? > ?", bun.Ident("status.id"), sinceID) - } - - if minID != "" { - // return only statuses HIGHER (ie., newer) than minID - q = q.Where("? > ?", bun.Ident("status.id"), minID) - - // page up - frontToBack = false - } - if local { - // return only statuses posted by local account havers - q = q.Where("? = ?", bun.Ident("status.local"), local) - } + // Only include statuses that aren't pending approval. + q = q.Where("NOT ? = ?", bun.Ident("pending_approval"), true) - // Select only statuses authored by - // accounts with IDs in the slice. - q = q.Where( - "? IN (?)", - bun.Ident("status.account_id"), - bun.In(targetAccountIDs), + return q, nil + }, ) - - // Only include statuses that aren't pending approval. - q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true) - - if limit > 0 { - // limit amount of statuses returned - q = q.Limit(limit) - } - - if frontToBack { - // Page down. - q = q.Order("status.id DESC") - } else { - // Page up. - q = q.Order("status.id ASC") - } - - if err := q.Scan(ctx, &statusIDs); err != nil { - return nil, err - } - - if len(statusIDs) == 0 { - return nil, nil - } - - // If we're paging up, we still want statuses - // to be sorted by ID desc, so reverse ids slice. - // https://zchee.github.io/golang-wiki/SliceTricks/#reversing - if !frontToBack { - for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 { - statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l] - } - } - - // Return status IDs loaded from cache + db. - return t.state.DB.GetStatusesByIDs(ctx, statusIDs) } -func (t *timelineDB) GetPublicTimeline( - ctx context.Context, - maxID string, - sinceID string, - minID string, - limit int, - local bool, -) ([]*gtsmodel.Status, error) { - // Ensure reasonable - if limit < 0 { - limit = 0 - } +func (t *timelineDB) GetPublicTimeline(ctx context.Context, page *paging.Page) ([]*gtsmodel.Status, error) { + return loadStatusTimelinePage(ctx, t.db, t.state, - if local { - return t.getLocalTimeline( - ctx, - maxID, - sinceID, - minID, - limit, - ) - } + // Paging + // params. + page, - // Make educated guess for slice size - var ( - statusIDs = make([]string, 0, limit) - frontToBack = true - ) + func(q *bun.SelectQuery) (*bun.SelectQuery, error) { + // Public only. + q = q.Where("? = ?", bun.Ident("visibility"), gtsmodel.VisibilityPublic) - q := t.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). - // Public only. - Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic). - // Ignore boosts. - Where("? IS NULL", bun.Ident("status.boost_of_id")). - // Only include statuses that aren't pending approval. - Where("? = ?", bun.Ident("status.pending_approval"), false). - // Select only IDs from table - Column("status.id") - - if maxID == "" || maxID >= id.Highest { - const future = 24 * time.Hour - - // don't return statuses more than 24hr in the future - maxID = id.NewULIDFromTime(time.Now().Add(future)) - } - - // return only statuses LOWER (ie., older) than maxID - q = q.Where("? < ?", bun.Ident("status.id"), maxID) - - if sinceID != "" { - // return only statuses HIGHER (ie., newer) than sinceID - q = q.Where("? > ?", bun.Ident("status.id"), sinceID) - } - - if minID != "" { - // return only statuses HIGHER (ie., newer) than minID - q = q.Where("? > ?", bun.Ident("status.id"), minID) - - // page up - frontToBack = false - } - - if limit > 0 { - // limit amount of statuses returned - q = q.Limit(limit) - } - - if frontToBack { - // Page down. - q = q.Order("status.id DESC") - } else { - // Page up. - q = q.Order("status.id ASC") - } - - if err := q.Scan(ctx, &statusIDs); err != nil { - return nil, err - } - - if len(statusIDs) == 0 { - return nil, nil - } - - // If we're paging up, we still want statuses - // to be sorted by ID desc, so reverse ids slice. - // https://zchee.github.io/golang-wiki/SliceTricks/#reversing - if !frontToBack { - for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 { - statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l] - } - } + // Ignore boosts. + q = q.Where("? IS NULL", bun.Ident("boost_of_id")) - // Return status IDs loaded from cache + db. - return t.state.DB.GetStatusesByIDs(ctx, statusIDs) -} + // Only include statuses that aren't pending approval. + q = q.Where("? = ?", bun.Ident("pending_approval"), false) -func (t *timelineDB) getLocalTimeline( - ctx context.Context, - maxID string, - sinceID string, - minID string, - limit int, -) ([]*gtsmodel.Status, error) { - // Make educated guess for slice size - var ( - statusIDs = make([]string, 0, limit) - frontToBack = true + return q, nil + }, ) +} - q := t.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). - // Local only. - Where("? = ?", bun.Ident("status.local"), true). - // Public only. - Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic). - // Only include statuses that aren't pending approval. - Where("? = ?", bun.Ident("status.pending_approval"), false). - // Ignore boosts. - Where("? IS NULL", bun.Ident("status.boost_of_id")). - // Select only IDs from table - Column("status.id") - - if maxID == "" || maxID >= id.Highest { - const future = 24 * time.Hour - - // don't return statuses more than 24hr in the future - maxID = id.NewULIDFromTime(time.Now().Add(future)) - } - - // return only statuses LOWER (ie., older) than maxID - q = q.Where("? < ?", bun.Ident("status.id"), maxID) - - if sinceID != "" { - // return only statuses HIGHER (ie., newer) than sinceID - q = q.Where("? > ?", bun.Ident("status.id"), sinceID) - } - - if minID != "" { - // return only statuses HIGHER (ie., newer) than minID - q = q.Where("? > ?", bun.Ident("status.id"), minID) - - // page up - frontToBack = false - } +func (t *timelineDB) GetLocalTimeline(ctx context.Context, page *paging.Page) ([]*gtsmodel.Status, error) { + return loadStatusTimelinePage(ctx, t.db, t.state, - if limit > 0 { - // limit amount of statuses returned - q = q.Limit(limit) - } + // Paging + // params. + page, - if frontToBack { - // Page down. - q = q.Order("status.id DESC") - } else { - // Page up. - q = q.Order("status.id ASC") - } + func(q *bun.SelectQuery) (*bun.SelectQuery, error) { + // Local only. + q = q.Where("? = ?", bun.Ident("local"), true) - if err := q.Scan(ctx, &statusIDs); err != nil { - return nil, err - } + // Public only. + q = q.Where("? = ?", bun.Ident("visibility"), gtsmodel.VisibilityPublic) - if len(statusIDs) == 0 { - return nil, nil - } + // Only include statuses that aren't pending approval. + q = q.Where("? = ?", bun.Ident("pending_approval"), false) - // If we're paging up, we still want statuses - // to be sorted by ID desc, so reverse ids slice. - // https://zchee.github.io/golang-wiki/SliceTricks/#reversing - if !frontToBack { - for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 { - statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l] - } - } + // Ignore boosts. + q = q.Where("? IS NULL", bun.Ident("boost_of_id")) - // Return status IDs loaded from cache + db. - return t.state.DB.GetStatusesByIDs(ctx, statusIDs) + return q, 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 (t *timelineDB) GetFavedTimeline(ctx context.Context, accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error) { + // Ensure reasonable if limit < 0 { limit = 0 @@ -442,205 +241,130 @@ func (t *timelineDB) GetFavedTimeline(ctx context.Context, accountID string, max return statuses, nextMaxID, prevMinID, nil } -func (t *timelineDB) GetListTimeline( - ctx context.Context, - listID string, - maxID string, - sinceID string, - minID string, - limit int, -) ([]*gtsmodel.Status, error) { - // Ensure reasonable - if limit < 0 { - limit = 0 - } - - // Make educated guess for slice size - var ( - statusIDs = make([]string, 0, limit) - frontToBack = true - ) - - // Fetch all follow IDs contained in list from DB. - followIDs, err := t.state.DB.GetFollowIDsInList( - ctx, listID, nil, - ) - if err != nil { - return nil, fmt.Errorf("error getting follows in list: %w", err) - } - - // If there's no list follows we can't - // possibly return anything for this list. - if len(followIDs) == 0 { - return make([]*gtsmodel.Status, 0), nil - } - - // Select target account IDs from follows. - subQ := t.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")). - Column("follow.target_account_id"). - Where("? IN (?)", bun.Ident("follow.id"), bun.In(followIDs)) - - // Select only status IDs created - // by one of the followed accounts. - q := t.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). - // Select only IDs from table - Column("status.id"). - Where("? IN (?)", bun.Ident("status.account_id"), subQ) - - if maxID == "" || maxID >= id.Highest { - const future = 24 * time.Hour +func (t *timelineDB) GetListTimeline(ctx context.Context, listID string, page *paging.Page) ([]*gtsmodel.Status, error) { + return loadStatusTimelinePage(ctx, t.db, t.state, - // don't return statuses more than 24hr in the future - maxID = id.NewULIDFromTime(time.Now().Add(future)) - } - - // return only statuses LOWER (ie., older) than maxID - q = q.Where("? < ?", bun.Ident("status.id"), maxID) - - if sinceID != "" { - // return only statuses HIGHER (ie., newer) than sinceID - q = q.Where("? > ?", bun.Ident("status.id"), sinceID) - } + // Paging + // params. + page, - if minID != "" { - // return only statuses HIGHER (ie., newer) than minID - q = q.Where("? > ?", bun.Ident("status.id"), minID) + // The actual meat of the list-timeline query, outside + // of any paging parameters, it selects by list entries. + func(q *bun.SelectQuery) (*bun.SelectQuery, error) { - // page up - frontToBack = false - } + // Fetch all follow IDs contained in list from DB. + followIDs, err := t.state.DB.GetFollowIDsInList( + ctx, listID, nil, + ) + if err != nil { + return nil, gtserror.Newf("error getting follows in list: %w", err) + } + + // Select target account + // IDs from list follows. + subQ := t.db.NewSelect(). + Table("follows"). + Column("follows.target_account_id"). + Where("? IN (?)", bun.Ident("follows.id"), bun.In(followIDs)) + q = q.Where("? IN (?)", bun.Ident("statuses.account_id"), subQ) + + // Only include statuses that aren't pending approval. + q = q.Where("NOT ? = ?", bun.Ident("pending_approval"), true) + + return q, nil + }, + ) +} - // Only include statuses that aren't pending approval. - q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true) +func (t *timelineDB) GetTagTimeline(ctx context.Context, tagID string, page *paging.Page) ([]*gtsmodel.Status, error) { + return loadStatusTimelinePage(ctx, t.db, t.state, - if limit > 0 { - // limit amount of statuses returned - q = q.Limit(limit) - } + // Paging + // params. + page, - if frontToBack { - // Page down. - q = q.Order("status.id DESC") - } else { - // Page up. - q = q.Order("status.id ASC") - } + // The actual meat of the list-timeline query, outside of any + // paging params, selects by status tags with public visibility. + func(q *bun.SelectQuery) (*bun.SelectQuery, error) { - if err := q.Scan(ctx, &statusIDs); err != nil { - return nil, err - } + // ... + q = q.Join( + "INNER JOIN ? ON ? = ?", + bun.Ident("status_to_tags"), + bun.Ident("statuses.id"), bun.Ident("status_to_tags.status_id"), + ) - if len(statusIDs) == 0 { - return nil, nil - } + // This tag only. + q = q.Where("? = ?", bun.Ident("status_to_tags.tag_id"), tagID) - // If we're paging up, we still want statuses - // to be sorted by ID desc, so reverse ids slice. - // https://zchee.github.io/golang-wiki/SliceTricks/#reversing - if !frontToBack { - for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 { - statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l] - } - } + // Public only. + q = q.Where("? = ?", bun.Ident("visibility"), gtsmodel.VisibilityPublic) - // Return status IDs loaded from cache + db. - return t.state.DB.GetStatusesByIDs(ctx, statusIDs) + return q, nil + }, + ) } -func (t *timelineDB) GetTagTimeline( +func loadStatusTimelinePage( ctx context.Context, - tagID string, - maxID string, - sinceID string, - minID string, - limit int, -) ([]*gtsmodel.Status, error) { - // Ensure reasonable - if limit < 0 { - limit = 0 - } - - // Make educated guess for slice size - var ( - statusIDs = make([]string, 0, limit) - frontToBack = true - ) + db *bun.DB, + state *state.State, + page *paging.Page, + query func(*bun.SelectQuery) (*bun.SelectQuery, error), +) ( + []*gtsmodel.Status, + error, +) { + // Extract page params. + minID := page.Min.Value + maxID := page.Max.Value + limit := page.Limit + order := page.Order() + + // Pre-allocate slice of IDs as dest. + statusIDs := make([]string, 0, limit) - q := t.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("status_to_tags"), bun.Ident("status_to_tag")). - Column("status_to_tag.status_id"). - // Join with statuses for filtering. - Join( - "INNER JOIN ? AS ? ON ? = ?", - bun.Ident("statuses"), bun.Ident("status"), - bun.Ident("status.id"), bun.Ident("status_to_tag.status_id"), - ). - // Public only. - Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic). - // This tag only. - Where("? = ?", bun.Ident("status_to_tag.tag_id"), tagID) - - if maxID == "" || maxID >= id.Highest { - const future = 24 * time.Hour - - // don't return statuses more than 24hr in the future - maxID = id.NewULIDFromTime(time.Now().Add(future)) + // Now start building the database query. + // + // Select the following: + // - status ID + q := db.NewSelect(). + Table("statuses"). + Column("id") + + // Append caller + // query details. + q, err := query(q) + if err != nil { + return nil, err } - // return only statuses LOWER (ie., older) than maxID - q = q.Where("? < ?", bun.Ident("status_to_tag.status_id"), maxID) - - if sinceID != "" { - // return only statuses HIGHER (ie., newer) than sinceID - q = q.Where("? > ?", bun.Ident("status_to_tag.status_id"), sinceID) + if maxID != "" { + // Set a maximum ID boundary if was given. + q = q.Where("? < ?", bun.Ident("id"), maxID) } if minID != "" { - // return only statuses HIGHER (ie., newer) than minID - q = q.Where("? > ?", bun.Ident("status_to_tag.status_id"), minID) - - // page up - frontToBack = false + // Set a minimum ID boundary if was given. + q = q.Where("? > ?", bun.Ident("id"), minID) } - // Only include statuses that aren't pending approval. - q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true) - - if limit > 0 { - // limit amount of statuses returned - q = q.Limit(limit) + // Set query ordering. + if order.Ascending() { + q = q.OrderExpr("? ASC", bun.Ident("id")) + } else /* i.e. descending */ { + q = q.OrderExpr("? DESC", bun.Ident("id")) } - if frontToBack { - // Page down. - q = q.Order("status_to_tag.status_id DESC") - } else { - // Page up. - q = q.Order("status_to_tag.status_id ASC") - } + // A limit should always + // be supplied for this. + q = q.Limit(limit) + // Finally, perform query into status ID slice. if err := q.Scan(ctx, &statusIDs); err != nil { return nil, err } - if len(statusIDs) == 0 { - return nil, nil - } - - // If we're paging up, we still want statuses - // to be sorted by ID desc, so reverse ids slice. - // https://zchee.github.io/golang-wiki/SliceTricks/#reversing - if !frontToBack { - for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 { - statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l] - } - } - - // Return status IDs loaded from cache + db. - return t.state.DB.GetStatusesByIDs(ctx, statusIDs) + // Fetch statuses from DB / cache with given IDs. + return state.DB.GetStatusesByIDs(ctx, statusIDs) } |
