mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 12:42:25 -05:00 
			
		
		
		
	[chore] Tidy up status deletion, remove from cache too (#845)
* add func for deleting status from db + cache * move deletes entirely back to processor and also only do a delete if the requesting account owns the item being deleted * tidy up unboost processing * delete status more efficiently * fix wrong account id on remote test attachments * fix federator test
This commit is contained in:
		
					parent
					
						
							
								8c20626c51
							
						
					
				
			
			
				commit
				
					
						4cf76a2bfc
					
				
			
		
					 12 changed files with 113 additions and 94 deletions
				
			
		
							
								
								
									
										5
									
								
								internal/cache/status.go
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								internal/cache/status.go
									
										
									
									
										vendored
									
									
								
							|  | @ -85,6 +85,11 @@ func (c *StatusCache) Put(status *gtsmodel.Status) { | ||||||
| 	c.cache.Set(status.ID, copyStatus(status)) | 	c.cache.Set(status.ID, copyStatus(status)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Invalidate invalidates one status from the cache using the ID of the status as key. | ||||||
|  | func (c *StatusCache) Invalidate(statusID string) { | ||||||
|  | 	c.cache.Invalidate(statusID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // copyStatus performs a surface-level copy of status, only keeping attached IDs intact, not the objects. | // copyStatus performs a surface-level copy of status, only keeping attached IDs intact, not the objects. | ||||||
| // due to all the data being copied being 99% primitive types or strings (which are immutable and passed by ptr) | // due to all the data being copied being 99% primitive types or strings (which are immutable and passed by ptr) | ||||||
| // this should be a relatively cheap process | // this should be a relatively cheap process | ||||||
|  |  | ||||||
|  | @ -227,6 +227,42 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status) (* | ||||||
| 	return status, err | 	return status, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) db.Error { | ||||||
|  | 	err := s.conn.RunInTx(ctx, func(tx bun.Tx) error { | ||||||
|  | 		// delete links between this status and any emojis it uses | ||||||
|  | 		if _, err := tx. | ||||||
|  | 			NewDelete(). | ||||||
|  | 			Model(>smodel.StatusToEmoji{}). | ||||||
|  | 			Where("status_id = ?", bun.Ident(id)). | ||||||
|  | 			Exec(ctx); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// delete links between this status and any tags it uses | ||||||
|  | 		if _, err := tx. | ||||||
|  | 			NewDelete(). | ||||||
|  | 			Model(>smodel.StatusToTag{}). | ||||||
|  | 			Where("status_id = ?", bun.Ident(id)). | ||||||
|  | 			Exec(ctx); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// delete the status itself | ||||||
|  | 		if _, err := tx. | ||||||
|  | 			NewDelete(). | ||||||
|  | 			Model(>smodel.Status{ID: id}). | ||||||
|  | 			WherePK(). | ||||||
|  | 			Exec(ctx); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		s.cache.Invalidate(id) | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	return s.conn.ProcessError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (s *statusDB) GetStatusParents(ctx context.Context, status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) { | func (s *statusDB) GetStatusParents(ctx context.Context, status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) { | ||||||
| 	parents := []*gtsmodel.Status{} | 	parents := []*gtsmodel.Status{} | ||||||
| 	s.statusParent(ctx, status, &parents, onlyDirect) | 	s.statusParent(ctx, status, &parents, onlyDirect) | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ import ( | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type StatusTestSuite struct { | type StatusTestSuite struct { | ||||||
|  | @ -132,6 +133,15 @@ func (suite *StatusTestSuite) TestGetStatusChildren() { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *StatusTestSuite) TestDeleteStatus() { | ||||||
|  | 	targetStatus := suite.testStatuses["admin_account_status_1"] | ||||||
|  | 	err := suite.db.DeleteStatusByID(context.Background(), targetStatus.ID) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	_, err = suite.db.GetStatusByID(context.Background(), targetStatus.ID) | ||||||
|  | 	suite.ErrorIs(err, db.ErrNoEntries) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestStatusTestSuite(t *testing.T) { | func TestStatusTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, new(StatusTestSuite)) | 	suite.Run(t, new(StatusTestSuite)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -41,6 +41,9 @@ type Status interface { | ||||||
| 	// UpdateStatus updates one status in the database and returns it to the caller. | 	// UpdateStatus updates one status in the database and returns it to the caller. | ||||||
| 	UpdateStatus(ctx context.Context, status *gtsmodel.Status) (*gtsmodel.Status, Error) | 	UpdateStatus(ctx context.Context, status *gtsmodel.Status) (*gtsmodel.Status, Error) | ||||||
| 
 | 
 | ||||||
|  | 	// DeleteStatusByID deletes one status from the database. | ||||||
|  | 	DeleteStatusByID(ctx context.Context, id string) Error | ||||||
|  | 
 | ||||||
| 	// CountStatusReplies returns the amount of replies recorded for a status, or an error if something goes wrong | 	// CountStatusReplies returns the amount of replies recorded for a status, or an error if something goes wrong | ||||||
| 	CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, Error) | 	CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, Error) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,12 +20,10 @@ package federatingdb | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" |  | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"codeberg.org/gruf/go-kv" | 	"codeberg.org/gruf/go-kv" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/messages" | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
| ) | ) | ||||||
|  | @ -38,12 +36,11 @@ import ( | ||||||
| // The library makes this call only after acquiring a lock first. | // The library makes this call only after acquiring a lock first. | ||||||
| func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { | func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { | ||||||
| 	l := log.WithFields(kv.Fields{ | 	l := log.WithFields(kv.Fields{ | ||||||
| 
 |  | ||||||
| 		{"id", id}, | 		{"id", id}, | ||||||
| 	}...) | 	}...) | ||||||
| 	l.Debug("entering Delete") | 	l.Debug("entering Delete") | ||||||
| 
 | 
 | ||||||
| 	receivingAccount, _ := extractFromCtx(ctx) | 	receivingAccount, requestingAccount := extractFromCtx(ctx) | ||||||
| 	if receivingAccount == nil { | 	if receivingAccount == nil { | ||||||
| 		// If the receiving account wasn't set on the context, that means this request didn't pass | 		// If the receiving account wasn't set on the context, that means this request didn't pass | ||||||
| 		// through the API, but came from inside GtS as the result of another activity on this instance. That being so, | 		// through the API, but came from inside GtS as the result of another activity on this instance. That being so, | ||||||
|  | @ -53,13 +50,8 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { | ||||||
| 
 | 
 | ||||||
| 	// in a delete we only get the URI, we can't know if we have a status or a profile or something else, | 	// in a delete we only get the URI, we can't know if we have a status or a profile or something else, | ||||||
| 	// so we have to try a few different things... | 	// so we have to try a few different things... | ||||||
| 	s, err := f.db.GetStatusByURI(ctx, id.String()) | 	if s, err := f.db.GetStatusByURI(ctx, id.String()); err == nil && requestingAccount.ID == s.AccountID { | ||||||
| 	if err == nil { | 		l.Debugf("uri is for STATUS with id: %s", s.ID) | ||||||
| 		// it's a status |  | ||||||
| 		l.Debugf("uri is for status with id: %s", s.ID) |  | ||||||
| 		if err := f.db.DeleteByID(ctx, s.ID, >smodel.Status{}); err != nil { |  | ||||||
| 			return fmt.Errorf("DELETE: err deleting status: %s", err) |  | ||||||
| 		} |  | ||||||
| 		f.fedWorker.Queue(messages.FromFederator{ | 		f.fedWorker.Queue(messages.FromFederator{ | ||||||
| 			APObjectType:     ap.ObjectNote, | 			APObjectType:     ap.ObjectNote, | ||||||
| 			APActivityType:   ap.ActivityDelete, | 			APActivityType:   ap.ActivityDelete, | ||||||
|  | @ -68,10 +60,8 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	a, err := f.db.GetAccountByURI(ctx, id.String()) | 	if a, err := f.db.GetAccountByURI(ctx, id.String()); err == nil && requestingAccount.ID == a.ID { | ||||||
| 	if err == nil { | 		l.Debugf("uri is for ACCOUNT with id %s", a.ID) | ||||||
| 		// it's an account |  | ||||||
| 		l.Debugf("uri is for an account with id %s, passing delete message to the processor", a.ID) |  | ||||||
| 		f.fedWorker.Queue(messages.FromFederator{ | 		f.fedWorker.Queue(messages.FromFederator{ | ||||||
| 			APObjectType:     ap.ObjectProfile, | 			APObjectType:     ap.ObjectProfile, | ||||||
| 			APActivityType:   ap.ActivityDelete, | 			APActivityType:   ap.ActivityDelete, | ||||||
|  |  | ||||||
|  | @ -57,7 +57,6 @@ import ( | ||||||
| // 18. Delete account itself | // 18. Delete account itself | ||||||
| func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { | func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { | ||||||
| 	fields := kv.Fields{ | 	fields := kv.Fields{ | ||||||
| 
 |  | ||||||
| 		{"username", account.Username}, | 		{"username", account.Username}, | ||||||
| 	} | 	} | ||||||
| 	if account.Domain != "" { | 	if account.Domain != "" { | ||||||
|  | @ -163,7 +162,7 @@ selectStatusesLoop: | ||||||
| 			// pass the status delete through the client api channel for processing | 			// pass the status delete through the client api channel for processing | ||||||
| 			s.Account = account | 			s.Account = account | ||||||
| 
 | 
 | ||||||
| 			l.Debug("putting status in the client api channel") | 			l.Debug("putting status delete in the client api channel") | ||||||
| 			p.clientWorker.Queue(messages.FromClientAPI{ | 			p.clientWorker.Queue(messages.FromClientAPI{ | ||||||
| 				APObjectType:   ap.ObjectNote, | 				APObjectType:   ap.ObjectNote, | ||||||
| 				APActivityType: ap.ActivityDelete, | 				APActivityType: ap.ActivityDelete, | ||||||
|  | @ -172,24 +171,8 @@ selectStatusesLoop: | ||||||
| 				TargetAccount:  account, | 				TargetAccount:  account, | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			if err := p.db.DeleteByID(ctx, s.ID, s); err != nil { |  | ||||||
| 				if !errors.Is(err, db.ErrNoEntries) { |  | ||||||
| 					// actual error has occurred |  | ||||||
| 					l.Errorf("Delete: db error deleting status %s for account %s: %s", s.ID, account.Username, err) |  | ||||||
| 					continue |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			// if there are any boosts of this status, delete them as well | 			// if there are any boosts of this status, delete them as well | ||||||
| 			boosts := []*gtsmodel.Status{} | 			if boosts, err := p.db.GetStatusReblogs(ctx, s); err == nil { | ||||||
| 			if err := p.db.GetWhere(ctx, []db.Where{{Key: "boost_of_id", Value: s.ID}}, &boosts); err != nil { |  | ||||||
| 				if !errors.Is(err, db.ErrNoEntries) { |  | ||||||
| 					// an actual error has occurred |  | ||||||
| 					l.Errorf("Delete: db error selecting boosts of status %s for account %s: %s", s.ID, account.Username, err) |  | ||||||
| 					continue |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 				for _, b := range boosts { | 				for _, b := range boosts { | ||||||
| 					if b.Account == nil { | 					if b.Account == nil { | ||||||
| 						bAccount, err := p.db.GetAccountByID(ctx, b.AccountID) | 						bAccount, err := p.db.GetAccountByID(ctx, b.AccountID) | ||||||
|  | @ -197,7 +180,6 @@ selectStatusesLoop: | ||||||
| 							l.Errorf("Delete: db error populating boosted status account: %v", err) | 							l.Errorf("Delete: db error populating boosted status account: %v", err) | ||||||
| 							continue | 							continue | ||||||
| 						} | 						} | ||||||
| 
 |  | ||||||
| 						b.Account = bAccount | 						b.Account = bAccount | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
|  | @ -209,13 +191,6 @@ selectStatusesLoop: | ||||||
| 						OriginAccount:  b.Account, | 						OriginAccount:  b.Account, | ||||||
| 						TargetAccount:  account, | 						TargetAccount:  account, | ||||||
| 					}) | 					}) | ||||||
| 
 |  | ||||||
| 				if err := p.db.DeleteByID(ctx, b.ID, b); err != nil { |  | ||||||
| 					if err != db.ErrNoEntries { |  | ||||||
| 						// actual error has occurred |  | ||||||
| 						l.Errorf("Delete: db error deleting boost with id %s: %s", b.ID, err) |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -288,6 +288,10 @@ func (p *processor) processUndoAnnounceFromClientAPI(ctx context.Context, client | ||||||
| 		return errors.New("undo was not parseable as *gtsmodel.Status") | 		return errors.New("undo was not parseable as *gtsmodel.Status") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if err := p.db.DeleteStatusByID(ctx, boost.ID); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if err := p.deleteStatusFromTimelines(ctx, boost); err != nil { | 	if err := p.deleteStatusFromTimelines(ctx, boost); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -469,36 +469,39 @@ func (p *processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete all mentions for this status | 	// delete all mention entries generated by this status | ||||||
| 	for _, m := range statusToDelete.MentionIDs { | 	for _, m := range statusToDelete.MentionIDs { | ||||||
| 		if err := p.db.DeleteByID(ctx, m, >smodel.Mention{}); err != nil { | 		if err := p.db.DeleteByID(ctx, m, >smodel.Mention{}); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete all notifications for this status | 	// delete all notification entries generated by this status | ||||||
| 	if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { | 	if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete all boosts for this status + remove them from timelines | 	// delete all boosts for this status + remove them from timelines | ||||||
| 	boosts, err := p.db.GetStatusReblogs(ctx, statusToDelete) | 	if boosts, err := p.db.GetStatusReblogs(ctx, statusToDelete); err == nil { | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 		for _, b := range boosts { | 		for _, b := range boosts { | ||||||
| 			if err := p.deleteStatusFromTimelines(ctx, b); err != nil { | 			if err := p.deleteStatusFromTimelines(ctx, b); err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 		if err := p.db.DeleteByID(ctx, b.ID, b); err != nil { | 			if err := p.db.DeleteStatusByID(ctx, b.ID); err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete this status from any and all timelines | 	// delete this status from any and all timelines | ||||||
| 	if err := p.deleteStatusFromTimelines(ctx, statusToDelete); err != nil { | 	if err := p.deleteStatusFromTimelines(ctx, statusToDelete); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// delete the status itself | ||||||
|  | 	if err := p.db.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -368,9 +368,12 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() { | ||||||
| 	suite.False(zorkFollowsSatan) | 	suite.False(zorkFollowsSatan) | ||||||
| 
 | 
 | ||||||
| 	// no statuses from foss satan should be left in the database | 	// no statuses from foss satan should be left in the database | ||||||
| 	dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false) | 	if !testrig.WaitFor(func() bool { | ||||||
| 	suite.ErrorIs(err, db.ErrNoEntries) | 		s, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false) | ||||||
| 	suite.Empty(dbStatuses) | 		return  s == nil && err == db.ErrNoEntries | ||||||
|  | 	}) { | ||||||
|  | 		suite.FailNow("timeout waiting for statuses to be deleted") | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID) | 	dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
|  |  | ||||||
|  | @ -48,11 +48,7 @@ func (p *processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco | ||||||
| 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) | 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := p.db.DeleteByID(ctx, targetStatus.ID, >smodel.Status{}); err != nil { | 	// send the status back to the processor for async processing | ||||||
| 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// send it back to the processor for async processing |  | ||||||
| 	p.clientWorker.Queue(messages.FromClientAPI{ | 	p.clientWorker.Queue(messages.FromClientAPI{ | ||||||
| 		APObjectType:   ap.ObjectNote, | 		APObjectType:   ap.ObjectNote, | ||||||
| 		APActivityType: ap.ActivityDelete, | 		APActivityType: ap.ActivityDelete, | ||||||
|  |  | ||||||
|  | @ -78,14 +78,8 @@ func (p *processor) Unboost(ctx context.Context, requestingAccount *gtsmodel.Acc | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if toUnboost { | 	if toUnboost { | ||||||
| 		// we had a boost, so take some action to get rid of it |  | ||||||
| 		if err := p.db.DeleteWhere(ctx, where, >smodel.Status{}); err != nil { |  | ||||||
| 			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unboosting status: %s", err)) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// pin some stuff onto the boost while we have it out of the db | 		// pin some stuff onto the boost while we have it out of the db | ||||||
| 		gtsBoost.Account = requestingAccount | 		gtsBoost.Account = requestingAccount | ||||||
| 
 |  | ||||||
| 		gtsBoost.BoostOf = targetStatus | 		gtsBoost.BoostOf = targetStatus | ||||||
| 		gtsBoost.BoostOfAccount = targetStatus.Account | 		gtsBoost.BoostOfAccount = targetStatus.Account | ||||||
| 		gtsBoost.BoostOf.Account = targetStatus.Account | 		gtsBoost.BoostOf.Account = targetStatus.Account | ||||||
|  |  | ||||||
|  | @ -822,7 +822,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { | ||||||
| 		"remote_account_1_status_1_attachment_1": { | 		"remote_account_1_status_1_attachment_1": { | ||||||
| 			ID:        "01FVW7RXPQ8YJHTEXYPE7Q8ZY0", | 			ID:        "01FVW7RXPQ8YJHTEXYPE7Q8ZY0", | ||||||
| 			StatusID:  "01FVW7JHQFSFK166WWKR8CBA6M", | 			StatusID:  "01FVW7JHQFSFK166WWKR8CBA6M", | ||||||
| 			URL:       "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 			URL:       "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 			RemoteURL: "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpeg", | 			RemoteURL: "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpeg", | ||||||
| 			CreatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | 			CreatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
| 			UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | 			UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
|  | @ -845,23 +845,23 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { | ||||||
| 					Y: 0, | 					Y: 0, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			AccountID:         "01F8MH1H7YV1Z7D2C8K2730QBF", | 			AccountID:         "01F8MH5ZK5VRH73AKHQM6Y9VNX", | ||||||
| 			Description:       "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted", | 			Description:       "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted", | ||||||
| 			ScheduledStatusID: "", | 			ScheduledStatusID: "", | ||||||
| 			Blurhash:          "LARysgM_IU_3~pD%M_Rj_39FIAt6", | 			Blurhash:          "LARysgM_IU_3~pD%M_Rj_39FIAt6", | ||||||
| 			Processing:        2, | 			Processing:        2, | ||||||
| 			File: gtsmodel.File{ | 			File: gtsmodel.File{ | ||||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 				ContentType: "image/jpeg", | 				ContentType: "image/jpeg", | ||||||
| 				FileSize:    19310, | 				FileSize:    19310, | ||||||
| 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
| 			}, | 			}, | ||||||
| 			Thumbnail: gtsmodel.Thumbnail{ | 			Thumbnail: gtsmodel.Thumbnail{ | ||||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 				ContentType: "image/jpeg", | 				ContentType: "image/jpeg", | ||||||
| 				FileSize:    20395, | 				FileSize:    20395, | ||||||
| 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
| 				URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 				URL:         "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 				RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpeg", | 				RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpeg", | ||||||
| 			}, | 			}, | ||||||
| 			Avatar: FalseBool(), | 			Avatar: FalseBool(), | ||||||
|  | @ -871,7 +871,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { | ||||||
| 		"remote_account_1_status_1_attachment_2": { | 		"remote_account_1_status_1_attachment_2": { | ||||||
| 			ID:        "01FVW7RXPQ8YJHTEXYPE7Q8ZY1", | 			ID:        "01FVW7RXPQ8YJHTEXYPE7Q8ZY1", | ||||||
| 			StatusID:  "01FVW7JHQFSFK166WWKR8CBA6M", | 			StatusID:  "01FVW7JHQFSFK166WWKR8CBA6M", | ||||||
| 			URL:       "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 			URL:       "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 			RemoteURL: "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpeg", | 			RemoteURL: "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpeg", | ||||||
| 			CreatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | 			CreatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
| 			UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | 			UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
|  | @ -894,23 +894,23 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { | ||||||
| 					Y: 0, | 					Y: 0, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			AccountID:         "01F8MH1H7YV1Z7D2C8K2730QBF", | 			AccountID:         "01F8MH5ZK5VRH73AKHQM6Y9VNX", | ||||||
| 			Description:       "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted", | 			Description:       "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted", | ||||||
| 			ScheduledStatusID: "", | 			ScheduledStatusID: "", | ||||||
| 			Blurhash:          "LARysgM_IU_3~pD%M_Rj_39FIAt6", | 			Blurhash:          "LARysgM_IU_3~pD%M_Rj_39FIAt6", | ||||||
| 			Processing:        2, | 			Processing:        2, | ||||||
| 			File: gtsmodel.File{ | 			File: gtsmodel.File{ | ||||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 				ContentType: "image/jpeg", | 				ContentType: "image/jpeg", | ||||||
| 				FileSize:    19310, | 				FileSize:    19310, | ||||||
| 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
| 			}, | 			}, | ||||||
| 			Thumbnail: gtsmodel.Thumbnail{ | 			Thumbnail: gtsmodel.Thumbnail{ | ||||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 				ContentType: "image/jpeg", | 				ContentType: "image/jpeg", | ||||||
| 				FileSize:    20395, | 				FileSize:    20395, | ||||||
| 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||||
| 				URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | 				URL:         "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||||
| 				RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpeg", | 				RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpeg", | ||||||
| 			}, | 			}, | ||||||
| 			Avatar: FalseBool(), | 			Avatar: FalseBool(), | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue