mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-17 03:03:01 -06:00
delete more stuff when an account is gone
This commit is contained in:
parent
c2db321861
commit
80924744d1
20 changed files with 682 additions and 284 deletions
|
|
@ -82,7 +82,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
maxID = maxIDString
|
maxID = maxIDString
|
||||||
}
|
}
|
||||||
|
|
||||||
pinned := false
|
pinnedOnly := false
|
||||||
pinnedString := c.Query(PinnedKey)
|
pinnedString := c.Query(PinnedKey)
|
||||||
if pinnedString != "" {
|
if pinnedString != "" {
|
||||||
i, err := strconv.ParseBool(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"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pinned = i
|
pinnedOnly = i
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaOnly := false
|
mediaOnly := false
|
||||||
|
|
@ -106,7 +106,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
mediaOnly = i
|
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 {
|
if errWithCode != nil {
|
||||||
l.Debugf("error from processor account statuses get: %s", errWithCode)
|
l.Debugf("error from processor account statuses get: %s", errWithCode)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
|
|
||||||
|
|
@ -150,11 +150,11 @@ type DB interface {
|
||||||
// CountStatusesByAccountID is a shortcut for the common action of counting statuses produced by accountID.
|
// CountStatusesByAccountID is a shortcut for the common action of counting statuses produced by accountID.
|
||||||
CountStatusesByAccountID(accountID string) (int, error)
|
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
|
// 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!
|
// be very memory intensive so you probably shouldn't do this!
|
||||||
// In case of no entries, a 'no entries' error will be returned
|
// 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.
|
// 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.
|
// The given slice 'status' pointer will be set to the result of the query, whatever it is.
|
||||||
|
|
|
||||||
|
|
@ -511,39 +511,43 @@ func (ps *postgresService) CountStatusesByAccountID(accountID string) (int, erro
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) error {
|
func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) {
|
||||||
q := ps.conn.Model(statuses).Order("created_at DESC")
|
statuses := []*gtsmodel.Status{}
|
||||||
|
|
||||||
|
q := ps.conn.Model(&statuses).Order("id DESC")
|
||||||
if accountID != "" {
|
if accountID != "" {
|
||||||
q = q.Where("account_id = ?", accountID)
|
q = q.Where("account_id = ?", accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit != 0 {
|
if limit != 0 {
|
||||||
q = q.Limit(limit)
|
q = q.Limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if excludeReplies {
|
if excludeReplies {
|
||||||
q = q.Where("? IS NULL", pg.Ident("in_reply_to_id"))
|
q = q.Where("? IS NULL", pg.Ident("in_reply_to_id"))
|
||||||
}
|
}
|
||||||
if pinned {
|
|
||||||
|
if pinnedOnly {
|
||||||
q = q.Where("pinned = ?", true)
|
q = q.Where("pinned = ?", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if mediaOnly {
|
if mediaOnly {
|
||||||
q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) {
|
q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) {
|
||||||
return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil
|
return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if maxID != "" {
|
if maxID != "" {
|
||||||
s := >smodel.Status{}
|
q = q.Where("id < ?", maxID)
|
||||||
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
q = q.Where("status.created_at < ?", s.CreatedAt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := q.Select(); err != nil {
|
if err := q.Select(); err != nil {
|
||||||
if err == pg.ErrNoRows {
|
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 {
|
func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error {
|
||||||
|
|
|
||||||
|
|
@ -73,14 +73,28 @@ func Authed(c *gin.Context, requireToken bool, requireApp bool, requireUser bool
|
||||||
if requireToken && a.Token == nil {
|
if requireToken && a.Token == nil {
|
||||||
return nil, errors.New("token not supplied")
|
return nil, errors.New("token not supplied")
|
||||||
}
|
}
|
||||||
|
|
||||||
if requireApp && a.Application == nil {
|
if requireApp && a.Application == nil {
|
||||||
return nil, errors.New("application not supplied")
|
return nil, errors.New("application not supplied")
|
||||||
}
|
}
|
||||||
if requireUser && a.User == nil {
|
|
||||||
|
if requireUser {
|
||||||
|
if a.User == nil {
|
||||||
return nil, errors.New("user not supplied")
|
return nil, errors.New("user not supplied")
|
||||||
}
|
}
|
||||||
if requireAccount && a.Account == nil {
|
if a.User.Disabled || !a.User.Approved {
|
||||||
|
return nil, errors.New("user disabled or not approved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if requireAccount {
|
||||||
|
if a.Account == nil {
|
||||||
return nil, errors.New("account not supplied")
|
return nil, errors.New("account not supplied")
|
||||||
}
|
}
|
||||||
|
if !a.Account.SuspendedAt.IsZero() {
|
||||||
|
return nil, errors.New("account suspended")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCrede
|
||||||
return p.accountProcessor.Update(authed.Account, form)
|
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) {
|
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, pinned, mediaOnly)
|
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) {
|
func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
|
|
|
||||||
|
|
@ -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 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)
|
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 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 processes the given request for account information.
|
||||||
Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error)
|
Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error)
|
||||||
// Update processes the update of an account with the given form
|
// Update processes the update of an account with the given form
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,15 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"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{
|
l := p.log.WithFields(logrus.Fields{
|
||||||
"func": "Delete",
|
"func": "Delete",
|
||||||
"username": account.Username,
|
"username": account.Username,
|
||||||
|
|
@ -39,11 +41,11 @@ func (p *processor) Delete(account *gtsmodel.Account) error {
|
||||||
// 3. Delete account's emoji
|
// 3. Delete account's emoji
|
||||||
// 4. Delete account's follow requests
|
// 4. Delete account's follow requests
|
||||||
// 5. Delete account's follows
|
// 5. Delete account's follows
|
||||||
// 6. Delete account's media attachments
|
// 6. Delete account's statuses
|
||||||
// 7. Delete account's mentions
|
// 7. Delete account's media attachments
|
||||||
// 8. Delete account's notifications
|
// 8. Delete account's mentions
|
||||||
// 9. Delete account's polls
|
// 9. Delete account's polls
|
||||||
// 10. Delete account's statuses
|
// 10. Delete account's notifications
|
||||||
// 11. Delete account's bookmarks
|
// 11. Delete account's bookmarks
|
||||||
// 12. Delete account's faves
|
// 12. Delete account's faves
|
||||||
// 13. Delete account's mutes
|
// 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)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"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{}
|
targetAccount := >smodel.Account{}
|
||||||
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
|
@ -36,9 +36,9 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses := []gtsmodel.Status{}
|
|
||||||
apiStatuses := []apimodel.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 {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return apiStatuses, nil
|
return apiStatuses, nil
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +46,7 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range statuses {
|
for _, s := range statuses {
|
||||||
visible, err := p.filter.StatusVisible(&s, requestingAccount)
|
visible, err := p.filter.StatusVisible(s, requestingAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToMasto(&s, requestingAccount)
|
apiStatus, err := p.tc.StatusToMasto(s, requestingAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"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")
|
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 {
|
if err := p.deleteStatusFromTimelines(statusToDelete); err != nil {
|
||||||
return err
|
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 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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,27 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("note was not parseable as *gtsmodel.Status")
|
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)
|
return p.deleteStatusFromTimelines(statusToDelete)
|
||||||
case gtsmodel.ActivityStreamsProfile:
|
case gtsmodel.ActivityStreamsProfile:
|
||||||
// DELETE A PROFILE/ACCOUNT
|
// DELETE A PROFILE/ACCOUNT
|
||||||
|
|
|
||||||
|
|
@ -19,268 +19,23 @@
|
||||||
package processing
|
package processing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
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/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
|
func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
|
||||||
// First check this user/account is permitted to create media
|
return p.mediaProcessor.Create(authed.Account, form)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
|
func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
|
||||||
attachment := >smodel.MediaAttachment{}
|
return p.mediaProcessor.GetMedia(authed.Account, mediaAttachmentID)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
|
func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
|
||||||
attachment := >smodel.MediaAttachment{}
|
return p.mediaProcessor.Update(authed.Account, mediaAttachmentID, form)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
|
func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
|
||||||
// parse the form fields
|
return p.mediaProcessor.GetFile(authed.Account, form)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
internal/processing/media/create.go
Normal file
79
internal/processing/media/create.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
51
internal/processing/media/delete.go
Normal file
51
internal/processing/media/delete.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
120
internal/processing/media/getfile.go
Normal file
120
internal/processing/media/getfile.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
51
internal/processing/media/getmedia.go
Normal file
51
internal/processing/media/getmedia.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
63
internal/processing/media/media.go
Normal file
63
internal/processing/media/media.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
70
internal/processing/media/update.go
Normal file
70
internal/processing/media/update.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
63
internal/processing/media/util.go
Normal file
63
internal/processing/media/util.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
|
"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/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/streaming"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/streaming"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/timeline"
|
"github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||||
|
|
@ -219,6 +220,7 @@ type processor struct {
|
||||||
adminProcessor admin.Processor
|
adminProcessor admin.Processor
|
||||||
statusProcessor status.Processor
|
statusProcessor status.Processor
|
||||||
streamingProcessor streaming.Processor
|
streamingProcessor streaming.Processor
|
||||||
|
mediaProcessor mediaProcessor.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessor returns a new Processor that uses the given federator and logger
|
// 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)
|
streamingProcessor := streaming.New(db, tc, oauthServer, config, log)
|
||||||
accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator, config, log)
|
accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator, config, log)
|
||||||
adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config, log)
|
adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config, log)
|
||||||
|
mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config, log)
|
||||||
|
|
||||||
return &processor{
|
return &processor{
|
||||||
fromClientAPI: fromClientAPI,
|
fromClientAPI: fromClientAPI,
|
||||||
|
|
@ -251,6 +254,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
|
||||||
adminProcessor: adminProcessor,
|
adminProcessor: adminProcessor,
|
||||||
statusProcessor: statusProcessor,
|
statusProcessor: statusProcessor,
|
||||||
streamingProcessor: streamingProcessor,
|
streamingProcessor: streamingProcessor,
|
||||||
|
mediaProcessor: mediaProcessor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a
|
||||||
APActivityType: gtsmodel.ActivityStreamsDelete,
|
APActivityType: gtsmodel.ActivityStreamsDelete,
|
||||||
GTSModel: targetStatus,
|
GTSModel: targetStatus,
|
||||||
OriginAccount: account,
|
OriginAccount: account,
|
||||||
|
TargetAccount: account,
|
||||||
}
|
}
|
||||||
|
|
||||||
return mastoStatus, nil
|
return mastoStatus, nil
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue