mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-25 16:40:31 +00: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…
Reference in a new issue