| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | // 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 ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	"maps" | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	"slices" | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	"sync/atomic" | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-structr" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/paging" | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/util/xslices" | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | // StatusMeta contains minimum viable metadata | 
					
						
							|  |  |  | // about a Status in order to cache a timeline. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | type StatusMeta struct { | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	ID               string | 
					
						
							|  |  |  | 	AccountID        string | 
					
						
							|  |  |  | 	BoostOfID        string | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	BoostOfAccountID string | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// is an internal flag that may be set on | 
					
						
							|  |  |  | 	// a StatusMeta object that will prevent | 
					
						
							|  |  |  | 	// preparation of its apimodel.Status, due | 
					
						
							|  |  |  | 	// to it being a recently repeated boost. | 
					
						
							|  |  |  | 	repeatBoost bool | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// prepared contains prepared frontend API | 
					
						
							|  |  |  | 	// model for the referenced status. This may | 
					
						
							|  |  |  | 	// or may-not be nil depending on whether the | 
					
						
							|  |  |  | 	// status has been "unprepared" since the last | 
					
						
							|  |  |  | 	// call to "prepare" the frontend model. | 
					
						
							|  |  |  | 	prepared *apimodel.Status | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// loaded is a temporary field that may be | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// set for a newly loaded timeline status | 
					
						
							|  |  |  | 	// so that statuses don't need to be loaded | 
					
						
							|  |  |  | 	// from the database twice in succession. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// i.e. this will only be set if the status | 
					
						
							|  |  |  | 	// was newly inserted into the timeline cache. | 
					
						
							|  |  |  | 	// for existing cache items this will be nil. | 
					
						
							|  |  |  | 	loaded *gtsmodel.Status | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // StatusTimelines is a concurrency safe map of StatusTimeline{} | 
					
						
							|  |  |  | // objects, optimizing *very heavily* for reads over writes. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | type StatusTimelines struct { | 
					
						
							|  |  |  | 	ptr atomic.Pointer[map[string]*StatusTimeline] // ronly except by CAS | 
					
						
							|  |  |  | 	cap int | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // Init stores the given argument(s) such that any created StatusTimeline{} | 
					
						
							|  |  |  | // objects by MustGet() will initialize them with the given arguments. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) Init(cap int) { t.cap = cap } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // MustGet will attempt to fetch StatusTimeline{} stored under key, else creating one. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) MustGet(key string) *StatusTimeline { | 
					
						
							|  |  |  | 	var tt *StatusTimeline | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		// Load current ptr. | 
					
						
							|  |  |  | 		cur := t.ptr.Load() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Get timeline map to work on. | 
					
						
							|  |  |  | 		var m map[string]*StatusTimeline | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if cur != nil { | 
					
						
							|  |  |  | 			// Look for existing | 
					
						
							|  |  |  | 			// timeline in cache. | 
					
						
							|  |  |  | 			tt = (*cur)[key] | 
					
						
							|  |  |  | 			if tt != nil { | 
					
						
							|  |  |  | 				return tt | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Get clone of current | 
					
						
							|  |  |  | 			// before modifications. | 
					
						
							|  |  |  | 			m = maps.Clone(*cur) | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			// Allocate new timeline map for below. | 
					
						
							|  |  |  | 			m = make(map[string]*StatusTimeline) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if tt == nil { | 
					
						
							|  |  |  | 			// Allocate new timeline. | 
					
						
							|  |  |  | 			tt = new(StatusTimeline) | 
					
						
							|  |  |  | 			tt.Init(t.cap) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Store timeline | 
					
						
							|  |  |  | 		// in new map. | 
					
						
							|  |  |  | 		m[key] = tt | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Attempt to update the map ptr. | 
					
						
							|  |  |  | 		if !t.ptr.CompareAndSwap(cur, &m) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// We failed the | 
					
						
							|  |  |  | 			// CAS, reloop. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Successfully inserted | 
					
						
							|  |  |  | 		// new timeline model. | 
					
						
							|  |  |  | 		return tt | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // Delete will delete the stored StatusTimeline{} under key, if any. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) Delete(key string) { | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		// Load current ptr. | 
					
						
							|  |  |  | 		cur := t.ptr.Load() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Check for empty map / not in map. | 
					
						
							|  |  |  | 		if cur == nil || (*cur)[key] == nil { | 
					
						
							|  |  |  | 			return | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Get clone of current | 
					
						
							|  |  |  | 		// before modifications. | 
					
						
							|  |  |  | 		m := maps.Clone(*cur) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Delete ID. | 
					
						
							|  |  |  | 		delete(m, key) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Attempt to update the map ptr. | 
					
						
							|  |  |  | 		if !t.ptr.CompareAndSwap(cur, &m) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// We failed the | 
					
						
							|  |  |  | 			// CAS, reloop. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Successfully | 
					
						
							|  |  |  | 		// deleted ID. | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // RemoveByStatusIDs calls RemoveByStatusIDs() for each of the stored StatusTimeline{}s. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) RemoveByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.RemoveByStatusIDs(statusIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // RemoveByAccountIDs calls RemoveByAccountIDs() for each of the stored StatusTimeline{}s. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) RemoveByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.RemoveByAccountIDs(accountIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // UnprepareByStatusIDs calls UnprepareByStatusIDs() for each of the stored StatusTimeline{}s. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) UnprepareByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.UnprepareByStatusIDs(statusIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // UnprepareByAccountIDs calls UnprepareByAccountIDs() for each of the stored StatusTimeline{}s. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) UnprepareByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.UnprepareByAccountIDs(accountIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // Unprepare attempts to call UnprepareAll() for StatusTimeline{} under key. | 
					
						
							| 
									
										
										
										
											2025-03-23 14:18:24 +00:00
										 |  |  | func (t *StatusTimelines) Unprepare(key string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		if tt := (*p)[key]; tt != nil { | 
					
						
							|  |  |  | 			tt.UnprepareAll() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // UnprepareAll calls UnprepareAll() for each of the stored StatusTimeline{}s. | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | func (t *StatusTimelines) UnprepareAll() { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.UnprepareAll() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // Trim calls Trim() for each of the stored StatusTimeline{}s. | 
					
						
							|  |  |  | func (t *StatusTimelines) Trim() { | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 			tt.Trim() | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // Clear attempts to call Clear() for StatusTimeline{} under key. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) Clear(key string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		if tt := (*p)[key]; tt != nil { | 
					
						
							|  |  |  | 			tt.Clear() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // ClearAll calls Clear() for each of the stored StatusTimeline{}s. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimelines) ClearAll() { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.Clear() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // StatusTimeline provides a concurrency-safe timeline | 
					
						
							|  |  |  | // cache of status information. Internally only StatusMeta{} | 
					
						
							|  |  |  | // objects are stored, and the statuses themselves are loaded | 
					
						
							|  |  |  | // as-needed, caching prepared frontend representations where | 
					
						
							|  |  |  | // possible. This is largely wrapping code for our own codebase | 
					
						
							|  |  |  | // to be able to smoothly interact with structr.Timeline{}. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | type StatusTimeline struct { | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:32:36 +01:00
										 |  |  | 	// underlying timeline cache of *StatusMeta{}, | 
					
						
							|  |  |  | 	// primary-keyed by ID, with extra indices below. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	cache structr.Timeline[*StatusMeta, string] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// fast-access cache indices. | 
					
						
							|  |  |  | 	idx_ID               *structr.Index //nolint:revive | 
					
						
							|  |  |  | 	idx_AccountID        *structr.Index //nolint:revive | 
					
						
							|  |  |  | 	idx_BoostOfID        *structr.Index //nolint:revive | 
					
						
							|  |  |  | 	idx_BoostOfAccountID *structr.Index //nolint:revive | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// cutoff and maximum item lengths. | 
					
						
							|  |  |  | 	// the timeline is trimmed back to | 
					
						
							|  |  |  | 	// cutoff on each call to Trim(), | 
					
						
							|  |  |  | 	// and maximum len triggers a Trim(). | 
					
						
							| 
									
										
										
										
											2025-03-20 14:01:26 +00:00
										 |  |  | 	// | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// the timeline itself does not | 
					
						
							| 
									
										
										
										
											2025-04-01 14:32:36 +01:00
										 |  |  | 	// limit items due to complexities | 
					
						
							|  |  |  | 	// it would introduce, so we apply | 
					
						
							|  |  |  | 	// a 'cut-off' at regular intervals. | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	cut, max int | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | // Init will initialize the timeline for usage, | 
					
						
							|  |  |  | // by preparing internal indices etc. This also | 
					
						
							|  |  |  | // sets the given max capacity for Trim() operations. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | func (t *StatusTimeline) Init(cap int) { | 
					
						
							|  |  |  | 	t.cache.Init(structr.TimelineConfig[*StatusMeta, string]{ | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Timeline item primary key field. | 
					
						
							| 
									
										
										
										
											2025-03-28 13:51:31 +00:00
										 |  |  | 		PKey: structr.IndexConfig{Fields: "ID"}, | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | 		// Additional indexed fields. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		Indices: []structr.IndexConfig{ | 
					
						
							|  |  |  | 			{Fields: "AccountID", Multiple: true}, | 
					
						
							|  |  |  | 			{Fields: "BoostOfAccountID", Multiple: true}, | 
					
						
							| 
									
										
										
										
											2025-04-03 17:17:39 +01:00
										 |  |  | 			{Fields: "BoostOfID", Multiple: true}, | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | 		// Timeline item copy function. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		Copy: func(s *StatusMeta) *StatusMeta { | 
					
						
							|  |  |  | 			var prepared *apimodel.Status | 
					
						
							|  |  |  | 			if s.prepared != nil { | 
					
						
							|  |  |  | 				prepared = new(apimodel.Status) | 
					
						
							|  |  |  | 				*prepared = *s.prepared | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return &StatusMeta{ | 
					
						
							|  |  |  | 				ID:               s.ID, | 
					
						
							|  |  |  | 				AccountID:        s.AccountID, | 
					
						
							|  |  |  | 				BoostOfID:        s.BoostOfID, | 
					
						
							|  |  |  | 				BoostOfAccountID: s.BoostOfAccountID, | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 				repeatBoost:      s.repeatBoost, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 				loaded:           nil, // NEVER stored | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 				prepared:         prepared, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 15:39:15 +01:00
										 |  |  | 	// Get fast index lookup ptrs. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	t.idx_ID = t.cache.Index("ID") | 
					
						
							|  |  |  | 	t.idx_AccountID = t.cache.Index("AccountID") | 
					
						
							|  |  |  | 	t.idx_BoostOfID = t.cache.Index("BoostOfID") | 
					
						
							|  |  |  | 	t.idx_BoostOfAccountID = t.cache.Index("BoostOfAccountID") | 
					
						
							| 
									
										
										
										
											2025-03-31 15:41:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// Set maximum capacity and | 
					
						
							|  |  |  | 	// cutoff threshold we trim to. | 
					
						
							|  |  |  | 	t.cut = int(0.60 * float64(cap)) | 
					
						
							| 
									
										
										
										
											2025-03-31 15:41:15 +01:00
										 |  |  | 	t.max = cap | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:10:53 +01:00
										 |  |  | // Load will load timeline statuses according to given | 
					
						
							|  |  |  | // page, using provided callbacks to load extra data when | 
					
						
							|  |  |  | // necessary, and perform fine-grained filtering loaded | 
					
						
							|  |  |  | // database models before eventual return to the user. The | 
					
						
							|  |  |  | // returned strings are the lo, hi ID paging values, used | 
					
						
							|  |  |  | // for generation of next, prev page links in the response. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | func (t *StatusTimeline) Load( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	page *paging.Page, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// loadPage should load the timeline of given page for cache hydration. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	// loadIDs should load status models with given IDs, this is used | 
					
						
							|  |  |  | 	// to load status models of already cached entries in the timeline. | 
					
						
							|  |  |  | 	loadIDs func(ids []string) (statuses []*gtsmodel.Status, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	// filter can be used to perform filtering of returned | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// statuses BEFORE insert into cache. i.e. this will effect | 
					
						
							|  |  |  | 	// what actually gets stored in the timeline cache. | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	filter func(each *gtsmodel.Status) (delete bool, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// prepareAPI should prepare internal status model to frontend API model. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	prepareAPI func(status *gtsmodel.Status) (apiStatus *apimodel.Status, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | ) ( | 
					
						
							|  |  |  | 	[]*apimodel.Status, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	string, // lo | 
					
						
							|  |  |  | 	string, // hi | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	error, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	switch { | 
					
						
							|  |  |  | 	case page == nil: | 
					
						
							|  |  |  | 		panic("nil page") | 
					
						
							|  |  |  | 	case loadPage == nil: | 
					
						
							|  |  |  | 		panic("nil load page func") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Get paging details. | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 	lo := page.Min.Value | 
					
						
							|  |  |  | 	hi := page.Max.Value | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 	limit := page.Limit | 
					
						
							|  |  |  | 	order := page.Order() | 
					
						
							|  |  |  | 	dir := toDirection(order) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Use a copy of current page so | 
					
						
							|  |  |  | 	// we can repeatedly update it. | 
					
						
							|  |  |  | 	nextPg := new(paging.Page) | 
					
						
							|  |  |  | 	*nextPg = *page | 
					
						
							|  |  |  | 	nextPg.Min.Value = lo | 
					
						
							|  |  |  | 	nextPg.Max.Value = hi | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	// First we attempt to load status | 
					
						
							|  |  |  | 	// metadata entries from the timeline | 
					
						
							|  |  |  | 	// cache, up to given limit. | 
					
						
							|  |  |  | 	metas := t.cache.Select( | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 		util.PtrIf(lo), | 
					
						
							|  |  |  | 		util.PtrIf(hi), | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 		util.PtrIf(limit), | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 		dir, | 
					
						
							|  |  |  | 	) | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:49:04 +01:00
										 |  |  | 	// TODO: in time, we should think about (dynamically?) preloading | 
					
						
							|  |  |  | 	// the timelines, and any page requests outside of the cached | 
					
						
							|  |  |  | 	// range go straight to the database. otherwise there's peculiarities | 
					
						
							|  |  |  | 	// that may arise due to concurrent new and old range inserts, also | 
					
						
							|  |  |  | 	// requests for old page ranges are almost always going to be one-off. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	// We now reset the lo,hi values to | 
					
						
							|  |  |  | 	// represent the lowest and highest | 
					
						
							|  |  |  | 	// index values of loaded statuses. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// We continually update these while | 
					
						
							|  |  |  | 	// building up statuses to return, for | 
					
						
							|  |  |  | 	// caller to build next / prev page | 
					
						
							|  |  |  | 	// response values. | 
					
						
							|  |  |  | 	lo, hi = "", "" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Preallocate a slice of up-to-limit API models. | 
					
						
							|  |  |  | 	apiStatuses := make([]*apimodel.Status, 0, limit) | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	if len(metas) > 0 { | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		// Before we can do any filtering, we need | 
					
						
							|  |  |  | 		// to load status models for cached entries. | 
					
						
							|  |  |  | 		err := loadStatuses(metas, loadIDs) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, "", "", gtserror.Newf("error loading statuses: %w", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 		// Set initial lo, hi values. | 
					
						
							| 
									
										
										
										
											2025-04-02 19:40:16 +01:00
										 |  |  | 		lo = metas[len(metas)-1].ID | 
					
						
							|  |  |  | 		hi = metas[0].ID | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Update paging parameters used for next database query. | 
					
						
							|  |  |  | 		nextPageParams(nextPg, metas[len(metas)-1].ID, order) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Prepare frontend API models for | 
					
						
							|  |  |  | 		// the cached statuses. For now this | 
					
						
							|  |  |  | 		// also does its own extra filtering. | 
					
						
							|  |  |  | 		apiStatuses = prepareStatuses(ctx, | 
					
						
							|  |  |  | 			metas, | 
					
						
							|  |  |  | 			prepareAPI, | 
					
						
							|  |  |  | 			apiStatuses, | 
					
						
							|  |  |  | 			limit, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 		) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Track all newly loaded status entries | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	// after filtering for insert into cache. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	var justLoaded []*StatusMeta | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	// Check whether loaded enough from cache. | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	if need := limit - len(apiStatuses); need > 0 { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Load a little more than | 
					
						
							|  |  |  | 		// limit to reduce db calls. | 
					
						
							|  |  |  | 		nextPg.Limit += 10 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 16:02:53 +01:00
										 |  |  | 		// Perform maximum of 5 load | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 		// attempts fetching statuses. | 
					
						
							| 
									
										
										
										
											2025-04-07 16:02:53 +01:00
										 |  |  | 		for i := 0; i < 5; i++ { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Load next timeline statuses. | 
					
						
							|  |  |  | 			statuses, err := loadPage(nextPg) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 				return nil, "", "", gtserror.Newf("error loading timeline: %w", err) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// No more statuses from | 
					
						
							|  |  |  | 			// load function = at end. | 
					
						
							|  |  |  | 			if len(statuses) == 0 { | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 19:40:16 +01:00
										 |  |  | 			if hi == "" { | 
					
						
							|  |  |  | 				// Set hi returned paging | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 				// value if not already set. | 
					
						
							| 
									
										
										
										
											2025-04-02 19:40:16 +01:00
										 |  |  | 				hi = statuses[0].ID | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 			// Update nextPg cursor parameter for next database query. | 
					
						
							|  |  |  | 			nextPageParams(nextPg, statuses[len(statuses)-1].ID, order) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Perform any filtering on newly loaded statuses. | 
					
						
							|  |  |  | 			statuses, err = doStatusFilter(statuses, filter) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			if err != nil { | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 				return nil, "", "", gtserror.Newf("error filtering statuses: %w", err) | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// After filtering no more | 
					
						
							|  |  |  | 			// statuses remain, retry. | 
					
						
							|  |  |  | 			if len(statuses) == 0 { | 
					
						
							|  |  |  | 				continue | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Convert to our cache type, | 
					
						
							|  |  |  | 			// these will get inserted into | 
					
						
							|  |  |  | 			// the cache in prepare() below. | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 			metas := toStatusMeta(statuses) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Append to newly loaded for later insert. | 
					
						
							|  |  |  | 			justLoaded = append(justLoaded, metas...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Prepare frontend API models for | 
					
						
							|  |  |  | 			// the loaded statuses. For now this | 
					
						
							|  |  |  | 			// also does its own extra filtering. | 
					
						
							|  |  |  | 			apiStatuses = prepareStatuses(ctx, | 
					
						
							|  |  |  | 				metas, | 
					
						
							|  |  |  | 				prepareAPI, | 
					
						
							|  |  |  | 				apiStatuses, | 
					
						
							|  |  |  | 				limit, | 
					
						
							|  |  |  | 			) | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 			// If we have anything, return | 
					
						
							|  |  |  | 			// here. Even if below limit. | 
					
						
							|  |  |  | 			if len(apiStatuses) > 0 { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 19:40:16 +01:00
										 |  |  | 				// Set returned lo status paging value. | 
					
						
							|  |  |  | 				lo = apiStatuses[len(apiStatuses)-1].ID | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	if order.Ascending() { | 
					
						
							|  |  |  | 		// The caller always expects the statuses | 
					
						
							|  |  |  | 		// to be returned in DESC order, but we | 
					
						
							|  |  |  | 		// build the status slice in paging order. | 
					
						
							|  |  |  | 		// If paging ASC, we need to reverse the | 
					
						
							|  |  |  | 		// returned statuses and paging values. | 
					
						
							|  |  |  | 		slices.Reverse(apiStatuses) | 
					
						
							|  |  |  | 		lo, hi = hi, lo | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 	if len(justLoaded) > 0 { | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 		// Even if not returning them, insert | 
					
						
							|  |  |  | 		// the excess (filtered) into cache. | 
					
						
							|  |  |  | 		t.insert(justLoaded...) | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return apiStatuses, lo, hi, nil | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | // LoadStatusTimeline is a function that may be used to load a timeline | 
					
						
							|  |  |  | // page in a functionally similar way to StatusTimeline{}.Load(), but without | 
					
						
							|  |  |  | // actually having access to a StatusTimeline{}. For example, for timelines that | 
					
						
							|  |  |  | // we want to share code, but without yet implementing a cache for them. Note this | 
					
						
							|  |  |  | // function may be removed in the future when un-needed. | 
					
						
							|  |  |  | func LoadStatusTimeline( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	page *paging.Page, | 
					
						
							|  |  |  | 	loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error), | 
					
						
							|  |  |  | 	loadIDs func(ids []string) (statuses []*gtsmodel.Status, err error), | 
					
						
							|  |  |  | 	filter func(each *gtsmodel.Status) (delete bool, err error), | 
					
						
							|  |  |  | 	prepareAPI func(status *gtsmodel.Status) (apiStatus *apimodel.Status, err error), | 
					
						
							|  |  |  | ) ( | 
					
						
							|  |  |  | 	[]*apimodel.Status, | 
					
						
							|  |  |  | 	string, // lo | 
					
						
							|  |  |  | 	string, // hi | 
					
						
							|  |  |  | 	error, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	switch { | 
					
						
							|  |  |  | 	case page == nil: | 
					
						
							|  |  |  | 		panic("nil page") | 
					
						
							|  |  |  | 	case loadPage == nil: | 
					
						
							|  |  |  | 		panic("nil load page func") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Get paging details. | 
					
						
							|  |  |  | 	lo := page.Min.Value | 
					
						
							|  |  |  | 	hi := page.Max.Value | 
					
						
							|  |  |  | 	limit := page.Limit | 
					
						
							|  |  |  | 	order := page.Order() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Use a copy of current page so | 
					
						
							|  |  |  | 	// we can repeatedly update it. | 
					
						
							|  |  |  | 	nextPg := new(paging.Page) | 
					
						
							|  |  |  | 	*nextPg = *page | 
					
						
							|  |  |  | 	nextPg.Min.Value = lo | 
					
						
							|  |  |  | 	nextPg.Max.Value = hi | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// We now reset the lo,hi values to | 
					
						
							|  |  |  | 	// represent the lowest and highest | 
					
						
							|  |  |  | 	// index values of loaded statuses. | 
					
						
							|  |  |  | 	lo, hi = "", "" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Preallocate a slice of up-to-limit API models. | 
					
						
							|  |  |  | 	apiStatuses := make([]*apimodel.Status, 0, limit) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check whether loaded enough from cache. | 
					
						
							|  |  |  | 	if need := limit - len(apiStatuses); need > 0 { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Load a little more than | 
					
						
							|  |  |  | 		// limit to reduce db calls. | 
					
						
							|  |  |  | 		nextPg.Limit += 10 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 16:02:53 +01:00
										 |  |  | 		// Perform maximum of 5 load | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 		// attempts fetching statuses. | 
					
						
							| 
									
										
										
										
											2025-04-07 16:02:53 +01:00
										 |  |  | 		for i := 0; i < 5; i++ { | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Load next timeline statuses. | 
					
						
							|  |  |  | 			statuses, err := loadPage(nextPg) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, "", "", gtserror.Newf("error loading timeline: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// No more statuses from | 
					
						
							|  |  |  | 			// load function = at end. | 
					
						
							|  |  |  | 			if len(statuses) == 0 { | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if hi == "" { | 
					
						
							|  |  |  | 				// Set hi returned paging | 
					
						
							|  |  |  | 				// value if not already set. | 
					
						
							|  |  |  | 				hi = statuses[0].ID | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Update nextPg cursor parameter for next database query. | 
					
						
							|  |  |  | 			nextPageParams(nextPg, statuses[len(statuses)-1].ID, order) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Perform any filtering on newly loaded statuses. | 
					
						
							|  |  |  | 			statuses, err = doStatusFilter(statuses, filter) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, "", "", gtserror.Newf("error filtering statuses: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// After filtering no more | 
					
						
							|  |  |  | 			// statuses remain, retry. | 
					
						
							|  |  |  | 			if len(statuses) == 0 { | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Convert to our cache type, | 
					
						
							|  |  |  | 			// these will get inserted into | 
					
						
							|  |  |  | 			// the cache in prepare() below. | 
					
						
							|  |  |  | 			metas := toStatusMeta(statuses) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Prepare frontend API models for | 
					
						
							|  |  |  | 			// the loaded statuses. For now this | 
					
						
							|  |  |  | 			// also does its own extra filtering. | 
					
						
							|  |  |  | 			apiStatuses = prepareStatuses(ctx, | 
					
						
							|  |  |  | 				metas, | 
					
						
							|  |  |  | 				prepareAPI, | 
					
						
							|  |  |  | 				apiStatuses, | 
					
						
							|  |  |  | 				limit, | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// If we have anything, return | 
					
						
							|  |  |  | 			// here. Even if below limit. | 
					
						
							|  |  |  | 			if len(apiStatuses) > 0 { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Set returned lo status paging value. | 
					
						
							|  |  |  | 				lo = apiStatuses[len(apiStatuses)-1].ID | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if order.Ascending() { | 
					
						
							|  |  |  | 		// The caller always expects the statuses | 
					
						
							|  |  |  | 		// to be returned in DESC order, but we | 
					
						
							|  |  |  | 		// build the status slice in paging order. | 
					
						
							|  |  |  | 		// If paging ASC, we need to reverse the | 
					
						
							|  |  |  | 		// returned statuses and paging values. | 
					
						
							|  |  |  | 		slices.Reverse(apiStatuses) | 
					
						
							|  |  |  | 		lo, hi = hi, lo | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return apiStatuses, lo, hi, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // InsertOne allows you to insert a single status into the timeline, with optional prepared API model, | 
					
						
							|  |  |  | // the return value indicates whether the passed status has been boosted recently on the timeline. | 
					
						
							|  |  |  | func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.Status) (repeatBoost bool) { | 
					
						
							|  |  |  | 	if status.BoostOfID != "" { | 
					
						
							|  |  |  | 		const repeatBoostDepth = 40 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Check through top $repeatBoostDepth number of timeline items. | 
					
						
							|  |  |  | 		for i, value := range t.cache.RangeUnsafe(structr.Desc) { | 
					
						
							|  |  |  | 			if i >= repeatBoostDepth { | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// If inserted status has already been boosted, or original was posted | 
					
						
							|  |  |  | 			// within last $repeatBoostDepth, we indicate it as a repeated boost. | 
					
						
							|  |  |  | 			if value.ID == status.BoostOfID || value.BoostOfID == status.BoostOfID { | 
					
						
							|  |  |  | 				repeatBoost = true | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Insert into timeline. | 
					
						
							|  |  |  | 	t.insert(&StatusMeta{ | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 		ID:               status.ID, | 
					
						
							|  |  |  | 		AccountID:        status.AccountID, | 
					
						
							|  |  |  | 		BoostOfID:        status.BoostOfID, | 
					
						
							|  |  |  | 		BoostOfAccountID: status.BoostOfAccountID, | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 		repeatBoost:      repeatBoost, | 
					
						
							|  |  |  | 		loaded:           nil, | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 		prepared:         prepared, | 
					
						
							|  |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 19:03:04 +01:00
										 |  |  | // insert will insert given StatusMeta into timeline, and | 
					
						
							|  |  |  | // if beyond t.max will initiate a timeline trim operation. | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | func (t *StatusTimeline) insert(metas ...*StatusMeta) { | 
					
						
							|  |  |  | 	if t.cache.Insert(metas...) > t.max { | 
					
						
							| 
									
										
										
										
											2025-04-07 19:03:04 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 		// If cache reached beyond | 
					
						
							|  |  |  | 		// maximum, perform a trim. | 
					
						
							|  |  |  | 		t.Trim() | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | // RemoveByStatusID removes all cached timeline entries pertaining to | 
					
						
							|  |  |  | // status ID, including those that may be a boost of the given status. | 
					
						
							|  |  |  | func (t *StatusTimeline) RemoveByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(statusIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_ID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_ID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries with IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_ID, keys...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries as boost of IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_BoostOfID, keys...) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // RemoveByAccountID removes all cached timeline entries authored by | 
					
						
							|  |  |  | // account ID, including those that may be boosted by account ID. | 
					
						
							|  |  |  | func (t *StatusTimeline) RemoveByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(accountIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_AccountID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfAccountID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_AccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries as by IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_AccountID, keys...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfAccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries as boosted by IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_BoostOfAccountID, keys...) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // UnprepareByStatusIDs removes cached frontend API models for all cached | 
					
						
							|  |  |  | // timeline entries pertaining to status ID, including boosts of given status. | 
					
						
							|  |  |  | func (t *StatusTimeline) UnprepareByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(statusIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_ID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_ID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.ID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_ID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.BoostOfID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_BoostOfID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // UnprepareByAccountIDs removes cached frontend API models for all cached | 
					
						
							|  |  |  | // timeline entries authored by account ID, including boosts by account ID. | 
					
						
							|  |  |  | func (t *StatusTimeline) UnprepareByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(accountIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_AccountID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfAccountID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_AccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.AccountID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_AccountID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfAccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.BoostOfAccountID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_BoostOfAccountID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | // UnprepareAll removes cached frontend API | 
					
						
							|  |  |  | // models for all cached timeline entries. | 
					
						
							|  |  |  | func (t *StatusTimeline) UnprepareAll() { | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	for _, value := range t.cache.RangeUnsafe(structr.Asc) { | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 		value.prepared = nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:10:53 +01:00
										 |  |  | // Trim will ensure that receiving timeline is less than or | 
					
						
							|  |  |  | // equal in length to the given threshold percentage of the | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // timeline's preconfigured maximum capacity. This will always | 
					
						
							|  |  |  | // trim from the bottom-up to prioritize streamed inserts. | 
					
						
							|  |  |  | func (t *StatusTimeline) Trim() { t.cache.Trim(t.cut, structr.Asc) } | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | // Clear will remove all cached entries from underlying timeline. | 
					
						
							|  |  |  | func (t *StatusTimeline) Clear() { t.cache.Trim(0, structr.Desc) } | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | // prepareStatuses takes a slice of cached (or, freshly loaded!) StatusMeta{} | 
					
						
							|  |  |  | // models, and use given function to return prepared frontend API models. | 
					
						
							|  |  |  | func prepareStatuses( | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	meta []*StatusMeta, | 
					
						
							|  |  |  | 	prepareAPI func(*gtsmodel.Status) (*apimodel.Status, error), | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	apiStatuses []*apimodel.Status, | 
					
						
							|  |  |  | 	limit int, | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | ) []*apimodel.Status { | 
					
						
							| 
									
										
										
										
											2025-03-26 20:43:41 +00:00
										 |  |  | 	switch { //nolint:gocritic | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	case prepareAPI == nil: | 
					
						
							|  |  |  | 		panic("nil prepare fn") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-24 21:34:36 +00:00
										 |  |  | 	// Iterate the given StatusMeta objects for pre-prepared | 
					
						
							|  |  |  | 	// frontend models, otherwise attempting to prepare them. | 
					
						
							|  |  |  | 	for _, meta := range meta { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 		// Check if we have prepared enough | 
					
						
							|  |  |  | 		// API statuses for caller to return. | 
					
						
							|  |  |  | 		if len(apiStatuses) >= limit { | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		if meta.loaded == nil { | 
					
						
							|  |  |  | 			// We failed loading this | 
					
						
							|  |  |  | 			// status, skip preparing. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 		if meta.repeatBoost { | 
					
						
							|  |  |  | 			// This is a repeat boost in | 
					
						
							|  |  |  | 			// short timespan, skip it. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-24 21:34:36 +00:00
										 |  |  | 		if meta.prepared == nil { | 
					
						
							|  |  |  | 			var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Prepare the provided status to frontend. | 
					
						
							|  |  |  | 			meta.prepared, err = prepareAPI(meta.loaded) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				log.Errorf(ctx, "error preparing status %s: %v", meta.loaded.URI, err) | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 		// Append to return slice. | 
					
						
							| 
									
										
										
										
											2025-03-24 21:34:36 +00:00
										 |  |  | 		if meta.prepared != nil { | 
					
						
							|  |  |  | 			apiStatuses = append(apiStatuses, meta.prepared) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	return apiStatuses | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-23 14:18:24 +00:00
										 |  |  | // loadStatuses loads statuses using provided callback | 
					
						
							|  |  |  | // for the statuses in meta slice that aren't loaded. | 
					
						
							|  |  |  | // the amount very much depends on whether meta objects | 
					
						
							|  |  |  | // are yet-to-be-cached (i.e. newly loaded, with status), | 
					
						
							|  |  |  | // or are from the timeline cache (unloaded status). | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | func loadStatuses( | 
					
						
							|  |  |  | 	metas []*StatusMeta, | 
					
						
							|  |  |  | 	loadIDs func([]string) ([]*gtsmodel.Status, error), | 
					
						
							|  |  |  | ) error { | 
					
						
							| 
									
										
										
										
											2025-03-23 14:18:24 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 	// Determine which of our passed status | 
					
						
							|  |  |  | 	// meta objects still need statuses loading. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	toLoadIDs := make([]string, len(metas)) | 
					
						
							|  |  |  | 	loadedMap := make(map[string]*StatusMeta, len(metas)) | 
					
						
							|  |  |  | 	for i, meta := range metas { | 
					
						
							|  |  |  | 		if meta.loaded == nil { | 
					
						
							|  |  |  | 			toLoadIDs[i] = meta.ID | 
					
						
							|  |  |  | 			loadedMap[meta.ID] = meta | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Load statuses with given IDs. | 
					
						
							|  |  |  | 	loaded, err := loadIDs(toLoadIDs) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return gtserror.Newf("error loading statuses: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Update returned StatusMeta objects | 
					
						
							|  |  |  | 	// with newly loaded statuses by IDs. | 
					
						
							|  |  |  | 	for i := range loaded { | 
					
						
							|  |  |  | 		status := loaded[i] | 
					
						
							|  |  |  | 		meta := loadedMap[status.ID] | 
					
						
							|  |  |  | 		meta.loaded = status | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | // toStatusMeta converts a slice of database model statuses | 
					
						
							|  |  |  | // into our cache wrapper type, a slice of []StatusMeta{}. | 
					
						
							|  |  |  | func toStatusMeta(statuses []*gtsmodel.Status) []*StatusMeta { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	return xslices.Gather(nil, statuses, func(s *gtsmodel.Status) *StatusMeta { | 
					
						
							|  |  |  | 		return &StatusMeta{ | 
					
						
							|  |  |  | 			ID:               s.ID, | 
					
						
							|  |  |  | 			AccountID:        s.AccountID, | 
					
						
							|  |  |  | 			BoostOfID:        s.BoostOfID, | 
					
						
							|  |  |  | 			BoostOfAccountID: s.BoostOfAccountID, | 
					
						
							|  |  |  | 			loaded:           s, | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			prepared:         nil, | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | // doStatusFilter performs given filter function on provided statuses, | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | // returning early if an error is returned. returns filtered statuses. | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | func doStatusFilter(statuses []*gtsmodel.Status, filter func(*gtsmodel.Status) (bool, error)) ([]*gtsmodel.Status, error) { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Check for provided | 
					
						
							|  |  |  | 	// filter function. | 
					
						
							|  |  |  | 	if filter == nil { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		return statuses, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Iterate through input statuses. | 
					
						
							|  |  |  | 	for i := 0; i < len(statuses); { | 
					
						
							|  |  |  | 		status := statuses[i] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Pass through filter func. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 		ok, err := filter(status) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if ok { | 
					
						
							|  |  |  | 			// Delete this status from input slice. | 
					
						
							|  |  |  | 			statuses = slices.Delete(statuses, i, i+1) | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Iter. | 
					
						
							|  |  |  | 		i++ | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return statuses, nil | 
					
						
							|  |  |  | } |