mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-04 04:12:25 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			283 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// GoToSocial
 | 
						|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
						|
// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
						|
//
 | 
						|
// 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 (
 | 
						|
	"container/list"
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
 | 
						|
	"codeberg.org/gruf/go-kv"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
						|
	"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
						|
)
 | 
						|
 | 
						|
func (t *timeline) indexXBetweenIDs(ctx context.Context, amount int, behindID string, beforeID string, frontToBack bool) error {
 | 
						|
	l := log.
 | 
						|
		WithContext(ctx).
 | 
						|
		WithFields(kv.Fields{
 | 
						|
			{"amount", amount},
 | 
						|
			{"behindID", behindID},
 | 
						|
			{"beforeID", beforeID},
 | 
						|
			{"frontToBack", frontToBack},
 | 
						|
		}...)
 | 
						|
	l.Trace("entering indexXBetweenIDs")
 | 
						|
 | 
						|
	if beforeID >= behindID {
 | 
						|
		// This is an impossible situation, we
 | 
						|
		// can't index anything between these.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	t.Lock()
 | 
						|
	defer t.Unlock()
 | 
						|
 | 
						|
	// Lazily init indexed items.
 | 
						|
	if t.items.data == nil {
 | 
						|
		t.items.data = &list.List{}
 | 
						|
		t.items.data.Init()
 | 
						|
	}
 | 
						|
 | 
						|
	// Start by mapping out the list so we know what
 | 
						|
	// we have to do. Depending on the current state
 | 
						|
	// of the list we might not have to do *anything*.
 | 
						|
	var (
 | 
						|
		position         int
 | 
						|
		listLen          = t.items.data.Len()
 | 
						|
		behindIDPosition int
 | 
						|
		beforeIDPosition int
 | 
						|
	)
 | 
						|
 | 
						|
	for e := t.items.data.Front(); e != nil; e = e.Next() {
 | 
						|
		entry := e.Value.(*indexedItemsEntry)
 | 
						|
 | 
						|
		position++
 | 
						|
 | 
						|
		if entry.itemID > behindID {
 | 
						|
			l.Trace("item is too new, continuing")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if behindIDPosition == 0 {
 | 
						|
			// Gone far enough through the list
 | 
						|
			// and found our behindID mark.
 | 
						|
			// We only need to set this once.
 | 
						|
			l.Tracef("found behindID mark %s at position %d", entry.itemID, position)
 | 
						|
			behindIDPosition = position
 | 
						|
		}
 | 
						|
 | 
						|
		if entry.itemID >= beforeID {
 | 
						|
			// Push the beforeID mark back
 | 
						|
			// one place every iteration.
 | 
						|
			l.Tracef("setting beforeID mark %s at position %d", entry.itemID, position)
 | 
						|
			beforeIDPosition = position
 | 
						|
		}
 | 
						|
 | 
						|
		if entry.itemID <= beforeID {
 | 
						|
			// We've gone beyond the bounds of
 | 
						|
			// items we're interested in; stop.
 | 
						|
			l.Trace("reached older items, breaking")
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// We can now figure out if we need to make db calls.
 | 
						|
	var grabMore bool
 | 
						|
	switch {
 | 
						|
	case listLen < amount:
 | 
						|
		// The whole list is shorter than the
 | 
						|
		// amount we're being asked to return,
 | 
						|
		// make up the difference.
 | 
						|
		grabMore = true
 | 
						|
		amount -= listLen
 | 
						|
	case beforeIDPosition-behindIDPosition < amount:
 | 
						|
		// Not enough items between behindID and
 | 
						|
		// beforeID to return amount required,
 | 
						|
		// try to get more.
 | 
						|
		grabMore = true
 | 
						|
	}
 | 
						|
 | 
						|
	if !grabMore {
 | 
						|
		// We're good!
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Fetch additional items.
 | 
						|
	items, err := t.grab(ctx, amount, behindID, beforeID, frontToBack)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Index all the items we got. We already have
 | 
						|
	// a lock on the timeline, so don't call IndexOne
 | 
						|
	// here, since that will also try to get a lock!
 | 
						|
	for _, item := range items {
 | 
						|
		entry := &indexedItemsEntry{
 | 
						|
			itemID:           item.GetID(),
 | 
						|
			boostOfID:        item.GetBoostOfID(),
 | 
						|
			accountID:        item.GetAccountID(),
 | 
						|
			boostOfAccountID: item.GetBoostOfAccountID(),
 | 
						|
		}
 | 
						|
 | 
						|
		if _, err := t.items.insertIndexed(ctx, entry); err != nil {
 | 
						|
			return gtserror.Newf("error inserting entry with itemID %s into index: %w", entry.itemID, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// grab wraps the timeline's grabFunction in paging + filtering logic.
 | 
						|
func (t *timeline) grab(ctx context.Context, amount int, behindID string, beforeID string, frontToBack bool) ([]Timelineable, error) {
 | 
						|
	var (
 | 
						|
		sinceID  string
 | 
						|
		minID    string
 | 
						|
		grabbed  int
 | 
						|
		maxID    = behindID
 | 
						|
		filtered = make([]Timelineable, 0, amount)
 | 
						|
	)
 | 
						|
 | 
						|
	if frontToBack {
 | 
						|
		sinceID = beforeID
 | 
						|
	} else {
 | 
						|
		minID = beforeID
 | 
						|
	}
 | 
						|
 | 
						|
	for attempts := 0; attempts < 5; attempts++ {
 | 
						|
		if grabbed >= amount {
 | 
						|
			// We got everything we needed.
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		items, stop, err := t.grabFunction(
 | 
						|
			ctx,
 | 
						|
			t.timelineID,
 | 
						|
			maxID,
 | 
						|
			sinceID,
 | 
						|
			minID,
 | 
						|
			// Don't grab more than we need to.
 | 
						|
			amount-grabbed,
 | 
						|
		)
 | 
						|
		if err != nil {
 | 
						|
			// Grab function already checks for
 | 
						|
			// db.ErrNoEntries, so if an error
 | 
						|
			// is returned then it's a real one.
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		if stop || len(items) == 0 {
 | 
						|
			// No items left.
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		// Set next query parameters.
 | 
						|
		if frontToBack {
 | 
						|
			// Page down.
 | 
						|
			maxID = items[len(items)-1].GetID()
 | 
						|
			if maxID <= beforeID {
 | 
						|
				// Can't go any further.
 | 
						|
				break
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			// Page up.
 | 
						|
			minID = items[0].GetID()
 | 
						|
			if minID >= behindID {
 | 
						|
				// Can't go any further.
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		for _, item := range items {
 | 
						|
			ok, err := t.filterFunction(ctx, t.timelineID, item)
 | 
						|
			if err != nil {
 | 
						|
				if !errors.Is(err, db.ErrNoEntries) {
 | 
						|
					// Real error here.
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
				log.Warnf(ctx, "errNoEntries while filtering item %s: %s", item.GetID(), err)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			if ok {
 | 
						|
				filtered = append(filtered, item)
 | 
						|
				grabbed++ // count this as grabbed
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return filtered, nil
 | 
						|
}
 | 
						|
 | 
						|
func (t *timeline) IndexAndPrepareOne(ctx context.Context, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) {
 | 
						|
	t.Lock()
 | 
						|
	defer t.Unlock()
 | 
						|
 | 
						|
	postIndexEntry := &indexedItemsEntry{
 | 
						|
		itemID:           statusID,
 | 
						|
		boostOfID:        boostOfID,
 | 
						|
		accountID:        accountID,
 | 
						|
		boostOfAccountID: boostOfAccountID,
 | 
						|
	}
 | 
						|
 | 
						|
	if inserted, err := t.items.insertIndexed(ctx, postIndexEntry); err != nil {
 | 
						|
		return false, gtserror.Newf("error inserting indexed: %w", err)
 | 
						|
	} else if !inserted {
 | 
						|
		// Entry wasn't inserted, so
 | 
						|
		// don't bother preparing it.
 | 
						|
		return false, nil
 | 
						|
	}
 | 
						|
 | 
						|
	preparable, err := t.prepareFunction(ctx, t.timelineID, statusID)
 | 
						|
	if err != nil {
 | 
						|
		return true, gtserror.Newf("error preparing: %w", err)
 | 
						|
	}
 | 
						|
	postIndexEntry.prepared = preparable
 | 
						|
 | 
						|
	return true, nil
 | 
						|
}
 | 
						|
 | 
						|
func (t *timeline) Len() int {
 | 
						|
	t.Lock()
 | 
						|
	defer t.Unlock()
 | 
						|
 | 
						|
	if t.items == nil || t.items.data == nil {
 | 
						|
		// indexedItems hasnt been initialized yet.
 | 
						|
		return 0
 | 
						|
	}
 | 
						|
 | 
						|
	return t.items.data.Len()
 | 
						|
}
 | 
						|
 | 
						|
func (t *timeline) OldestIndexedItemID() string {
 | 
						|
	t.Lock()
 | 
						|
	defer t.Unlock()
 | 
						|
 | 
						|
	if t.items == nil || t.items.data == nil {
 | 
						|
		// indexedItems hasnt been initialized yet.
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
 | 
						|
	e := t.items.data.Back()
 | 
						|
	if e == nil {
 | 
						|
		// List was empty.
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
 | 
						|
	return e.Value.(*indexedItemsEntry).itemID
 | 
						|
}
 |