mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 02:52:26 -05:00 
			
		
		
		
	implement new timeline code into more areas of codebase, pull in latest go-mangler, go-mutexes, go-structr
This commit is contained in:
		
					parent
					
						
							
								bc46cd72b6
							
						
					
				
			
			
				commit
				
					
						4803ae6bad
					
				
			
		
					 5 changed files with 82 additions and 86 deletions
				
			
		
							
								
								
									
										1
									
								
								internal/cache/cache.go
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								internal/cache/cache.go
									
										
									
									
										vendored
									
									
								
							|  | @ -216,7 +216,6 @@ func (c *Caches) Sweep(threshold float64) { | ||||||
| 	c.Timelines.Home.Trim(threshold) | 	c.Timelines.Home.Trim(threshold) | ||||||
| 	c.Timelines.List.Trim(threshold) | 	c.Timelines.List.Trim(threshold) | ||||||
| 	c.Timelines.Public.Trim(threshold) | 	c.Timelines.Public.Trim(threshold) | ||||||
| 	c.Timelines.Local.Trim(threshold) |  | ||||||
| 	c.Visibility.Trim(threshold) | 	c.Visibility.Trim(threshold) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								internal/cache/timeline.go
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								internal/cache/timeline.go
									
										
									
									
										vendored
									
									
								
							|  | @ -32,9 +32,6 @@ type TimelineCaches struct { | ||||||
| 
 | 
 | ||||||
| 	// Public ... | 	// Public ... | ||||||
| 	Public timeline.StatusTimeline | 	Public timeline.StatusTimeline | ||||||
| 
 |  | ||||||
| 	// Local ... |  | ||||||
| 	Local timeline.StatusTimeline |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *Caches) initHomeTimelines() { | func (c *Caches) initHomeTimelines() { | ||||||
|  | @ -60,11 +57,3 @@ func (c *Caches) initPublicTimeline() { | ||||||
| 
 | 
 | ||||||
| 	c.Timelines.Public.Init(cap) | 	c.Timelines.Public.Init(cap) | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func (c *Caches) initLocalTimeline() { |  | ||||||
| 	cap := 1000 |  | ||||||
| 
 |  | ||||||
| 	log.Infof(nil, "cache size = %d", cap) |  | ||||||
| 
 |  | ||||||
| 	c.Timelines.Local.Init(cap) |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										54
									
								
								internal/cache/timeline/status.go
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										54
									
								
								internal/cache/timeline/status.go
									
										
									
									
										vendored
									
									
								
							|  | @ -159,7 +159,12 @@ func (t *StatusTimelines) Delete(key string) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Insert ... | // InsertInto allows you to bulk insert many statuses into timeline mapped by key. | ||||||
|  | func (t *StatusTimelines) InsertInto(key string, statuses ...*gtsmodel.Status) { | ||||||
|  | 	t.MustGet(key).Insert(statuses...) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Insert allows you to bulk insert many statuses into *all* mapped timelines. | ||||||
| func (t *StatusTimelines) Insert(statuses ...*gtsmodel.Status) { | func (t *StatusTimelines) Insert(statuses ...*gtsmodel.Status) { | ||||||
| 	meta := toStatusMeta(statuses) | 	meta := toStatusMeta(statuses) | ||||||
| 	if p := t.ptr.Load(); p != nil { | 	if p := t.ptr.Load(); p != nil { | ||||||
|  | @ -169,11 +174,6 @@ func (t *StatusTimelines) Insert(statuses ...*gtsmodel.Status) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // InsertInto ... |  | ||||||
| func (t *StatusTimelines) InsertInto(key string, statuses ...*gtsmodel.Status) { |  | ||||||
| 	t.MustGet(key).Insert(statuses...) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // RemoveByStatusIDs ... | // RemoveByStatusIDs ... | ||||||
| func (t *StatusTimelines) RemoveByStatusIDs(statusIDs ...string) { | func (t *StatusTimelines) RemoveByStatusIDs(statusIDs ...string) { | ||||||
| 	if p := t.ptr.Load(); p != nil { | 	if p := t.ptr.Load(); p != nil { | ||||||
|  | @ -210,6 +210,15 @@ func (t *StatusTimelines) UnprepareByAccountIDs(accountIDs ...string) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // UnprepareAll ... | ||||||
|  | func (t *StatusTimelines) UnprepareAll() { | ||||||
|  | 	if p := t.ptr.Load(); p != nil { | ||||||
|  | 		for _, tt := range *p { | ||||||
|  | 			tt.UnprepareAll() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Trim ... | // Trim ... | ||||||
| func (t *StatusTimelines) Trim(threshold float64) { | func (t *StatusTimelines) Trim(threshold float64) { | ||||||
| 	if p := t.ptr.Load(); p != nil { | 	if p := t.ptr.Load(); p != nil { | ||||||
|  | @ -470,7 +479,20 @@ func (t *StatusTimeline) Load( | ||||||
| 	return apiStatuses, lo, hi, nil | 	return apiStatuses, lo, hi, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Insert ... | // InsertOne allows you to insert a single status into the timeline, with optional prepared API model. | ||||||
|  | func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.Status) { | ||||||
|  | 	t.cache.Insert(&StatusMeta{ | ||||||
|  | 		ID:               status.ID, | ||||||
|  | 		AccountID:        status.AccountID, | ||||||
|  | 		BoostOfID:        status.BoostOfID, | ||||||
|  | 		BoostOfAccountID: status.BoostOfAccountID, | ||||||
|  | 		Local:            *status.Local, | ||||||
|  | 		loaded:           status, | ||||||
|  | 		prepared:         prepared, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Insert allows you to bulk insert many statuses into the timeline. | ||||||
| func (t *StatusTimeline) Insert(statuses ...*gtsmodel.Status) { | func (t *StatusTimeline) Insert(statuses ...*gtsmodel.Status) { | ||||||
| 	t.cache.Insert(toStatusMeta(statuses)...) | 	t.cache.Insert(toStatusMeta(statuses)...) | ||||||
| } | } | ||||||
|  | @ -595,6 +617,14 @@ func (t *StatusTimeline) UnprepareByAccountIDs(accountIDs ...string) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // UnprepareAll removes cached frontend API | ||||||
|  | // models for all cached timeline entries. | ||||||
|  | func (t *StatusTimeline) UnprepareAll() { | ||||||
|  | 	for value := range t.cache.RangeUnsafe(structr.Asc) { | ||||||
|  | 		value.prepared = nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Trim ... | // Trim ... | ||||||
| func (t *StatusTimeline) Trim(threshold float64) { | func (t *StatusTimeline) Trim(threshold float64) { | ||||||
| 
 | 
 | ||||||
|  | @ -690,7 +720,8 @@ func loadStatuses( | ||||||
| 	metas []*StatusMeta, | 	metas []*StatusMeta, | ||||||
| 	loadIDs func([]string) ([]*gtsmodel.Status, error), | 	loadIDs func([]string) ([]*gtsmodel.Status, error), | ||||||
| ) error { | ) error { | ||||||
| 	// ... | 	// Determine which of our passed status | ||||||
|  | 	// meta objects still need statuses loading. | ||||||
| 	toLoadIDs := make([]string, len(metas)) | 	toLoadIDs := make([]string, len(metas)) | ||||||
| 	loadedMap := make(map[string]*StatusMeta, len(metas)) | 	loadedMap := make(map[string]*StatusMeta, len(metas)) | ||||||
| 	for i, meta := range metas { | 	for i, meta := range metas { | ||||||
|  | @ -733,7 +764,8 @@ func toStatusMeta(statuses []*gtsmodel.Status) []*StatusMeta { | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ... | // doStatusPreFilter performs given filter function on provided statuses, | ||||||
|  | // returning early if an error is returned. returns filtered statuses. | ||||||
| func doStatusPreFilter(statuses []*gtsmodel.Status, filter func(*gtsmodel.Status) (bool, error)) ([]*gtsmodel.Status, error) { | func doStatusPreFilter(statuses []*gtsmodel.Status, filter func(*gtsmodel.Status) (bool, error)) ([]*gtsmodel.Status, error) { | ||||||
| 
 | 
 | ||||||
| 	// Check for provided | 	// Check for provided | ||||||
|  | @ -765,7 +797,9 @@ func doStatusPreFilter(statuses []*gtsmodel.Status, filter func(*gtsmodel.Status | ||||||
| 	return statuses, nil | 	return statuses, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ... | // doStatusPostFilter performs given filter function on provided status meta, | ||||||
|  | // expecting that embedded status is already loaded, returning filtered status | ||||||
|  | // meta, as well as those *filtered out*. returns early if error is returned. | ||||||
| func doStatusPostFilter(metas []*StatusMeta, filter func(*gtsmodel.Status) (bool, error)) ([]*StatusMeta, []*StatusMeta, error) { | func doStatusPostFilter(metas []*StatusMeta, filter func(*gtsmodel.Status) (bool, error)) ([]*StatusMeta, []*StatusMeta, error) { | ||||||
| 
 | 
 | ||||||
| 	// Check for provided | 	// Check for provided | ||||||
|  |  | ||||||
|  | @ -23,7 +23,6 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/paging" | 	"github.com/superseriousbusiness/gotosocial/internal/paging" | ||||||
|  | @ -35,10 +34,6 @@ type HomeTestSuite struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *HomeTestSuite) TearDownTest() { | func (suite *HomeTestSuite) TearDownTest() { | ||||||
| 	if err := suite.state.Timelines.Home.Stop(); err != nil { |  | ||||||
| 		suite.FailNow(err.Error()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	suite.TimelineStandardTestSuite.TearDownTest() | 	suite.TimelineStandardTestSuite.TearDownTest() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -47,7 +42,6 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() { | ||||||
| 	var ( | 	var ( | ||||||
| 		ctx                 = context.Background() | 		ctx                 = context.Background() | ||||||
| 		requester           = suite.testAccounts["local_account_1"] | 		requester           = suite.testAccounts["local_account_1"] | ||||||
| 		authed              = &apiutil.Auth{Account: requester} |  | ||||||
| 		maxID               = "" | 		maxID               = "" | ||||||
| 		sinceID             = "" | 		sinceID             = "" | ||||||
| 		minID               = "01F8MHAAY43M6RJ473VQFCVH36" // 1 before filteredStatus | 		minID               = "01F8MHAAY43M6RJ473VQFCVH36" // 1 before filteredStatus | ||||||
|  | @ -98,10 +92,9 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() { | ||||||
| 	if !filteredStatusFound { | 	if !filteredStatusFound { | ||||||
| 		suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline") | 		suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline") | ||||||
| 	} | 	} | ||||||
| 	// Prune the timeline to drop cached prepared statuses, a side effect of this precondition check. | 
 | ||||||
| 	if _, err := suite.state.Timelines.Home.Prune(ctx, requester.ID, 0, 0); err != nil { | 	// Clear the timeline to drop all cached statuses. | ||||||
| 		suite.FailNow(err.Error()) | 	suite.state.Caches.Timelines.Home.Clear(requester.ID) | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// Create a filter to hide one status on the timeline. | 	// Create a filter to hide one status on the timeline. | ||||||
| 	if err := suite.db.PutFilter(ctx, filter); err != nil { | 	if err := suite.db.PutFilter(ctx, filter); err != nil { | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 
 | 
 | ||||||
|  | 	timeline2 "github.com/superseriousbusiness/gotosocial/internal/cache/timeline" | ||||||
| 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/filter/usermute" | 	"github.com/superseriousbusiness/gotosocial/internal/filter/usermute" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | ||||||
|  | @ -28,7 +29,6 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/stream" | 	"github.com/superseriousbusiness/gotosocial/internal/stream" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/timeline" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -161,21 +161,16 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( | ||||||
| 
 | 
 | ||||||
| 			// Add status to home timeline for owner of | 			// Add status to home timeline for owner of | ||||||
| 			// this follow (origin account), if applicable. | 			// this follow (origin account), if applicable. | ||||||
| 			homeTimelined, err = s.timelineStatus(ctx, | 			if homeTimelined := s.timelineStatus(ctx, | ||||||
| 				s.State.Timelines.Home.IngestOne, | 				s.State.Caches.Timelines.Home.MustGet(follow.AccountID), | ||||||
| 				follow.AccountID, // home timelines are keyed by account ID |  | ||||||
| 				follow.Account, | 				follow.Account, | ||||||
| 				status, | 				status, | ||||||
| 				stream.TimelineHome, | 				stream.TimelineHome, | ||||||
|  | 				statusfilter.FilterContextHome, | ||||||
| 				filters, | 				filters, | ||||||
| 				mutes, | 				mutes, | ||||||
| 			) | 			); homeTimelined { | ||||||
| 			if err != nil { |  | ||||||
| 				log.Errorf(ctx, "error home timelining status: %v", err) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			if homeTimelined { |  | ||||||
| 				// If hometimelined, add to list of returned account IDs. | 				// If hometimelined, add to list of returned account IDs. | ||||||
| 				homeTimelinedAccountIDs = append(homeTimelinedAccountIDs, follow.AccountID) | 				homeTimelinedAccountIDs = append(homeTimelinedAccountIDs, follow.AccountID) | ||||||
| 			} | 			} | ||||||
|  | @ -261,21 +256,16 @@ func (s *Surface) listTimelineStatusForFollow( | ||||||
| 		exclusive = exclusive || *list.Exclusive | 		exclusive = exclusive || *list.Exclusive | ||||||
| 
 | 
 | ||||||
| 		// At this point we are certain this status | 		// At this point we are certain this status | ||||||
| 		// should be included in the timeline of the | 		// should be included in timeline of this list. | ||||||
| 		// list that this list entry belongs to. | 		listTimelined := s.timelineStatus(ctx, | ||||||
| 		listTimelined, err := s.timelineStatus(ctx, | 			s.State.Caches.Timelines.List.MustGet(list.ID), | ||||||
| 			s.State.Timelines.List.IngestOne, |  | ||||||
| 			list.ID, // list timelines are keyed by list ID |  | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status, | 			status, | ||||||
| 			stream.TimelineList+":"+list.ID, // key streamType to this specific list | 			stream.TimelineList+":"+list.ID, // key streamType to this specific list | ||||||
|  | 			statusfilter.FilterContextHome, | ||||||
| 			filters, | 			filters, | ||||||
| 			mutes, | 			mutes, | ||||||
| 		) | 		) | ||||||
| 		if err != nil { |  | ||||||
| 			log.Errorf(ctx, "error adding status to list timeline: %v", err) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		// Update flag based on if timelined. | 		// Update flag based on if timelined. | ||||||
| 		timelined = timelined || listTimelined | 		timelined = timelined || listTimelined | ||||||
|  | @ -371,48 +361,46 @@ func (s *Surface) listEligible( | ||||||
| // | // | ||||||
| // If the status was inserted into the timeline, true will be returned | // If the status was inserted into the timeline, true will be returned | ||||||
| // + it will also be streamed to the user using the given streamType. | // + it will also be streamed to the user using the given streamType. | ||||||
|  | 
 | ||||||
|  | // timelineStatus ... | ||||||
| func (s *Surface) timelineStatus( | func (s *Surface) timelineStatus( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	ingest func(context.Context, string, timeline.Timelineable) (bool, error), | 	timeline *timeline2.StatusTimeline, | ||||||
| 	timelineID string, |  | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	streamType string, | 	streamType string, | ||||||
|  | 	filterCtx statusfilter.FilterContext, | ||||||
| 	filters []*gtsmodel.Filter, | 	filters []*gtsmodel.Filter, | ||||||
| 	mutes *usermute.CompiledUserMuteList, | 	mutes *usermute.CompiledUserMuteList, | ||||||
| ) (bool, error) { | ) bool { | ||||||
| 
 | 
 | ||||||
| 	// Ingest status into given timeline using provided function. | 	// Attempt to convert status to frontend API representation, | ||||||
| 	if inserted, err := ingest(ctx, timelineID, status); err != nil && | 	// this will check whether status is filtered / muted. | ||||||
| 		!errors.Is(err, statusfilter.ErrHideStatus) { | 	apiModel, err := s.Converter.StatusToAPIStatus(ctx, | ||||||
| 		err := gtserror.Newf("error ingesting status %s: %w", status.ID, err) |  | ||||||
| 		return false, err |  | ||||||
| 	} else if !inserted { |  | ||||||
| 		// Nothing more to do. |  | ||||||
| 		return false, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Convert updated database model to frontend model. |  | ||||||
| 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, |  | ||||||
| 		status, | 		status, | ||||||
| 		account, | 		account, | ||||||
| 		statusfilter.FilterContextHome, | 		filterCtx, | ||||||
| 		filters, | 		filters, | ||||||
| 		mutes, | 		mutes, | ||||||
| 	) | 	) | ||||||
| 	if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { | 	if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { | ||||||
| 		err := gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | 		log.Error(ctx, "error converting status %s to frontend: %v", status.URI, err) | ||||||
| 		return true, err |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if apiStatus != nil { | 	// Insert status to timeline cache regardless of | ||||||
| 		// The status was inserted so stream it to the user. | 	// if API model was successfully prepared or not. | ||||||
| 		s.Stream.Update(ctx, account, apiStatus, streamType) | 	timeline.InsertOne(status, apiModel) | ||||||
| 		return true, nil | 
 | ||||||
|  | 	if apiModel != nil { | ||||||
|  | 		// Only send the status to user's stream if not | ||||||
|  | 		// filtered / muted, i.e. successfully prepared model. | ||||||
|  | 		s.Stream.Update(ctx, account, apiModel, streamType) | ||||||
|  | 		return true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Status was hidden. | 	// Status was | ||||||
| 	return false, nil | 	// filtered / muted. | ||||||
|  | 	return false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // timelineAndNotifyStatusForTagFollowers inserts the status into the | // timelineAndNotifyStatusForTagFollowers inserts the status into the | ||||||
|  | @ -443,23 +431,16 @@ func (s *Surface) timelineAndNotifyStatusForTagFollowers( | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if _, err := s.timelineStatus(ctx, | 		_ = s.timelineStatus(ctx, | ||||||
| 			s.State.Timelines.Home.IngestOne, | 			s.State.Caches.Timelines.Home.MustGet(tagFollowerAccount.ID), | ||||||
| 			tagFollowerAccount.ID, // home timelines are keyed by account ID |  | ||||||
| 			tagFollowerAccount, | 			tagFollowerAccount, | ||||||
| 			status, | 			status, | ||||||
| 			stream.TimelineHome, | 			stream.TimelineHome, | ||||||
|  | 			statusfilter.FilterContextHome, | ||||||
| 			filters, | 			filters, | ||||||
| 			mutes, | 			mutes, | ||||||
| 		); err != nil { |  | ||||||
| 			errs.Appendf( |  | ||||||
| 				"error inserting status %s into home timeline for account %s: %w", |  | ||||||
| 				status.ID, |  | ||||||
| 				tagFollowerAccount.ID, |  | ||||||
| 				err, |  | ||||||
| 		) | 		) | ||||||
| 	} | 	} | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	return errs.Combine() | 	return errs.Combine() | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue