| 
									
										
										
										
											2023-05-25 10:37:38 +02: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 ( | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	"context" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"net/url" | 
					
						
							|  |  |  | 	"slices" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/cache" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/cache/timeline" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | 
					
						
							|  |  |  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/filter/usermute" | 
					
						
							| 
									
										
										
										
											2024-02-27 13:22:05 +01:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/paging" | 
					
						
							| 
									
										
										
										
											2023-05-25 10:37:38 +02:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/state" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type Processor struct { | 
					
						
							| 
									
										
										
										
											2023-09-23 17:44:11 +01:00
										 |  |  | 	state     *state.State | 
					
						
							|  |  |  | 	converter *typeutils.Converter | 
					
						
							| 
									
										
										
										
											2024-07-24 13:27:42 +02:00
										 |  |  | 	visFilter *visibility.Filter | 
					
						
							| 
									
										
										
										
											2023-05-25 10:37:38 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-24 13:27:42 +02:00
										 |  |  | func New(state *state.State, converter *typeutils.Converter, visFilter *visibility.Filter) Processor { | 
					
						
							| 
									
										
										
										
											2023-05-25 10:37:38 +02:00
										 |  |  | 	return Processor{ | 
					
						
							| 
									
										
										
										
											2023-09-23 17:44:11 +01:00
										 |  |  | 		state:     state, | 
					
						
							|  |  |  | 		converter: converter, | 
					
						
							| 
									
										
										
										
											2024-07-24 13:27:42 +02:00
										 |  |  | 		visFilter: visFilter, | 
					
						
							| 
									
										
										
										
											2023-05-25 10:37:38 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | func (p *Processor) getStatusTimeline( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	requester *gtsmodel.Account, | 
					
						
							|  |  |  | 	timeline *timeline.StatusTimeline, | 
					
						
							|  |  |  | 	page *paging.Page, | 
					
						
							|  |  |  | 	pgPath string, // timeline page path | 
					
						
							|  |  |  | 	pgQuery url.Values, // timeline query parameters | 
					
						
							|  |  |  | 	filterCtx statusfilter.FilterContext, | 
					
						
							|  |  |  | 	loadPage func(*paging.Page) (statuses []*gtsmodel.Status, err error), | 
					
						
							|  |  |  | 	preFilter func(*gtsmodel.Status) (bool, error), | 
					
						
							|  |  |  | 	postFilter func(*timeline.StatusMeta) bool, | 
					
						
							|  |  |  | ) ( | 
					
						
							|  |  |  | 	*apimodel.PageableResponse, | 
					
						
							|  |  |  | 	gtserror.WithCode, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	var ( | 
					
						
							|  |  |  | 		filters []*gtsmodel.Filter | 
					
						
							|  |  |  | 		mutes   *usermute.CompiledUserMuteList | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if requester != nil { | 
					
						
							|  |  |  | 		var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Fetch all filters relevant for requesting account. | 
					
						
							|  |  |  | 		filters, err = p.state.DB.GetFiltersForAccountID(ctx, | 
					
						
							|  |  |  | 			requester.ID, | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if err != nil && !errors.Is(err, db.ErrNoEntries) { | 
					
						
							|  |  |  | 			err := gtserror.Newf("error getting account filters: %w", err) | 
					
						
							|  |  |  | 			return nil, gtserror.NewErrorInternalError(err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Get a list of all account mutes for requester. | 
					
						
							|  |  |  | 		allMutes, err := p.state.DB.GetAccountMutes(ctx, | 
					
						
							|  |  |  | 			requester.ID, | 
					
						
							|  |  |  | 			nil, // nil page, i.e. all | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if err != nil && !errors.Is(err, db.ErrNoEntries) { | 
					
						
							|  |  |  | 			err := gtserror.Newf("error getting account mutes: %w", err) | 
					
						
							|  |  |  | 			return nil, gtserror.NewErrorInternalError(err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Compile all account mutes to useable form. | 
					
						
							|  |  |  | 		mutes = usermute.NewCompiledUserMuteList(allMutes) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// ... | 
					
						
							|  |  |  | 	statuses, err := timeline.Load(ctx, | 
					
						
							|  |  |  | 		page, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// ... | 
					
						
							|  |  |  | 		loadPage, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// ... | 
					
						
							|  |  |  | 		func(ids []string) ([]*gtsmodel.Status, error) { | 
					
						
							|  |  |  | 			return p.state.DB.GetStatusesByIDs(ctx, ids) | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// ... | 
					
						
							|  |  |  | 		preFilter, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// ... | 
					
						
							|  |  |  | 		postFilter, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// ... | 
					
						
							|  |  |  | 		func(status *gtsmodel.Status) (*apimodel.Status, error) { | 
					
						
							|  |  |  | 			apiStatus, err := p.converter.StatusToAPIStatus(ctx, | 
					
						
							|  |  |  | 				status, | 
					
						
							|  |  |  | 				requester, | 
					
						
							|  |  |  | 				filterCtx, | 
					
						
							|  |  |  | 				filters, | 
					
						
							|  |  |  | 				mutes, | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 			if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return apiStatus, nil | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		panic(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (p *Processor) getTimeline( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	requester *gtsmodel.Account, | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	timeline *timeline.StatusTimeline, | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	page *paging.Page, | 
					
						
							|  |  |  | 	pgPath string, // timeline page path | 
					
						
							|  |  |  | 	pgQuery url.Values, // timeline query parameters | 
					
						
							|  |  |  | 	filterCtx statusfilter.FilterContext, | 
					
						
							|  |  |  | 	load func(*paging.Page) (statuses []*gtsmodel.Status, next *paging.Page, err error), // timeline cache load function | 
					
						
							|  |  |  | 	filter func(*gtsmodel.Status) bool, // per-request filtering function, done AFTER timeline caching | 
					
						
							|  |  |  | ) ( | 
					
						
							|  |  |  | 	*apimodel.PageableResponse, | 
					
						
							|  |  |  | 	gtserror.WithCode, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	// Load timeline with cache / loader funcs. | 
					
						
							|  |  |  | 	statuses, errWithCode := p.loadTimeline(ctx, | 
					
						
							|  |  |  | 		timeline, | 
					
						
							|  |  |  | 		page, | 
					
						
							|  |  |  | 		load, | 
					
						
							|  |  |  | 		filter, | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 	if errWithCode != nil { | 
					
						
							|  |  |  | 		return nil, errWithCode | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(statuses) == 0 { | 
					
						
							|  |  |  | 		// Check for an empty timeline rsp. | 
					
						
							|  |  |  | 		return paging.EmptyResponse(), nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Get the lowest and highest | 
					
						
							|  |  |  | 	// ID values, used for paging. | 
					
						
							|  |  |  | 	lo := statuses[len(statuses)-1].ID | 
					
						
							|  |  |  | 	hi := statuses[0].ID | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var ( | 
					
						
							|  |  |  | 		filters []*gtsmodel.Filter | 
					
						
							|  |  |  | 		mutes   *usermute.CompiledUserMuteList | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if requester != nil { | 
					
						
							|  |  |  | 		var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Fetch all filters relevant for requesting account. | 
					
						
							|  |  |  | 		filters, err = p.state.DB.GetFiltersForAccountID(ctx, | 
					
						
							|  |  |  | 			requester.ID, | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if err != nil && !errors.Is(err, db.ErrNoEntries) { | 
					
						
							|  |  |  | 			err := gtserror.Newf("error getting account filters: %w", err) | 
					
						
							|  |  |  | 			return nil, gtserror.NewErrorInternalError(err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Get a list of all account mutes for requester. | 
					
						
							|  |  |  | 		allMutes, err := p.state.DB.GetAccountMutes(ctx, | 
					
						
							|  |  |  | 			requester.ID, | 
					
						
							|  |  |  | 			nil, // nil page, i.e. all | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if err != nil && !errors.Is(err, db.ErrNoEntries) { | 
					
						
							|  |  |  | 			err := gtserror.Newf("error getting account mutes: %w", err) | 
					
						
							|  |  |  | 			return nil, gtserror.NewErrorInternalError(err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Compile all account mutes to useable form. | 
					
						
							|  |  |  | 		mutes = usermute.NewCompiledUserMuteList(allMutes) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// NOTE: | 
					
						
							|  |  |  | 	// Right now this is not ideal, as we perform mute and | 
					
						
							|  |  |  | 	// status filtering *after* the above load loop, so we | 
					
						
							|  |  |  | 	// could end up with no statuses still AFTER all loading. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// In a PR coming *soon* we will move the filtering and | 
					
						
							|  |  |  | 	// status muting into separate module similar to the visibility | 
					
						
							|  |  |  | 	// filtering and caching which should move it to the above | 
					
						
							|  |  |  | 	// load loop and provided function. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// API response requires them in interface{} form. | 
					
						
							|  |  |  | 	items := make([]interface{}, 0, len(statuses)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, status := range statuses { | 
					
						
							|  |  |  | 		// Convert internal status model to frontend model. | 
					
						
							|  |  |  | 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, | 
					
						
							|  |  |  | 			status, | 
					
						
							|  |  |  | 			requester, | 
					
						
							|  |  |  | 			filterCtx, | 
					
						
							|  |  |  | 			filters, | 
					
						
							|  |  |  | 			mutes, | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { | 
					
						
							|  |  |  | 			log.Errorf(ctx, "error converting status: %v", err) | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if apiStatus != nil { | 
					
						
							|  |  |  | 			// Append status to return slice. | 
					
						
							|  |  |  | 			items = append(items, apiStatus) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Package converted API statuses as pageable response. | 
					
						
							|  |  |  | 	return paging.PackageResponse(paging.ResponseParams{ | 
					
						
							|  |  |  | 		Items: items, | 
					
						
							|  |  |  | 		Next:  page.Next(lo, hi), | 
					
						
							|  |  |  | 		Prev:  page.Prev(lo, hi), | 
					
						
							|  |  |  | 		Path:  pgPath, | 
					
						
							|  |  |  | 		Query: pgQuery, | 
					
						
							|  |  |  | 	}), nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (p *Processor) loadTimeline( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	timeline *cache.TimelineCache[*gtsmodel.Status], | 
					
						
							|  |  |  | 	page *paging.Page, | 
					
						
							|  |  |  | 	load func(*paging.Page) (statuses []*gtsmodel.Status, next *paging.Page, err error), | 
					
						
							|  |  |  | 	filter func(*gtsmodel.Status) bool, | 
					
						
							|  |  |  | ) ( | 
					
						
							|  |  |  | 	[]*gtsmodel.Status, | 
					
						
							|  |  |  | 	gtserror.WithCode, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	if load == nil { | 
					
						
							|  |  |  | 		// nil check outside | 
					
						
							|  |  |  | 		// below main loop. | 
					
						
							|  |  |  | 		panic("nil func") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if page == nil { | 
					
						
							|  |  |  | 		const text = "timeline must be paged" | 
					
						
							|  |  |  | 		return nil, gtserror.NewErrorBadRequest( | 
					
						
							|  |  |  | 			errors.New(text), | 
					
						
							|  |  |  | 			text, | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Try load statuses from cache. | 
					
						
							|  |  |  | 	statuses := timeline.Select(page) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Filter statuses using provided function. | 
					
						
							|  |  |  | 	statuses = slices.DeleteFunc(statuses, filter) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check if more statuses need to be loaded. | 
					
						
							|  |  |  | 	if limit := page.Limit; len(statuses) < limit { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Set first page | 
					
						
							|  |  |  | 		// query to load. | 
					
						
							|  |  |  | 		nextPg := page | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for i := 0; i < 5; i++ { | 
					
						
							|  |  |  | 			var err error | 
					
						
							|  |  |  | 			var next []*gtsmodel.Status | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Load next timeline statuses. | 
					
						
							|  |  |  | 			next, nextPg, err = load(nextPg) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				err := gtserror.Newf("error loading timeline: %w", err) | 
					
						
							|  |  |  | 				return nil, gtserror.NewErrorInternalError(err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// An empty next page means no more. | 
					
						
							|  |  |  | 			if len(next) == 0 && nextPg == nil { | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Cache loaded statuses. | 
					
						
							|  |  |  | 			timeline.Insert(next...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Filter statuses using provided function, | 
					
						
							|  |  |  | 			// this must be done AFTER cache insert but | 
					
						
							|  |  |  | 			// BEFORE adding to slice, as this is used | 
					
						
							|  |  |  | 			// for request-specific timeline filtering, | 
					
						
							|  |  |  | 			// as opposed to filtering for entire cache. | 
					
						
							|  |  |  | 			next = slices.DeleteFunc(next, filter) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Append loaded statuses to return. | 
					
						
							|  |  |  | 			statuses = append(statuses, next...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if len(statuses) >= limit { | 
					
						
							|  |  |  | 				// We loaded all the statuses | 
					
						
							|  |  |  | 				// that were requested of us! | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return statuses, nil | 
					
						
							|  |  |  | } |