mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:52:26 -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)) | ||||
| } | ||||
| 
 | ||||
| // 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. | ||||
| // 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 | ||||
|  |  | |||
|  | @ -227,6 +227,42 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status) (* | |||
| 	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) { | ||||
| 	parents := []*gtsmodel.Status{} | ||||
| 	s.statusParent(ctx, status, &parents, onlyDirect) | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ import ( | |||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| ) | ||||
| 
 | ||||
| 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) { | ||||
| 	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(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(ctx context.Context, status *gtsmodel.Status) (int, Error) | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,12 +20,10 @@ package federatingdb | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"codeberg.org/gruf/go-kv" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||
| ) | ||||
|  | @ -38,12 +36,11 @@ import ( | |||
| // The library makes this call only after acquiring a lock first. | ||||
| func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { | ||||
| 	l := log.WithFields(kv.Fields{ | ||||
| 
 | ||||
| 		{"id", id}, | ||||
| 	}...) | ||||
| 	l.Debug("entering Delete") | ||||
| 
 | ||||
| 	receivingAccount, _ := extractFromCtx(ctx) | ||||
| 	receivingAccount, requestingAccount := extractFromCtx(ctx) | ||||
| 	if receivingAccount == nil { | ||||
| 		// 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, | ||||
|  | @ -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, | ||||
| 	// so we have to try a few different things... | ||||
| 	s, err := f.db.GetStatusByURI(ctx, id.String()) | ||||
| 	if err == nil { | ||||
| 		// 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) | ||||
| 		} | ||||
| 	if s, err := f.db.GetStatusByURI(ctx, id.String()); err == nil && requestingAccount.ID == s.AccountID { | ||||
| 		l.Debugf("uri is for STATUS with id: %s", s.ID) | ||||
| 		f.fedWorker.Queue(messages.FromFederator{ | ||||
| 			APObjectType:     ap.ObjectNote, | ||||
| 			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 err == nil { | ||||
| 		// it's an account | ||||
| 		l.Debugf("uri is for an account with id %s, passing delete message to the processor", a.ID) | ||||
| 	if a, err := f.db.GetAccountByURI(ctx, id.String()); err == nil && requestingAccount.ID == a.ID { | ||||
| 		l.Debugf("uri is for ACCOUNT with id %s", a.ID) | ||||
| 		f.fedWorker.Queue(messages.FromFederator{ | ||||
| 			APObjectType:     ap.ObjectProfile, | ||||
| 			APActivityType:   ap.ActivityDelete, | ||||
|  |  | |||
|  | @ -57,7 +57,6 @@ import ( | |||
| // 18. Delete account itself | ||||
| func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { | ||||
| 	fields := kv.Fields{ | ||||
| 
 | ||||
| 		{"username", account.Username}, | ||||
| 	} | ||||
| 	if account.Domain != "" { | ||||
|  | @ -163,7 +162,7 @@ selectStatusesLoop: | |||
| 			// pass the status delete through the client api channel for processing | ||||
| 			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{ | ||||
| 				APObjectType:   ap.ObjectNote, | ||||
| 				APActivityType: ap.ActivityDelete, | ||||
|  | @ -172,50 +171,26 @@ selectStatusesLoop: | |||
| 				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 | ||||
| 			boosts := []*gtsmodel.Status{} | ||||
| 			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 { | ||||
| 				if b.Account == nil { | ||||
| 					bAccount, err := p.db.GetAccountByID(ctx, b.AccountID) | ||||
| 					if err != nil { | ||||
| 						l.Errorf("Delete: db error populating boosted status account: %v", err) | ||||
| 						continue | ||||
| 			if boosts, err := p.db.GetStatusReblogs(ctx, s); err == nil { | ||||
| 				for _, b := range boosts { | ||||
| 					if b.Account == nil { | ||||
| 						bAccount, err := p.db.GetAccountByID(ctx, b.AccountID) | ||||
| 						if err != nil { | ||||
| 							l.Errorf("Delete: db error populating boosted status account: %v", err) | ||||
| 							continue | ||||
| 						} | ||||
| 						b.Account = bAccount | ||||
| 					} | ||||
| 
 | ||||
| 					b.Account = bAccount | ||||
| 				} | ||||
| 
 | ||||
| 				l.Debug("putting boost undo in the client api channel") | ||||
| 				p.clientWorker.Queue(messages.FromClientAPI{ | ||||
| 					APObjectType:   ap.ActivityAnnounce, | ||||
| 					APActivityType: ap.ActivityUndo, | ||||
| 					GTSModel:       s, | ||||
| 					OriginAccount:  b.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 | ||||
| 					} | ||||
| 					l.Debug("putting boost undo in the client api channel") | ||||
| 					p.clientWorker.Queue(messages.FromClientAPI{ | ||||
| 						APObjectType:   ap.ActivityAnnounce, | ||||
| 						APActivityType: ap.ActivityUndo, | ||||
| 						GTSModel:       s, | ||||
| 						OriginAccount:  b.Account, | ||||
| 						TargetAccount:  account, | ||||
| 					}) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
|  |  | |||
|  | @ -288,6 +288,10 @@ func (p *processor) processUndoAnnounceFromClientAPI(ctx context.Context, client | |||
| 		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 { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -469,29 +469,27 @@ 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 { | ||||
| 		if err := p.db.DeleteByID(ctx, m, >smodel.Mention{}); err != nil { | ||||
| 			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 { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// delete all boosts for this status + remove them from timelines | ||||
| 	boosts, err := p.db.GetStatusReblogs(ctx, statusToDelete) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, b := range boosts { | ||||
| 		if err := p.deleteStatusFromTimelines(ctx, b); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := p.db.DeleteByID(ctx, b.ID, b); err != nil { | ||||
| 			return err | ||||
| 	if boosts, err := p.db.GetStatusReblogs(ctx, statusToDelete); err == nil { | ||||
| 		for _, b := range boosts { | ||||
| 			if err := p.deleteStatusFromTimelines(ctx, b); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			if err := p.db.DeleteStatusByID(ctx, b.ID); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -500,5 +498,10 @@ func (p *processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta | |||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// delete the status itself | ||||
| 	if err := p.db.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -368,9 +368,12 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() { | |||
| 	suite.False(zorkFollowsSatan) | ||||
| 
 | ||||
| 	// 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) | ||||
| 	suite.ErrorIs(err, db.ErrNoEntries) | ||||
| 	suite.Empty(dbStatuses) | ||||
| 	if !testrig.WaitFor(func() bool { | ||||
| 		s, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false) | ||||
| 		return  s == nil && err == db.ErrNoEntries | ||||
| 	}) { | ||||
| 		suite.FailNow("timeout waiting for statuses to be deleted") | ||||
| 	} | ||||
| 
 | ||||
| 	dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID) | ||||
| 	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)) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := p.db.DeleteByID(ctx, targetStatus.ID, >smodel.Status{}); err != nil { | ||||
| 		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err)) | ||||
| 	} | ||||
| 
 | ||||
| 	// send it back to the processor for async processing | ||||
| 	// send the status back to the processor for async processing | ||||
| 	p.clientWorker.Queue(messages.FromClientAPI{ | ||||
| 		APObjectType:   ap.ObjectNote, | ||||
| 		APActivityType: ap.ActivityDelete, | ||||
|  |  | |||
|  | @ -78,14 +78,8 @@ func (p *processor) Unboost(ctx context.Context, requestingAccount *gtsmodel.Acc | |||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| 		gtsBoost.Account = requestingAccount | ||||
| 
 | ||||
| 		gtsBoost.BoostOf = targetStatus | ||||
| 		gtsBoost.BoostOfAccount = targetStatus.Account | ||||
| 		gtsBoost.BoostOf.Account = targetStatus.Account | ||||
|  |  | |||
|  | @ -822,7 +822,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { | |||
| 		"remote_account_1_status_1_attachment_1": { | ||||
| 			ID:        "01FVW7RXPQ8YJHTEXYPE7Q8ZY0", | ||||
| 			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", | ||||
| 			CreatedAt: 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, | ||||
| 				}, | ||||
| 			}, | ||||
| 			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", | ||||
| 			ScheduledStatusID: "", | ||||
| 			Blurhash:          "LARysgM_IU_3~pD%M_Rj_39FIAt6", | ||||
| 			Processing:        2, | ||||
| 			File: gtsmodel.File{ | ||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				ContentType: "image/jpeg", | ||||
| 				FileSize:    19310, | ||||
| 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||
| 			}, | ||||
| 			Thumbnail: gtsmodel.Thumbnail{ | ||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				ContentType: "image/jpeg", | ||||
| 				FileSize:    20395, | ||||
| 				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", | ||||
| 			}, | ||||
| 			Avatar: FalseBool(), | ||||
|  | @ -871,7 +871,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { | |||
| 		"remote_account_1_status_1_attachment_2": { | ||||
| 			ID:        "01FVW7RXPQ8YJHTEXYPE7Q8ZY1", | ||||
| 			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", | ||||
| 			CreatedAt: 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, | ||||
| 				}, | ||||
| 			}, | ||||
| 			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", | ||||
| 			ScheduledStatusID: "", | ||||
| 			Blurhash:          "LARysgM_IU_3~pD%M_Rj_39FIAt6", | ||||
| 			Processing:        2, | ||||
| 			File: gtsmodel.File{ | ||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				ContentType: "image/jpeg", | ||||
| 				FileSize:    19310, | ||||
| 				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"), | ||||
| 			}, | ||||
| 			Thumbnail: gtsmodel.Thumbnail{ | ||||
| 				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", | ||||
| 				ContentType: "image/jpeg", | ||||
| 				FileSize:    20395, | ||||
| 				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", | ||||
| 			}, | ||||
| 			Avatar: FalseBool(), | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue