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