summaryrefslogtreecommitdiff
path: root/internal/timeline/manager.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/timeline/manager.go')
-rw-r--r--internal/timeline/manager.go217
1 files changed, 217 insertions, 0 deletions
diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go
new file mode 100644
index 000000000..9d28b5060
--- /dev/null
+++ b/internal/timeline/manager.go
@@ -0,0 +1,217 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+*/
+
+package timeline
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "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/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+const (
+ desiredPostIndexLength = 400
+)
+
+// Manager abstracts functions for creating timelines for multiple accounts, and adding, removing, and fetching entries from those timelines.
+//
+// By the time a status hits the manager interface, it should already have been filtered and it should be established that the status indeed
+// belongs in the home timeline of the given account ID.
+//
+// The manager makes a distinction between *indexed* posts and *prepared* posts.
+//
+// Indexed posts consist of just that post's ID (in the database) and the time it was created. An indexed post takes up very little memory, so
+// it's not a huge priority to keep trimming the indexed posts list.
+//
+// Prepared posts consist of the post's database ID, the time it was created, AND the apimodel representation of that post, for quick serialization.
+// Prepared posts of course take up more memory than indexed posts, so they should be regularly pruned if they're not being actively served.
+type Manager interface {
+ // Ingest takes one status and indexes it into the timeline for the given account ID.
+ //
+ // It should already be established before calling this function that the status/post actually belongs in the timeline!
+ Ingest(status *gtsmodel.Status, timelineAccountID string) error
+ // IngestAndPrepare takes one status and indexes it into the timeline for the given account ID, and then immediately prepares it for serving.
+ // This is useful in cases where we know the status will need to be shown at the top of a user's timeline immediately (eg., a new status is created).
+ //
+ // It should already be established before calling this function that the status/post actually belongs in the timeline!
+ 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.
+ // If maxID is provided, it will return entries from that maxID onwards, inclusive.
+ 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(timelineAccountID string) int
+ // GetDesiredIndexLength returns the amount of posts that we, ideally, index for each user.
+ GetDesiredIndexLength() int
+ // GetOldestIndexedID returns the status ID for the oldest post that we have indexed for the given account.
+ GetOldestIndexedID(timelineAccountID string) (string, error)
+ // PrepareXFromTop prepares limit n amount of posts, based on their indexed representations, from the top of the index.
+ PrepareXFromTop(timelineAccountID string, limit int) error
+ // WipeStatusFromTimeline completely removes a status and from the index and prepared posts of the given account ID
+ //
+ // The returned int indicates how many entries were removed.
+ WipeStatusFromTimeline(timelineAccountID string, statusID string) (int, error)
+ // WipeStatusFromAllTimelines removes the status from the index and prepared posts of all timelines
+ WipeStatusFromAllTimelines(statusID string) error
+}
+
+// NewManager returns a new timeline manager with the given database, typeconverter, config, and log.
+func NewManager(db db.DB, tc typeutils.TypeConverter, config *config.Config, log *logrus.Logger) Manager {
+ return &manager{
+ accountTimelines: sync.Map{},
+ db: db,
+ tc: tc,
+ config: config,
+ log: log,
+ }
+}
+
+type manager struct {
+ accountTimelines sync.Map
+ db db.DB
+ tc typeutils.TypeConverter
+ config *config.Config
+ log *logrus.Logger
+}
+
+func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) error {
+ l := m.log.WithFields(logrus.Fields{
+ "func": "Ingest",
+ "timelineAccountID": timelineAccountID,
+ "statusID": status.ID,
+ })
+
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ l.Trace("ingesting status")
+ return t.IndexOne(status.CreatedAt, status.ID)
+}
+
+func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error {
+ l := m.log.WithFields(logrus.Fields{
+ "func": "IngestAndPrepare",
+ "timelineAccountID": timelineAccountID,
+ "statusID": status.ID,
+ })
+
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ l.Trace("ingesting status")
+ return t.IndexAndPrepareOne(status.CreatedAt, status.ID)
+}
+
+func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) {
+ l := m.log.WithFields(logrus.Fields{
+ "func": "Remove",
+ "timelineAccountID": timelineAccountID,
+ "statusID": statusID,
+ })
+
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ l.Trace("removing status")
+ return t.Remove(statusID)
+}
+
+func (m *manager) HomeTimeline(timelineAccountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, error) {
+ l := m.log.WithFields(logrus.Fields{
+ "func": "HomeTimelineGet",
+ "timelineAccountID": timelineAccountID,
+ })
+
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ statuses, err := t.Get(limit, maxID, sinceID, minID)
+ if err != nil {
+ l.Errorf("error getting statuses: %s", err)
+ }
+ return statuses, nil
+}
+
+func (m *manager) GetIndexedLength(timelineAccountID string) int {
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ return t.PostIndexLength()
+}
+
+func (m *manager) GetDesiredIndexLength() int {
+ return desiredPostIndexLength
+}
+
+func (m *manager) GetOldestIndexedID(timelineAccountID string) (string, error) {
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ return t.OldestIndexedPostID()
+}
+
+func (m *manager) PrepareXFromTop(timelineAccountID string, limit int) error {
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ return t.PrepareFromTop(limit)
+}
+
+func (m *manager) WipeStatusFromTimeline(timelineAccountID string, statusID string) (int, error) {
+ t := m.getOrCreateTimeline(timelineAccountID)
+
+ return t.Remove(statusID)
+}
+
+func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
+ errors := []string{}
+ m.accountTimelines.Range(func(k interface{}, i interface{}) bool {
+ t, ok := i.(Timeline)
+ if !ok {
+ panic("couldn't parse entry as Timeline, this should never happen so panic")
+ }
+
+ if _, err := t.Remove(statusID); err != nil {
+ errors = append(errors, err.Error())
+ }
+
+ return false
+ })
+
+ var err error
+ if len(errors) > 0 {
+ err = fmt.Errorf("one or more errors removing status %s from all timelines: %s", statusID, strings.Join(errors, ";"))
+ }
+
+ return err
+}
+
+func (m *manager) getOrCreateTimeline(timelineAccountID string) Timeline {
+ var t Timeline
+ i, ok := m.accountTimelines.Load(timelineAccountID)
+ if !ok {
+ t = NewTimeline(timelineAccountID, m.db, m.tc, m.log)
+ m.accountTimelines.Store(timelineAccountID, t)
+ } else {
+ t, ok = i.(Timeline)
+ if !ok {
+ panic("couldn't parse entry as Timeline, this should never happen so panic")
+ }
+ }
+
+ return t
+}