From 80924744d11cf7b3d8ffd9cf079556c7fa7ef09f Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Fri, 2 Jul 2021 13:23:21 +0200 Subject: [PATCH] delete more stuff when an account is gone --- internal/api/client/account/statuses.go | 6 +- internal/db/db.go | 4 +- internal/db/pg/pg.go | 26 ++- internal/oauth/util.go | 22 +- internal/processing/account.go | 4 +- internal/processing/account/account.go | 2 +- internal/processing/account/delete.go | 83 ++++++- internal/processing/account/getstatuses.go | 10 +- internal/processing/fromclientapi.go | 33 ++- internal/processing/fromfederator.go | 21 ++ internal/processing/media.go | 253 +-------------------- internal/processing/media/create.go | 79 +++++++ internal/processing/media/delete.go | 51 +++++ internal/processing/media/getfile.go | 120 ++++++++++ internal/processing/media/getmedia.go | 51 +++++ internal/processing/media/media.go | 63 +++++ internal/processing/media/update.go | 70 ++++++ internal/processing/media/util.go | 63 +++++ internal/processing/processor.go | 4 + internal/processing/status/delete.go | 1 + 20 files changed, 682 insertions(+), 284 deletions(-) create mode 100644 internal/processing/media/create.go create mode 100644 internal/processing/media/delete.go create mode 100644 internal/processing/media/getfile.go create mode 100644 internal/processing/media/getmedia.go create mode 100644 internal/processing/media/media.go create mode 100644 internal/processing/media/update.go create mode 100644 internal/processing/media/util.go diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go index f03a942f3..c92e85cee 100644 --- a/internal/api/client/account/statuses.go +++ b/internal/api/client/account/statuses.go @@ -82,7 +82,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { maxID = maxIDString } - pinned := false + pinnedOnly := false pinnedString := c.Query(PinnedKey) if pinnedString != "" { i, err := strconv.ParseBool(pinnedString) @@ -91,7 +91,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"}) return } - pinned = i + pinnedOnly = i } mediaOnly := false @@ -106,7 +106,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { mediaOnly = i } - statuses, errWithCode := m.processor.AccountStatusesGet(authed, targetAcctID, limit, excludeReplies, maxID, pinned, mediaOnly) + statuses, errWithCode := m.processor.AccountStatusesGet(authed, targetAcctID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) if errWithCode != nil { l.Debugf("error from processor account statuses get: %s", errWithCode) c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) diff --git a/internal/db/db.go b/internal/db/db.go index 3010242a9..1ec02d22c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -150,11 +150,11 @@ type DB interface { // CountStatusesByAccountID is a shortcut for the common action of counting statuses produced by accountID. CountStatusesByAccountID(accountID string) (int, error) - // GetStatusesByTimeDescending is a shortcut for getting the most recent statuses. accountID is optional, if not provided + // GetStatusesForAccount is a shortcut for getting the most recent statuses. accountID is optional, if not provided // then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can // be very memory intensive so you probably shouldn't do this! // In case of no entries, a 'no entries' error will be returned - GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) error + GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) // GetLastStatusForAccountID simply gets the most recent status by the given account. // The given slice 'status' pointer will be set to the result of the query, whatever it is. diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index 2758d3c3d..dffe06ed7 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -511,39 +511,43 @@ func (ps *postgresService) CountStatusesByAccountID(accountID string) (int, erro return count, nil } -func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) error { - q := ps.conn.Model(statuses).Order("created_at DESC") +func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) { + statuses := []*gtsmodel.Status{} + + q := ps.conn.Model(&statuses).Order("id DESC") if accountID != "" { q = q.Where("account_id = ?", accountID) } + if limit != 0 { q = q.Limit(limit) } + if excludeReplies { q = q.Where("? IS NULL", pg.Ident("in_reply_to_id")) } - if pinned { + + if pinnedOnly { q = q.Where("pinned = ?", true) } + if mediaOnly { q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) { return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil }) } + if maxID != "" { - s := >smodel.Status{} - if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil { - return err - } - q = q.Where("status.created_at < ?", s.CreatedAt) + q = q.Where("id < ?", maxID) } + if err := q.Select(); err != nil { if err == pg.ErrNoRows { - return db.ErrNoEntries{} + return nil, db.ErrNoEntries{} } - return err + return nil, err } - return nil + return statuses, nil } func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { diff --git a/internal/oauth/util.go b/internal/oauth/util.go index 378b81450..2f2e2e182 100644 --- a/internal/oauth/util.go +++ b/internal/oauth/util.go @@ -73,14 +73,28 @@ func Authed(c *gin.Context, requireToken bool, requireApp bool, requireUser bool if requireToken && a.Token == nil { return nil, errors.New("token not supplied") } + if requireApp && a.Application == nil { return nil, errors.New("application not supplied") } - if requireUser && a.User == nil { - return nil, errors.New("user not supplied") + + if requireUser { + if a.User == nil { + return nil, errors.New("user not supplied") + } + if a.User.Disabled || !a.User.Approved { + return nil, errors.New("user disabled or not approved") + } } - if requireAccount && a.Account == nil { - return nil, errors.New("account not supplied") + + if requireAccount { + if a.Account == nil { + return nil, errors.New("account not supplied") + } + if !a.Account.SuspendedAt.IsZero() { + return nil, errors.New("account suspended") + } } + return a, nil } diff --git a/internal/processing/account.go b/internal/processing/account.go index dc1664db9..ec58846d1 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -36,8 +36,8 @@ func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCrede return p.accountProcessor.Update(authed.Account, form) } -func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { - return p.accountProcessor.StatusesGet(authed.Account, targetAccountID, limit, excludeReplies, maxID, pinned, mediaOnly) +func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { + return p.accountProcessor.StatusesGet(authed.Account, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) } func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index 03ced3160..442b6fa9f 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -40,7 +40,7 @@ type Processor interface { // Create processes the given form for creating a new account, returning an oauth token for that account if successful. Create(applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) // Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc. - Delete(account *gtsmodel.Account) error + Delete(account *gtsmodel.Account, deletedBy string) error // Get processes the given request for account information. Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) // Update processes the update of an account with the given form diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 61d21eb81..26dae19fc 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -19,13 +19,15 @@ package account import ( + "time" + "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) Delete(account *gtsmodel.Account) error { +func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error { l := p.log.WithFields(logrus.Fields{ "func": "Delete", "username": account.Username, @@ -39,11 +41,11 @@ func (p *processor) Delete(account *gtsmodel.Account) error { // 3. Delete account's emoji // 4. Delete account's follow requests // 5. Delete account's follows - // 6. Delete account's media attachments - // 7. Delete account's mentions - // 8. Delete account's notifications + // 6. Delete account's statuses + // 7. Delete account's media attachments + // 8. Delete account's mentions // 9. Delete account's polls - // 10. Delete account's statuses + // 10. Delete account's notifications // 11. Delete account's bookmarks // 12. Delete account's faves // 13. Delete account's mutes @@ -117,5 +119,74 @@ func (p *processor) Delete(account *gtsmodel.Account) error { l.Errorf("error deleting follows targeting account: %s", err) } - return nil + // 6. Delete account's statuses + // we'll select statuses 20 at a time so we don't wreck the db, and pass them through to the client api channel + // Deleting the statuses in this way also handles 7. Delete account's media attachments, 8. Delete account's mentions, and 9. Delete account's polls, + // since these are all attached to statuses. + var maxID string +selectStatusesLoop: + for { + statuses, err := p.db.GetStatusesForAccount(account.ID, 20, false, maxID, false, false) + if err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + // no accounts left for this instance so we're done + l.Infof("Delete: done iterating through statuses for account %s", account.Username) + break selectStatusesLoop + } + // an actual error has occurred + l.Errorf("Delete: db error selecting statuses for account %s: %s", account.Username, err) + break selectStatusesLoop + } + + for i, s := range statuses { + // pass the status delete through the client api channel for processing + s.GTSAuthorAccount = account + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsNote, + APActivityType: gtsmodel.ActivityStreamsDelete, + GTSModel: s, + OriginAccount: account, + TargetAccount: account, + } + + if err := p.db.DeleteByID(s.ID, s); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // actual error has occurred + l.Errorf("Delete: db error status %s for account %s: %s", s.ID, account.Username, err) + break selectStatusesLoop + } + } + + // if this is the last status in the slice, set the maxID appropriately for the next query + if i == len(statuses)-1 { + maxID = s.ID + } + } + } + + // 10. Delete account's notifications + if err := p.db.DeleteWhere([]db.Where{{Key: "origin_account_id", Value: account.ID}}, &[]*gtsmodel.Notification{}); err != nil { + l.Errorf("error deleting notifications created by account: %s", err) + } + + // to prevent the account being created again, set all these fields and update it in the db + // the account won't actually be *removed* from the database but it will be set to just a stub + + account.Note = "" + account.DisplayName = "" + account.AvatarMediaAttachmentID = "" + account.AvatarRemoteURL = "" + account.HeaderMediaAttachmentID = "" + account.HeaderRemoteURL = "" + account.Reason = "" + account.Fields = []gtsmodel.Field{} + account.HideCollections = true + account.Discoverable = false + + account.UpdatedAt = time.Now() + + account.SuspendedAt = time.Now() + account.SuspensionOrigin = deletedBy + + return p.db.UpdateByID(account.ID, account) } diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go index eb0f448d0..9b317aa13 100644 --- a/internal/processing/account/getstatuses.go +++ b/internal/processing/account/getstatuses.go @@ -27,7 +27,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { +func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { targetAccount := >smodel.Account{} if err := p.db.GetByID(targetAccountID, targetAccount); err != nil { if _, ok := err.(db.ErrNoEntries); ok { @@ -36,9 +36,9 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou return nil, gtserror.NewErrorInternalError(err) } - statuses := []gtsmodel.Status{} apiStatuses := []apimodel.Status{} - if err := p.db.GetStatusesByTimeDescending(targetAccountID, &statuses, limit, excludeReplies, maxID, pinned, mediaOnly); err != nil { + statuses, err := p.db.GetStatusesForAccount(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) + if err != nil { if _, ok := err.(db.ErrNoEntries); ok { return apiStatuses, nil } @@ -46,7 +46,7 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou } for _, s := range statuses { - visible, err := p.filter.StatusVisible(&s, requestingAccount) + visible, err := p.filter.StatusVisible(s, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err)) } @@ -54,7 +54,7 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou continue } - apiStatus, err := p.tc.StatusToMasto(&s, requestingAccount) + apiStatus, err := p.tc.StatusToMasto(s, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err)) } diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 64b7f296b..9141e3367 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -25,6 +25,7 @@ import ( "net/url" "github.com/go-fed/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -161,6 +162,31 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return errors.New("note was not parseable as *gtsmodel.Status") } + if statusToDelete.GTSAuthorAccount == nil { + statusToDelete.GTSAuthorAccount = clientMsg.OriginAccount + } + + // delete all attachments for this status + for _, a := range statusToDelete.Attachments { + if err := p.mediaProcessor.Delete(a); err != nil { + return err + } + } + + // delete all mentions for this status + for _, m := range statusToDelete.Mentions { + if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil { + return err + } + } + + // delete all notifications for this status + if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { + return err + } + + + // delete this status from any and all timelines if err := p.deleteStatusFromTimelines(statusToDelete); err != nil { return err } @@ -173,7 +199,12 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return errors.New("account was not parseable as *gtsmodel.Account") } - return p.accountProcessor.Delete(accountToDelete) + var deletedBy string + if clientMsg.OriginAccount != nil { + deletedBy = clientMsg.OriginAccount.ID + } + + return p.accountProcessor.Delete(accountToDelete, deletedBy) } } return nil diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 4fd68330c..36568cf13 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -159,6 +159,27 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er if !ok { return errors.New("note was not parseable as *gtsmodel.Status") } + + // delete all attachments for this status + for _, a := range statusToDelete.Attachments { + if err := p.mediaProcessor.Delete(a); err != nil { + return err + } + } + + // delete all mentions for this status + for _, m := range statusToDelete.Mentions { + if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil { + return err + } + } + + // delete all notifications for this status + if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { + return err + } + + // remove this status from any and all timelines return p.deleteStatusFromTimelines(statusToDelete) case gtsmodel.ActivityStreamsProfile: // DELETE A PROFILE/ACCOUNT diff --git a/internal/processing/media.go b/internal/processing/media.go index 4f15632c1..6ca0eda5b 100644 --- a/internal/processing/media.go +++ b/internal/processing/media.go @@ -19,268 +19,23 @@ package processing import ( - "bytes" - "errors" - "fmt" - "io" - "strconv" - "strings" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { - // First check this user/account is permitted to create media - // There's no point continuing otherwise. - // - // TODO: move this check to the oauth.Authed function and do it for all accounts - if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { - return nil, errors.New("not authorized to post new media") - } - - // open the attachment and extract the bytes from it - f, err := form.File.Open() - if err != nil { - return nil, fmt.Errorf("error opening attachment: %s", err) - } - buf := new(bytes.Buffer) - size, err := io.Copy(buf, f) - if err != nil { - return nil, fmt.Errorf("error reading attachment: %s", err) - - } - if size == 0 { - return nil, errors.New("could not read provided attachment: size 0 bytes") - } - - // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using - attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID, "") - if err != nil { - return nil, fmt.Errorf("error reading attachment: %s", err) - } - - // now we need to add extra fields that the attachment processor doesn't know (from the form) - // TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it) - - // first description - attachment.Description = form.Description - - // now parse the focus parameter - focusx, focusy, err := parseFocus(form.Focus) - if err != nil { - return nil, err - } - attachment.FileMeta.Focus.X = focusx - attachment.FileMeta.Focus.Y = focusy - - // prepare the frontend representation now -- if there are any errors here at least we can bail without - // having already put something in the database and then having to clean it up again (eugh) - mastoAttachment, err := p.tc.AttachmentToMasto(attachment) - if err != nil { - return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err) - } - - // now we can confidently put the attachment in the database - if err := p.db.Put(attachment); err != nil { - return nil, fmt.Errorf("error storing media attachment in db: %s", err) - } - - return &mastoAttachment, nil + return p.mediaProcessor.Create(authed.Account, form) } func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { - attachment := >smodel.MediaAttachment{} - if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - // attachment doesn't exist - return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) - } - return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) - } - - if attachment.AccountID != authed.Account.ID { - return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account")) - } - - a, err := p.tc.AttachmentToMasto(attachment) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) - } - - return &a, nil + return p.mediaProcessor.GetMedia(authed.Account, mediaAttachmentID) } func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { - attachment := >smodel.MediaAttachment{} - if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - // attachment doesn't exist - return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) - } - return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) - } - - if attachment.AccountID != authed.Account.ID { - return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account")) - } - - if form.Description != nil { - attachment.Description = *form.Description - if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err)) - } - } - - if form.Focus != nil { - focusx, focusy, err := parseFocus(*form.Focus) - if err != nil { - return nil, gtserror.NewErrorBadRequest(err) - } - attachment.FileMeta.Focus.X = focusx - attachment.FileMeta.Focus.Y = focusy - if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err)) - } - } - - a, err := p.tc.AttachmentToMasto(attachment) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) - } - - return &a, nil + return p.mediaProcessor.Update(authed.Account, mediaAttachmentID, form) } func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) { - // parse the form fields - mediaSize, err := media.ParseMediaSize(form.MediaSize) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize)) - } - - mediaType, err := media.ParseMediaType(form.MediaType) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType)) - } - - spl := strings.Split(form.FileName, ".") - if len(spl) != 2 || spl[0] == "" || spl[1] == "" { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName)) - } - wantedMediaID := spl[0] - - // get the account that owns the media and make sure it's not suspended - acct := >smodel.Account{} - if err := p.db.GetByID(form.AccountID, acct); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err)) - } - if !acct.SuspendedAt.IsZero() { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID)) - } - - // make sure the requesting account and the media account don't block each other - if authed.Account != nil { - blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err)) - } - if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID)) - } - } - - // the way we store emojis is a little different from the way we store other attachments, - // so we need to take different steps depending on the media type being requested - content := &apimodel.Content{} - var storagePath string - switch mediaType { - case media.Emoji: - e := >smodel.Emoji{} - if err := p.db.GetByID(wantedMediaID, e); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err)) - } - if e.Disabled { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID)) - } - switch mediaSize { - case media.Original: - content.ContentType = e.ImageContentType - storagePath = e.ImagePath - case media.Static: - content.ContentType = e.ImageStaticContentType - storagePath = e.ImageStaticPath - default: - return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize)) - } - case media.Attachment, media.Header, media.Avatar: - a := >smodel.MediaAttachment{} - if err := p.db.GetByID(wantedMediaID, a); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err)) - } - if a.AccountID != form.AccountID { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID)) - } - switch mediaSize { - case media.Original: - content.ContentType = a.File.ContentType - storagePath = a.File.Path - case media.Small: - content.ContentType = a.Thumbnail.ContentType - storagePath = a.Thumbnail.Path - default: - return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize)) - } - } - - bytes, err := p.storage.RetrieveFileFrom(storagePath) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err)) - } - - content.ContentLength = int64(len(bytes)) - content.Content = bytes - return content, nil -} - -func parseFocus(focus string) (focusx, focusy float32, err error) { - if focus == "" { - return - } - spl := strings.Split(focus, ",") - if len(spl) != 2 { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - xStr := spl[0] - yStr := spl[1] - if xStr == "" || yStr == "" { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - fx, err := strconv.ParseFloat(xStr, 32) - if err != nil { - err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) - return - } - if fx > 1 || fx < -1 { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - focusx = float32(fx) - fy, err := strconv.ParseFloat(yStr, 32) - if err != nil { - err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) - return - } - if fy > 1 || fy < -1 { - err = fmt.Errorf("improperly formatted focus %s", focus) - return - } - focusy = float32(fy) - return + return p.mediaProcessor.GetFile(authed.Account, form) } diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go new file mode 100644 index 000000000..f9e383504 --- /dev/null +++ b/internal/processing/media/create.go @@ -0,0 +1,79 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package media + +import ( + "bytes" + "errors" + "fmt" + "io" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) Create(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { + // open the attachment and extract the bytes from it + f, err := form.File.Open() + if err != nil { + return nil, fmt.Errorf("error opening attachment: %s", err) + } + buf := new(bytes.Buffer) + size, err := io.Copy(buf, f) + if err != nil { + return nil, fmt.Errorf("error reading attachment: %s", err) + } + if size == 0 { + return nil, errors.New("could not read provided attachment: size 0 bytes") + } + + // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using + attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), account.ID, "") + if err != nil { + return nil, fmt.Errorf("error reading attachment: %s", err) + } + + // now we need to add extra fields that the attachment processor doesn't know (from the form) + // TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it) + + // first description + attachment.Description = form.Description + + // now parse the focus parameter + focusx, focusy, err := parseFocus(form.Focus) + if err != nil { + return nil, err + } + attachment.FileMeta.Focus.X = focusx + attachment.FileMeta.Focus.Y = focusy + + // prepare the frontend representation now -- if there are any errors here at least we can bail without + // having already put something in the database and then having to clean it up again (eugh) + mastoAttachment, err := p.tc.AttachmentToMasto(attachment) + if err != nil { + return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err) + } + + // now we can confidently put the attachment in the database + if err := p.db.Put(attachment); err != nil { + return nil, fmt.Errorf("error storing media attachment in db: %s", err) + } + + return &mastoAttachment, nil +} diff --git a/internal/processing/media/delete.go b/internal/processing/media/delete.go new file mode 100644 index 000000000..694d78ac3 --- /dev/null +++ b/internal/processing/media/delete.go @@ -0,0 +1,51 @@ +package media + +import ( + "fmt" + "strings" + + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode { + a := >smodel.MediaAttachment{} + if err := p.db.GetByID(mediaAttachmentID, a); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + // attachment already gone + return nil + } + // actual error + return gtserror.NewErrorInternalError(err) + } + + errs := []string{} + + // delete the thumbnail from storage + if a.Thumbnail.Path != "" { + if err := p.storage.RemoveFileAt(a.Thumbnail.Path); err != nil { + errs = append(errs, fmt.Sprintf("remove thumbnail at path %s: %s", a.Thumbnail.Path, err)) + } + } + + // delete the file from storage + if a.File.Path != "" { + if err := p.storage.RemoveFileAt(a.File.Path); err != nil { + errs = append(errs, fmt.Sprintf("remove file at path %s: %s", a.File.Path, err)) + } + } + + // delete the attachment + if err := p.db.DeleteByID(mediaAttachmentID, a); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + errs = append(errs, fmt.Sprintf("remove attachment: %s", err)) + } + } + + if len(errs) != 0 { + return gtserror.NewErrorInternalError(fmt.Errorf("Delete: one or more errors removing attachment with id %s: %s", mediaAttachmentID, strings.Join(errs, "; "))) + } + + return nil +} diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go new file mode 100644 index 000000000..f64d79a26 --- /dev/null +++ b/internal/processing/media/getfile.go @@ -0,0 +1,120 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package media + +import ( + "fmt" + "strings" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +) + +func (p *processor) GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) { + // parse the form fields + mediaSize, err := media.ParseMediaSize(form.MediaSize) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize)) + } + + mediaType, err := media.ParseMediaType(form.MediaType) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType)) + } + + spl := strings.Split(form.FileName, ".") + if len(spl) != 2 || spl[0] == "" || spl[1] == "" { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName)) + } + wantedMediaID := spl[0] + + // get the account that owns the media and make sure it's not suspended + acct := >smodel.Account{} + if err := p.db.GetByID(form.AccountID, acct); err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err)) + } + if !acct.SuspendedAt.IsZero() { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID)) + } + + // make sure the requesting account and the media account don't block each other + if account != nil { + blocked, err := p.db.Blocked(account.ID, form.AccountID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, account.ID, err)) + } + if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, account.ID)) + } + } + + // the way we store emojis is a little different from the way we store other attachments, + // so we need to take different steps depending on the media type being requested + content := &apimodel.Content{} + var storagePath string + switch mediaType { + case media.Emoji: + e := >smodel.Emoji{} + if err := p.db.GetByID(wantedMediaID, e); err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err)) + } + if e.Disabled { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID)) + } + switch mediaSize { + case media.Original: + content.ContentType = e.ImageContentType + storagePath = e.ImagePath + case media.Static: + content.ContentType = e.ImageStaticContentType + storagePath = e.ImageStaticPath + default: + return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize)) + } + case media.Attachment, media.Header, media.Avatar: + a := >smodel.MediaAttachment{} + if err := p.db.GetByID(wantedMediaID, a); err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err)) + } + if a.AccountID != form.AccountID { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID)) + } + switch mediaSize { + case media.Original: + content.ContentType = a.File.ContentType + storagePath = a.File.Path + case media.Small: + content.ContentType = a.Thumbnail.ContentType + storagePath = a.Thumbnail.Path + default: + return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize)) + } + } + + bytes, err := p.storage.RetrieveFileFrom(storagePath) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err)) + } + + content.ContentLength = int64(len(bytes)) + content.Content = bytes + return content, nil +} diff --git a/internal/processing/media/getmedia.go b/internal/processing/media/getmedia.go new file mode 100644 index 000000000..c36370225 --- /dev/null +++ b/internal/processing/media/getmedia.go @@ -0,0 +1,51 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package media + +import ( + "errors" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { + attachment := >smodel.MediaAttachment{} + if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + // attachment doesn't exist + return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) + } + return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) + } + + if attachment.AccountID != account.ID { + return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account")) + } + + a, err := p.tc.AttachmentToMasto(attachment) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) + } + + return &a, nil +} diff --git a/internal/processing/media/media.go b/internal/processing/media/media.go new file mode 100644 index 000000000..79c9a7e18 --- /dev/null +++ b/internal/processing/media/media.go @@ -0,0 +1,63 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package media + +import ( + "github.com/sirupsen/logrus" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/blob" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" +) + +// Processor wraps a bunch of functions for processing media actions. +type Processor interface { + // Create creates a new media attachment belonging to the given account, using the request form. + Create(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) + // Delete deletes the media attachment with the given ID, including all files pertaining to that attachment. + Delete(mediaAttachmentID string) gtserror.WithCode + GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) + GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) + Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) +} + +type processor struct { + tc typeutils.TypeConverter + config *config.Config + mediaHandler media.Handler + storage blob.Storage + db db.DB + log *logrus.Logger +} + +// New returns a new media processor. +func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, storage blob.Storage, config *config.Config, log *logrus.Logger) Processor { + return &processor{ + tc: tc, + config: config, + mediaHandler: mediaHandler, + storage: storage, + db: db, + log: log, + } +} diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go new file mode 100644 index 000000000..e27960371 --- /dev/null +++ b/internal/processing/media/update.go @@ -0,0 +1,70 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package media + +import ( + "errors" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +) + +func (p *processor) Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { + attachment := >smodel.MediaAttachment{} + if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + // attachment doesn't exist + return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) + } + return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) + } + + if attachment.AccountID != account.ID { + return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account")) + } + + if form.Description != nil { + attachment.Description = *form.Description + if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err)) + } + } + + if form.Focus != nil { + focusx, focusy, err := parseFocus(*form.Focus) + if err != nil { + return nil, gtserror.NewErrorBadRequest(err) + } + attachment.FileMeta.Focus.X = focusx + attachment.FileMeta.Focus.Y = focusy + if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err)) + } + } + + a, err := p.tc.AttachmentToMasto(attachment) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) + } + + return &a, nil +} diff --git a/internal/processing/media/util.go b/internal/processing/media/util.go new file mode 100644 index 000000000..47ea4fccd --- /dev/null +++ b/internal/processing/media/util.go @@ -0,0 +1,63 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package media + +import ( + "fmt" + "strconv" + "strings" +) + +func parseFocus(focus string) (focusx, focusy float32, err error) { + if focus == "" { + return + } + spl := strings.Split(focus, ",") + if len(spl) != 2 { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + xStr := spl[0] + yStr := spl[1] + if xStr == "" || yStr == "" { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + fx, err := strconv.ParseFloat(xStr, 32) + if err != nil { + err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) + return + } + if fx > 1 || fx < -1 { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + focusx = float32(fx) + fy, err := strconv.ParseFloat(yStr, 32) + if err != nil { + err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) + return + } + if fy > 1 || fy < -1 { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + focusy = float32(fy) + return +} diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 9ede72967..c0eaad42a 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -35,6 +35,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/processing/account" "github.com/superseriousbusiness/gotosocial/internal/processing/admin" + mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media" "github.com/superseriousbusiness/gotosocial/internal/processing/status" "github.com/superseriousbusiness/gotosocial/internal/processing/streaming" "github.com/superseriousbusiness/gotosocial/internal/timeline" @@ -219,6 +220,7 @@ type processor struct { adminProcessor admin.Processor statusProcessor status.Processor streamingProcessor streaming.Processor + mediaProcessor mediaProcessor.Processor } // NewProcessor returns a new Processor that uses the given federator and logger @@ -231,6 +233,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f streamingProcessor := streaming.New(db, tc, oauthServer, config, log) accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator, config, log) adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config, log) + mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config, log) return &processor{ fromClientAPI: fromClientAPI, @@ -251,6 +254,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f adminProcessor: adminProcessor, statusProcessor: statusProcessor, streamingProcessor: streamingProcessor, + mediaProcessor: mediaProcessor, } } diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go index 5da196a9f..259038dee 100644 --- a/internal/processing/status/delete.go +++ b/internal/processing/status/delete.go @@ -49,6 +49,7 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a APActivityType: gtsmodel.ActivityStreamsDelete, GTSModel: targetStatus, OriginAccount: account, + TargetAccount: account, } return mastoStatus, nil