mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 16:12:24 -05: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
 | |
| }
 |